(function() {
// 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 addressRowTemplate = $("#address-row-template");
var showIndex = true;
var showAddress = true;
var showPubKey = true;
var showPrivKey = true;
var entropyChangeTimeoutEvent = null;
var phraseChangeTimeoutEvent = null;
var rootKeyChangedTimeoutEvent = null;
var DOM = {};
DOM.network = $(".network");
DOM.phraseNetwork = $("#network-phrase");
DOM.useEntropy = $(".use-entropy");
DOM.entropyContainer = $(".entropy-container");
DOM.entropy = $(".entropy");
DOM.entropyError = $(".entropy-error");
DOM.phrase = $(".phrase");
DOM.passphrase = $(".passphrase");
DOM.generateContainer = $(".generate-container");
DOM.generate = $(".generate");
DOM.seed = $(".seed");
DOM.rootKey = $(".root-key");
DOM.extendedPrivKey = $(".extended-priv-key");
DOM.extendedPubKey = $(".extended-pub-key");
DOM.bip32tab = $("#bip32-tab");
DOM.bip44tab = $("#bip44-tab");
DOM.bip32panel = $("#bip32");
DOM.bip44panel = $("#bip44");
DOM.bip32path = $("#bip32-path");
DOM.bip44path = $("#bip44-path");
DOM.bip44purpose = $("#bip44 .purpose");
DOM.bip44coin = $("#bip44 .coin");
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");
DOM.feedback = $(".feedback");
DOM.tab = $(".derivation-type a");
DOM.indexToggle = $(".index-toggle");
DOM.addressToggle = $(".address-toggle");
DOM.publicKeyToggle = $(".public-key-toggle");
DOM.privateKeyToggle = $(".private-key-toggle");
DOM.languages = $(".languages a");
function init() {
// Events
DOM.network.on("change", networkChanged);
DOM.useEntropy.on("change", setEntropyVisibility);
DOM.entropy.on("input", delayedEntropyChanged);
DOM.phrase.on("input", delayedPhraseChanged);
DOM.passphrase.on("input", delayedPhraseChanged);
DOM.generate.on("click", generateClicked);
DOM.more.on("click", showMore);
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.publicKeyToggle.on("click", togglePublicKeys);
DOM.privateKeyToggle.on("click", togglePrivateKeys);
DOM.languages.on("click", languageChanged);
disableForms();
hidePending();
hideValidationError();
populateNetworkSelect();
}
// Event handlers
function networkChanged(e) {
var networkIndex = e.target.value;
networks[networkIndex].onSelect();
if (seed != null) {
phraseChanged();
}
else {
rootKeyChanged();
}
}
function setEntropyVisibility() {
if (isUsingOwnEntropy()) {
DOM.entropyContainer.removeClass("hidden");
DOM.generateContainer.addClass("hidden");
DOM.phrase.prop("readonly", true);
DOM.entropy.focus();
entropyChanged();
}
else {
DOM.entropyContainer.addClass("hidden");
DOM.generateContainer.removeClass("hidden");
DOM.phrase.prop("readonly", false);
hidePending();
}
}
function delayedPhraseChanged() {
hideValidationError();
showPending();
if (phraseChangeTimeoutEvent != null) {
clearTimeout(phraseChangeTimeoutEvent);
}
phraseChangeTimeoutEvent = setTimeout(phraseChanged, 400);
}
function phraseChanged() {
showPending();
hideValidationError();
setMnemonicLanguage();
// Get the mnemonic phrase
var phrase = DOM.phrase.val();
var errorText = findPhraseErrors(phrase);
if (errorText) {
showValidationError(errorText);
return;
}
// Calculate and display
var passphrase = DOM.passphrase.val();
calcBip32RootKeyFromSeed(phrase, passphrase);
calcForDerivationPath();
hidePending();
}
function delayedEntropyChanged() {
hideValidationError();
showPending();
if (entropyChangeTimeoutEvent != null) {
clearTimeout(entropyChangeTimeoutEvent);
}
entropyChangeTimeoutEvent = setTimeout(entropyChanged, 400);
}
function entropyChanged() {
// If blank entropy, clear mnemonic, addresses, errors
if (DOM.entropy.val().trim().length == 0) {
clearDisplay();
hideEntropyError();
DOM.phrase.val("");
showValidationError("Blank entropy");
return;
}
// Get the current phrase to detect changes
var phrase = DOM.phrase.val();
// Set the phrase from the entropy
setMnemonicFromEntropy();
// Recalc addresses if the phrase has changed
var newPhrase = DOM.phrase.val();
if (newPhrase != phrase) {
if (newPhrase.length == 0) {
clearDisplay();
}
else {
phraseChanged();
}
}
else {
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
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();
}
function generateClicked() {
if (isUsingOwnEntropy()) {
return;
}
clearDisplay();
showPending();
setTimeout(function() {
setMnemonicLanguage();
var phrase = generateRandomPhrase();
if (!phrase) {
return;
}
phraseChanged();
}, 50);
}
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() {
showIndex = !showIndex;
$("td.index span").toggleClass("invisible");
}
function toggleAddresses() {
showAddress = !showAddress;
$("td.address span").toggleClass("invisible");
}
function togglePublicKeys() {
showPubKey = !showPubKey;
$("td.pubkey span").toggleClass("invisible");
}
function togglePrivateKeys() {
showPrivKey = !showPrivKey;
$("td.privkey span").toggleClass("invisible");
}
// Private methods
function generateRandomPhrase() {
if (!hasStrongRandom()) {
var errorText = "This browser does not support strong randomness";
showValidationError(errorText);
return;
}
var numWords = parseInt(DOM.strength.val());
var strength = numWords / 3 * 32;
var words = mnemonic.generate(strength);
DOM.phrase.val(words);
return words;
}
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("/");
for (var i=0; i<pathBits.length; i++) {
var bit = pathBits[i];
var index = parseInt(bit);
if (isNaN(index)) {
continue;
}
var hardened = bit[bit.length-1] == "'";
if (hardened) {
bip32ExtendedKey = bip32ExtendedKey.deriveHardened(index);
}
else {
bip32ExtendedKey = bip32ExtendedKey.derive(index);
}
}
}
function showValidationError(errorText) {
DOM.feedback
.text(errorText)
.show();
}
function hideValidationError() {
DOM.feedback
.text("")
.hide();
}
function findPhraseErrors(phrase) {
// Preprocess the words
phrase = mnemonic.normalizeString(phrase);
var words = phraseToWordArray(phrase);
// Detect blank phrase
if (words.length == 0) {
return "Blank mnemonic";
}
// Check each word
for (var i=0; i<words.length; i++) {
var word = words[i];
var language = getLanguage();
if (WORDLISTS[language].indexOf(word) == -1) {
console.log("Finding closest match to " + word);
var nearestWord = findNearestWord(word);
return word + " not in wordlist, did you mean " + nearestWord + "?";
}
}
// Check the words are valid
var properPhrase = wordArrayToPhrase(words);
var isValid = mnemonic.check(properPhrase);
if (!isValid) {
return "Invalid mnemonic";
}
return false;
}
function validateRootKey(rootKeyBase58) {
try {
bitcoin.HDNode.fromBase58(rootKeyBase58);
}
catch (e) {
return "Invalid root key";
}
return "";
}
function getDerivationPath() {
if (DOM.bip44tab.hasClass("active")) {
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);
var derivationPath = DOM.bip44path.val();
console.log("Using derivation path from BIP44 tab: " + derivationPath);
return derivationPath;
}
else if (DOM.bip32tab.hasClass("active")) {
var derivationPath = DOM.bip32path.val();
console.log("Using derivation path from BIP32 tab: " + derivationPath);
return derivationPath;
}
else {
console.log("Unknown derivation path");
}
}
function findDerivationPathErrors(path) {
// TODO is not perfect but is better than nothing
// Inspired by
// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vectors
// and
// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#extended-keys
var maxDepth = 255; // TODO verify this!!
var maxIndexValue = Math.pow(2, 31); // TODO verify this!!
if (path[0] != "m") {
return "First character must be 'm'";
}
if (path.length > 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<indexes.length; depth++) {
var index = indexes[depth];
var invalidChars = index.replace(/^[0-9]+'?$/g, "")
if (invalidChars.length > 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();
DOM.extendedPrivKey.val(extendedPrivKey);
var extendedPubKey = bip32ExtendedKey.toBase58(false);
DOM.extendedPubKey.val(extendedPubKey);
// Display the addresses and privkeys
clearAddressesList();
displayAddresses(0, 20);
}
function displayAddresses(start, total) {
for (var i=0; i<total; i++) {
var index = i + start;
new TableRow(index);
}
}
function TableRow(index) {
var useHardenedAddresses = DOM.hardenedAddresses.prop("checked");
function init() {
calculateValues();
}
function calculateValues() {
setTimeout(function() {
var key = "";
if (useHardenedAddresses) {
key = bip32ExtendedKey.deriveHardened(index);
}
else {
key = bip32ExtendedKey.derive(index);
}
var address = key.getAddress().toString();
var privkey = key.privKey.toWIF(network);
var pubkey = key.pubKey.toHex();
var indexText = getDerivationPath() + "/" + index;
if (useHardenedAddresses) {
indexText = indexText + "'";
}
addAddressToList(indexText, address, pubkey, privkey);
}, 50)
}
init();
}
function showMore() {
var start = DOM.addresses.children().length;
var rowsToAdd = parseInt(DOM.rowsToAdd.val());
if (isNaN(rowsToAdd)) {
rowsToAdd = 20;
DOM.rowsToAdd.val("20");
}
if (rowsToAdd > 200) {
var msg = "Generating " + rowsToAdd + " rows could take a while. ";
msg += "Do you want to continue?";
if (!confirm(msg)) {
return;
}
}
displayAddresses(start, rowsToAdd);
}
function clearDisplay() {
clearAddressesList();
clearKey();
hideValidationError();
}
function clearAddressesList() {
DOM.addresses.empty();
}
function clearKey() {
DOM.rootKey.val("");
DOM.extendedPrivKey.val("");
DOM.extendedPubKey.val("");
}
function addAddressToList(indexText, address, pubkey, privkey) {
var row = $(addressRowTemplate.html());
// Elements
var indexCell = row.find(".index span");
var addressCell = row.find(".address span");
var pubkeyCell = row.find(".pubkey span");
var privkeyCell = row.find(".privkey span");
// Content
indexCell.text(indexText);
addressCell.text(address);
pubkeyCell.text(pubkey);
privkeyCell.text(privkey);
// Visibility
if (!showIndex) {
indexCell.addClass("invisible");
}
if (!showAddress) {
addressCell.addClass("invisible");
}
if (!showPubKey) {
pubkeyCell.addClass("invisible");
}
if (!showPrivKey) {
privkeyCell.addClass("invisible");
}
DOM.addresses.append(row);
}
function hasStrongRandom() {
return 'crypto' in window && window['crypto'] !== null;
}
function disableForms() {
$("form").on("submit", function(e) {
e.preventDefault();
});
}
function parseIntNoNaN(val, defaultVal) {
var v = parseInt(val);
if (isNaN(v)) {
return defaultVal;
}
return v;
}
function showPending() {
DOM.feedback
.text("Calculating...")
.show();
}
function findNearestWord(word) {
var language = getLanguage();
var words = WORDLISTS[language];
var minDistance = 99;
var closestWord = words[0];
for (var i=0; i<words.length; i++) {
var comparedTo = words[i];
var distance = Levenshtein.get(word, comparedTo);
if (distance < minDistance) {
closestWord = comparedTo;
minDistance = distance;
}
}
return closestWord;
}
function hidePending() {
DOM.feedback
.text("")
.hide();
}
function populateNetworkSelect() {
for (var i=0; i<networks.length; i++) {
var network = networks[i];
var option = $("<option>");
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<words.length; i++) {
var wordInLanguage = WORDLISTS[l].indexOf(words[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() {
for (var language in WORDLISTS) {
if (window.location.hash.indexOf(language) > -1) {
return language;
}
}
return "";
}
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<oldWords.length; i++) {
var oldWord = oldWords[i];
var index = WORDLISTS[oldLanguage].indexOf(oldWord);
var newWord = WORDLISTS[newLanguage][index];
newWords.push(newWord);
}
newPhrase = wordArrayToPhrase(newWords);
return newPhrase;
}
// TODO look at jsbip39 - mnemonic.splitWords
function phraseToWordArray(phrase) {
var words = phrase.split(/\s/g);
var noBlanks = [];
for (var i=0; i<words.length; i++) {
var word = words[i];
if (word.length > 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;
}
function isUsingOwnEntropy() {
return DOM.useEntropy.prop("checked");
}
function setMnemonicFromEntropy() {
hideEntropyError();
// Get entropy value
var entropyStr = DOM.entropy.val();
// Work out minimum base for entropy
var entropy = Entropy.fromString(entropyStr);
if (entropy.binaryStr.length == 0) {
return;
}
// Show entropy details
var extraBits = 32 - (entropy.binaryStr.length % 32);
var extraChars = Math.ceil(extraBits * Math.log(2) / Math.log(entropy.base.asInt));
var strength = "an extremely weak";
if (entropy.hexStr.length >= 8) {
strength = "a very weak";
}
if (entropy.hexStr.length >= 12) {
strength = "a weak";
}
if (entropy.hexStr.length >= 24) {
strength = "a strong";
}
if (entropy.hexStr.length >= 32) {
strength = "a very strong";
}
if (entropy.hexStr.length >= 40) {
strength = "an extremely strong";
}
if (entropy.hexStr.length >=48) {
strength = "an even stronger"
}
var msg = "Have " + entropy.binaryStr.length + " bits of entropy, " + extraChars + " more " + entropy.base.str + " chars required to generate " + strength + " mnemonic: " + entropy.cleanStr;
showEntropyError(msg);
// Discard trailing entropy
var hexStr = entropy.hexStr.substring(0, Math.floor(entropy.hexStr.length / 8) * 8);
// Convert entropy string to numeric array
var entropyArr = [];
for (var i=0; i<hexStr.length / 2; i++) {
var entropyByte = parseInt(hexStr[i*2].concat(hexStr[i*2+1]), 16);
entropyArr.push(entropyByte)
}
// Convert entropy array to mnemonic
var phrase = mnemonic.toMnemonic(entropyArr);
// Set the mnemonic in the UI
DOM.phrase.val(phrase);
}
function hideEntropyError() {
DOM.entropyError.addClass("hidden");
}
function showEntropyError(msg) {
DOM.entropyError.text(msg);
DOM.entropyError.removeClass("hidden");
}
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();
})();