(function() {
var mnemonic = new Mnemonic("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;
+ var showAddress = true;
+ var showPrivKey = true;
+
var phraseChangeTimeoutEvent = null;
var DOM = {};
+ DOM.network = $(".network");
+ DOM.phraseNetwork = $("#network-phrase");
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");
+ 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.addressToggle = $(".address-toggle");
DOM.privateKeyToggle = $(".private-key-toggle");
- var derivationPath = DOM.bip44path.val();
- var currentPhrase = DOM.phrase.val();
- var currentPassphrase = DOM.passphrase.val();
-
function init() {
// Events
- DOM.phrase.on("keyup", delayedPhraseChanged);
- DOM.passphrase.on("keyup", delayedPhraseChanged);
+ DOM.network.on("change", networkChanged);
+ DOM.phrase.on("input", delayedPhraseChanged);
+ DOM.passphrase.on("input", delayedPhraseChanged);
DOM.generate.on("click", generateClicked);
DOM.more.on("click", showMore);
- DOM.bip32path.on("keyup", bip32Changed);
- DOM.bip44purpose.on("keyup", bip44Changed);
- DOM.bip44coin.on("keyup", bip44Changed);
- DOM.bip44account.on("keyup", bip44Changed);
- DOM.bip44change.on("keyup", bip44Changed);
- DOM.tab.on("click", tabClicked);
+ DOM.bip32path.on("input", delayedPhraseChanged);
+ DOM.bip44purpose.on("input", delayedPhraseChanged);
+ DOM.bip44coin.on("input", delayedPhraseChanged);
+ DOM.bip44account.on("input", delayedPhraseChanged);
+ DOM.bip44change.on("input", delayedPhraseChanged);
+ DOM.tab.on("click", delayedPhraseChanged);
DOM.indexToggle.on("click", toggleIndexes);
DOM.addressToggle.on("click", toggleAddresses);
DOM.privateKeyToggle.on("click", togglePrivateKeys);
disableForms();
hidePending();
hideValidationError();
+ populateNetworkSelect();
}
// Event handlers
+ function networkChanged(e) {
+ var network = e.target.value;
+ networks[network].onSelect();
+ delayedPhraseChanged();
+ }
+
function delayedPhraseChanged() {
- if (!hasChanged()) {
- return;
- }
hideValidationError();
showPending();
if (phraseChangeTimeoutEvent != null) {
return;
}
// Get the derivation path
- var errorText = findDerivationPathErrors();
+ var derivationPath = getDerivationPath();
+ var errorText = findDerivationPathErrors(derivationPath);
if (errorText) {
showValidationError(errorText);
return;
calcBip32Seed(phrase, passphrase, derivationPath);
displayBip32Info();
hidePending();
- // Set current state so we only update as needed
- currentPhrase = phrase;
- currentPassphrase = passphrase;
}
function generateClicked() {
}, 50);
}
- function tabClicked(e) {
- var activePath = $(e.target.getAttribute("href") + " .path");
- derivationPath = activePath.val();
- derivationChanged();
- }
-
- function derivationChanged() {
- hideValidationError();
- showPending();
- setTimeout(phraseChanged, 50);
- }
-
- function bip32Changed() {
- derivationPath = DOM.bip32path.val();
- derivationChanged();
- }
-
- function bip44Changed() {
- setBip44DerivationPath();
- derivationPath = DOM.bip44path.val();
- derivationChanged();
- }
-
function toggleIndexes() {
+ showIndex = !showIndex;
$("td.index span").toggleClass("invisible");
}
function toggleAddresses() {
+ showAddress = !showAddress;
$("td.address span").toggleClass("invisible");
}
function togglePrivateKeys() {
+ showPrivKey = !showPrivKey;
$("td.privkey span").toggleClass("invisible");
}
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);
}
function calcBip32Seed(phrase, passphrase, path) {
- var seed = mnemonic.toSeed(phrase, passphrase);
- var seedHash = Bitcoin.crypto.sha256(seed).toString("hex");
- bip32RootKey = Bitcoin.HDNode.fromSeedHex(seedHash, network);
+ seed = mnemonic.toSeed(phrase, passphrase);
+ bip32RootKey = bitcoin.HDNode.fromSeedHex(seed, network);
bip32ExtendedKey = bip32RootKey;
// Derive the key from the path
var pathBits = path.split("/");
function findPhraseErrors(phrase) {
// TODO make this right
// Preprocess the words
+ phrase = mnemonic.normalizeString(phrase);
var parts = phrase.split(" ");
var proper = [];
for (var i=0; i<parts.length; i++) {
proper.push(part.toLowerCase());
}
}
- // TODO some levenstein on the words
var properPhrase = proper.join(' ');
+ // Check each word
+ for (var i=0; i<proper.length; i++) {
+ var word = proper[i];
+ if (WORDLISTS["english"].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 isValid = mnemonic.check(properPhrase);
if (!isValid) {
return false;
}
+ 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
+ // 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();
function displayAddresses(start, total) {
for (var i=0; i<total; i++) {
- var index = i+ start;
- var key = bip32ExtendedKey.derive(index);
- var address = key.getAddress().toString();
- var privkey = key.privKey.toWIF();
- addAddressToList(index, address, privkey);
+ var index = i + start;
+ new TableRow(index);
+ }
+ }
+
+ function TableRow(index) {
+
+ function init() {
+ calculateValues();
+ }
+
+ function calculateValues() {
+ setTimeout(function() {
+ var key = bip32ExtendedKey.derive(index);
+ var address = key.getAddress().toString();
+ var privkey = key.privKey.toWIF(network);
+ var indexText = getDerivationPath() + "/" + index;
+ addAddressToList(indexText, address, privkey);
+ }, 50)
}
+
+ init();
+
}
function showMore() {
return;
}
}
- showPending();
- setTimeout(function() {
displayAddresses(start, rowsToAdd);
- hidePending();
- }, 50);
}
function clearDisplay() {
DOM.extendedPubKey.val("");
}
- function addAddressToList(index, address, privkey) {
+ function addAddressToList(indexText, address, privkey) {
var row = $(addressRowTemplate.html());
- row.find(".index span").text(index);
- row.find(".address span").text(address);
- row.find(".privkey span").text(privkey);
+ // Elements
+ var indexCell = row.find(".index span");
+ var addressCell = row.find(".address span");
+ var privkeyCell = row.find(".privkey span");
+ // Content
+ indexCell.text(indexText);
+ addressCell.text(address);
+ privkeyCell.text(privkey);
+ // Visibility
+ if (!showIndex) {
+ indexCell.addClass("invisible");
+ }
+ if (!showAddress) {
+ addressCell.addClass("invisible");
+ }
+ if (!showPrivKey) {
+ privkeyCell.addClass("invisible");
+ }
DOM.addresses.append(row);
}
});
}
- 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)) {
.show();
}
+ function findNearestWord(word) {
+ var words = WORDLISTS["english"];
+ 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 hasChanged() {
- var phraseChanged = DOM.phrase.val() != currentPhrase;
- var passphraseChanged = DOM.passphrase.val() != currentPassphrase;
- return phraseChanged || passphraseChanged;
+ 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);
+ }
}
+ 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);
+ },
+ },
+ ]
+
init();
})();