aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--bip39-standalone.html143
-rw-r--r--readme.md10
-rw-r--r--src/index.html34
-rw-r--r--src/js/index.js109
-rw-r--r--tests.js523
5 files changed, 773 insertions, 46 deletions
diff --git a/bip39-standalone.html b/bip39-standalone.html
index b1fe90e..5f9cf7c 100644
--- a/bip39-standalone.html
+++ b/bip39-standalone.html
@@ -71,14 +71,14 @@
71 <div class="col-sm-10"> 71 <div class="col-sm-10">
72 <div class="input-group"> 72 <div class="input-group">
73 <select id="strength" class="strength form-control"> 73 <select id="strength" class="strength form-control">
74 <option val="3">3</option> 74 <option value="3">3</option>
75 <option val="6">6</option> 75 <option value="6">6</option>
76 <option val="9">9</option> 76 <option value="9">9</option>
77 <option val="12">12</option> 77 <option value="12">12</option>
78 <option val="15" selected>15</option> 78 <option value="15" selected>15</option>
79 <option val="18">18</option> 79 <option value="18">18</option>
80 <option val="21">21</option> 80 <option value="21">21</option>
81 <option val="24">24</option> 81 <option value="24">24</option>
82 </select> 82 </select>
83 <span class="input-group-btn"> 83 <span class="input-group-btn">
84 <button class="btn generate">Generate Random Mnemonic</button> 84 <button class="btn generate">Generate Random Mnemonic</button>
@@ -109,7 +109,7 @@
109 <div class="form-group"> 109 <div class="form-group">
110 <label for="root-key" class="col-sm-2 control-label">BIP32 Root Key</label> 110 <label for="root-key" class="col-sm-2 control-label">BIP32 Root Key</label>
111 <div class="col-sm-10"> 111 <div class="col-sm-10">
112 <textarea id="root-key" class="root-key form-control" readonly="readonly"></textarea> 112 <textarea id="root-key" class="root-key form-control"></textarea>
113 </div> 113 </div>
114 </div> 114 </div>
115 </form> 115 </form>
@@ -191,6 +191,13 @@
191 </div> 191 </div>
192 </div> 192 </div>
193 <div class="form-group"> 193 <div class="form-group">
194 <div class="col-sm-2"></div>
195 <label class="col-sm-10">
196 <input class="hardened-addresses" type="checkbox">
197 Use hardened addresses
198 </label>
199 </div>
200 <div class="form-group">
194 <label class="col-sm-2 control-label">Hive Wallet</label> 201 <label class="col-sm-2 control-label">Hive Wallet</label>
195 <div class="col-sm-10"> 202 <div class="col-sm-10">
196 <p class="form-control no-border"> 203 <p class="form-control no-border">
@@ -208,6 +215,15 @@
208 </p> 215 </p>
209 </div> 216 </div>
210 </div> 217 </div>
218 <div class="form-group">
219 <label for="core-path" class="col-sm-2 control-label">Bitcoin Core</label>
220 <div class="col-sm-10">
221 <p class="form-control no-border">
222 Use path <code>m/0'/0'</code> with hardened addresses.
223 For more info see the <a href="https://github.com/bitcoin/bitcoin/pull/8035" target="_blank">Bitcoin Core BIP32 implementation</a>
224 </p>
225 </div>
226 </div>
211 </form> 227 </form>
212 </div> 228 </div>
213 </div> 229 </div>
@@ -14830,6 +14846,7 @@ var Mnemonic = function(language) {
14830 var showPrivKey = true; 14846 var showPrivKey = true;
14831 14847
14832 var phraseChangeTimeoutEvent = null; 14848 var phraseChangeTimeoutEvent = null;
14849 var rootKeyChangedTimeoutEvent = null;
14833 14850
14834 var DOM = {}; 14851 var DOM = {};
14835 DOM.network = $(".network"); 14852 DOM.network = $(".network");
@@ -14852,6 +14869,7 @@ var Mnemonic = function(language) {
14852 DOM.bip44account = $("#bip44 .account"); 14869 DOM.bip44account = $("#bip44 .account");
14853 DOM.bip44change = $("#bip44 .change"); 14870 DOM.bip44change = $("#bip44 .change");
14854 DOM.strength = $(".strength"); 14871 DOM.strength = $(".strength");
14872 DOM.hardenedAddresses = $(".hardened-addresses");
14855 DOM.addresses = $(".addresses"); 14873 DOM.addresses = $(".addresses");
14856 DOM.rowsToAdd = $(".rows-to-add"); 14874 DOM.rowsToAdd = $(".rows-to-add");
14857 DOM.more = $(".more"); 14875 DOM.more = $(".more");
@@ -14868,12 +14886,14 @@ var Mnemonic = function(language) {
14868 DOM.passphrase.on("input", delayedPhraseChanged); 14886 DOM.passphrase.on("input", delayedPhraseChanged);
14869 DOM.generate.on("click", generateClicked); 14887 DOM.generate.on("click", generateClicked);
14870 DOM.more.on("click", showMore); 14888 DOM.more.on("click", showMore);
14871 DOM.bip32path.on("input", delayedPhraseChanged); 14889 DOM.rootKey.on("input", delayedRootKeyChanged);
14872 DOM.bip44purpose.on("input", delayedPhraseChanged); 14890 DOM.bip32path.on("input", calcForDerivationPath);
14873 DOM.bip44coin.on("input", delayedPhraseChanged); 14891 DOM.bip44purpose.on("input", calcForDerivationPath);
14874 DOM.bip44account.on("input", delayedPhraseChanged); 14892 DOM.bip44coin.on("input", calcForDerivationPath);
14875 DOM.bip44change.on("input", delayedPhraseChanged); 14893 DOM.bip44account.on("input", calcForDerivationPath);
14876 DOM.tab.on("click", delayedPhraseChanged); 14894 DOM.bip44change.on("input", calcForDerivationPath);
14895 DOM.tab.on("shown.bs.tab", calcForDerivationPath);
14896 DOM.hardenedAddresses.on("change", calcForDerivationPath);
14877 DOM.indexToggle.on("click", toggleIndexes); 14897 DOM.indexToggle.on("click", toggleIndexes);
14878 DOM.addressToggle.on("click", toggleAddresses); 14898 DOM.addressToggle.on("click", toggleAddresses);
14879 DOM.privateKeyToggle.on("click", togglePrivateKeys); 14899 DOM.privateKeyToggle.on("click", togglePrivateKeys);
@@ -14886,9 +14906,14 @@ var Mnemonic = function(language) {
14886 // Event handlers 14906 // Event handlers
14887 14907
14888 function networkChanged(e) { 14908 function networkChanged(e) {
14889 var network = e.target.value; 14909 var networkIndex = e.target.value;
14890 networks[network].onSelect(); 14910 networks[networkIndex].onSelect();
14891 delayedPhraseChanged(); 14911 if (seed != null) {
14912 phraseChanged();
14913 }
14914 else {
14915 rootKeyChanged();
14916 }
14892 } 14917 }
14893 14918
14894 function delayedPhraseChanged() { 14919 function delayedPhraseChanged() {
@@ -14905,12 +14930,57 @@ var Mnemonic = function(language) {
14905 hideValidationError(); 14930 hideValidationError();
14906 // Get the mnemonic phrase 14931 // Get the mnemonic phrase
14907 var phrase = DOM.phrase.val(); 14932 var phrase = DOM.phrase.val();
14908 var passphrase = DOM.passphrase.val();
14909 var errorText = findPhraseErrors(phrase); 14933 var errorText = findPhraseErrors(phrase);
14910 if (errorText) { 14934 if (errorText) {
14911 showValidationError(errorText); 14935 showValidationError(errorText);
14912 return; 14936 return;
14913 } 14937 }
14938 // Calculate and display
14939 var passphrase = DOM.passphrase.val();
14940 calcBip32RootKeyFromSeed(phrase, passphrase);
14941 calcForDerivationPath();
14942 hidePending();
14943 }
14944
14945 function delayedRootKeyChanged() {
14946 // Warn if there is an existing mnemonic or passphrase.
14947 if (DOM.phrase.val().length > 0 || DOM.passphrase.val().length > 0) {
14948 if (!confirm("This will clear existing mnemonic and passphrase")) {
14949 DOM.rootKey.val(bip32RootKey);
14950 return
14951 }
14952 }
14953 hideValidationError();
14954 showPending();
14955 // Clear existing mnemonic and passphrase
14956 DOM.phrase.val("");
14957 DOM.passphrase.val("");
14958 seed = null;
14959 if (rootKeyChangedTimeoutEvent != null) {
14960 clearTimeout(rootKeyChangedTimeoutEvent);
14961 }
14962 rootKeyChangedTimeoutEvent = setTimeout(rootKeyChanged, 400);
14963 }
14964
14965 function rootKeyChanged() {
14966 showPending();
14967 hideValidationError();
14968 // Validate the root key TODO
14969 var rootKeyBase58 = DOM.rootKey.val();
14970 var errorText = validateRootKey(rootKeyBase58);
14971 if (errorText) {
14972 showValidationError(errorText);
14973 return;
14974 }
14975 // Calculate and display
14976 calcBip32RootKeyFromBase58(rootKeyBase58);
14977 calcForDerivationPath();
14978 hidePending();
14979 }
14980
14981 function calcForDerivationPath() {
14982 showPending();
14983 hideValidationError();
14914 // Get the derivation path 14984 // Get the derivation path
14915 var derivationPath = getDerivationPath(); 14985 var derivationPath = getDerivationPath();
14916 var errorText = findDerivationPathErrors(derivationPath); 14986 var errorText = findDerivationPathErrors(derivationPath);
@@ -14918,8 +14988,7 @@ var Mnemonic = function(language) {
14918 showValidationError(errorText); 14988 showValidationError(errorText);
14919 return; 14989 return;
14920 } 14990 }
14921 // Calculate and display 14991 calcBip32ExtendedKey(derivationPath);
14922 calcBip32Seed(phrase, passphrase, derivationPath);
14923 displayBip32Info(); 14992 displayBip32Info();
14924 hidePending(); 14993 hidePending();
14925 } 14994 }
@@ -14966,9 +15035,16 @@ var Mnemonic = function(language) {
14966 return words; 15035 return words;
14967 } 15036 }
14968 15037
14969 function calcBip32Seed(phrase, passphrase, path) { 15038 function calcBip32RootKeyFromSeed(phrase, passphrase) {
14970 seed = mnemonic.toSeed(phrase, passphrase); 15039 seed = mnemonic.toSeed(phrase, passphrase);
14971 bip32RootKey = bitcoin.HDNode.fromSeedHex(seed, network); 15040 bip32RootKey = bitcoin.HDNode.fromSeedHex(seed, network);
15041 }
15042
15043 function calcBip32RootKeyFromBase58(rootKeyBase58) {
15044 bip32RootKey = bitcoin.HDNode.fromBase58(rootKeyBase58);
15045 }
15046
15047 function calcBip32ExtendedKey(path) {
14972 bip32ExtendedKey = bip32RootKey; 15048 bip32ExtendedKey = bip32RootKey;
14973 // Derive the key from the path 15049 // Derive the key from the path
14974 var pathBits = path.split("/"); 15050 var pathBits = path.split("/");
@@ -15031,6 +15107,16 @@ var Mnemonic = function(language) {
15031 return false; 15107 return false;
15032 } 15108 }
15033 15109
15110 function validateRootKey(rootKeyBase58) {
15111 try {
15112 bitcoin.HDNode.fromBase58(rootKeyBase58);
15113 }
15114 catch (e) {
15115 return "Invalid root key";
15116 }
15117 return "";
15118 }
15119
15034 function getDerivationPath() { 15120 function getDerivationPath() {
15035 if (DOM.bip44tab.hasClass("active")) { 15121 if (DOM.bip44tab.hasClass("active")) {
15036 var purpose = parseIntNoNaN(DOM.bip44purpose.val(), 44); 15122 var purpose = parseIntNoNaN(DOM.bip44purpose.val(), 44);
@@ -15117,16 +15203,27 @@ var Mnemonic = function(language) {
15117 15203
15118 function TableRow(index) { 15204 function TableRow(index) {
15119 15205
15206 var useHardenedAddresses = DOM.hardenedAddresses.prop("checked");
15207
15120 function init() { 15208 function init() {
15121 calculateValues(); 15209 calculateValues();
15122 } 15210 }
15123 15211
15124 function calculateValues() { 15212 function calculateValues() {
15125 setTimeout(function() { 15213 setTimeout(function() {
15126 var key = bip32ExtendedKey.derive(index); 15214 var key = "";
15215 if (useHardenedAddresses) {
15216 key = bip32ExtendedKey.deriveHardened(index);
15217 }
15218 else {
15219 key = bip32ExtendedKey.derive(index);
15220 }
15127 var address = key.getAddress().toString(); 15221 var address = key.getAddress().toString();
15128 var privkey = key.privKey.toWIF(network); 15222 var privkey = key.privKey.toWIF(network);
15129 var indexText = getDerivationPath() + "/" + index; 15223 var indexText = getDerivationPath() + "/" + index;
15224 if (useHardenedAddresses) {
15225 indexText = indexText + "'";
15226 }
15130 addAddressToList(indexText, address, privkey); 15227 addAddressToList(indexText, address, privkey);
15131 }, 50) 15228 }, 50)
15132 } 15229 }
diff --git a/readme.md b/readme.md
index 210703b..02e1e18 100644
--- a/readme.md
+++ b/readme.md
@@ -34,3 +34,13 @@ Please do not make modifications to `bip39-standalone.html`, since they will
34be overwritten by `compile.py`. 34be overwritten by `compile.py`.
35 35
36Make changes in `src/*` and apply them using the command `python compile.py` 36Make changes in `src/*` and apply them using the command `python compile.py`
37
38# Tests
39
40Tests depend on [phantomjs](http://phantomjs.org/).
41
42Run tests from the command-line
43
44```
45$ phantomjs tests.js
46```
diff --git a/src/index.html b/src/index.html
index 6708675..ea7096c 100644
--- a/src/index.html
+++ b/src/index.html
@@ -67,14 +67,14 @@
67 <div class="col-sm-10"> 67 <div class="col-sm-10">
68 <div class="input-group"> 68 <div class="input-group">
69 <select id="strength" class="strength form-control"> 69 <select id="strength" class="strength form-control">
70 <option val="3">3</option> 70 <option value="3">3</option>
71 <option val="6">6</option> 71 <option value="6">6</option>
72 <option val="9">9</option> 72 <option value="9">9</option>
73 <option val="12">12</option> 73 <option value="12">12</option>
74 <option val="15" selected>15</option> 74 <option value="15" selected>15</option>
75 <option val="18">18</option> 75 <option value="18">18</option>
76 <option val="21">21</option> 76 <option value="21">21</option>
77 <option val="24">24</option> 77 <option value="24">24</option>
78 </select> 78 </select>
79 <span class="input-group-btn"> 79 <span class="input-group-btn">
80 <button class="btn generate">Generate Random Mnemonic</button> 80 <button class="btn generate">Generate Random Mnemonic</button>
@@ -105,7 +105,7 @@
105 <div class="form-group"> 105 <div class="form-group">
106 <label for="root-key" class="col-sm-2 control-label">BIP32 Root Key</label> 106 <label for="root-key" class="col-sm-2 control-label">BIP32 Root Key</label>
107 <div class="col-sm-10"> 107 <div class="col-sm-10">
108 <textarea id="root-key" class="root-key form-control" readonly="readonly"></textarea> 108 <textarea id="root-key" class="root-key form-control"></textarea>
109 </div> 109 </div>
110 </div> 110 </div>
111 </form> 111 </form>
@@ -187,6 +187,13 @@
187 </div> 187 </div>
188 </div> 188 </div>
189 <div class="form-group"> 189 <div class="form-group">
190 <div class="col-sm-2"></div>
191 <label class="col-sm-10">
192 <input class="hardened-addresses" type="checkbox">
193 Use hardened addresses
194 </label>
195 </div>
196 <div class="form-group">
190 <label class="col-sm-2 control-label">Hive Wallet</label> 197 <label class="col-sm-2 control-label">Hive Wallet</label>
191 <div class="col-sm-10"> 198 <div class="col-sm-10">
192 <p class="form-control no-border"> 199 <p class="form-control no-border">
@@ -204,6 +211,15 @@
204 </p> 211 </p>
205 </div> 212 </div>
206 </div> 213 </div>
214 <div class="form-group">
215 <label for="core-path" class="col-sm-2 control-label">Bitcoin Core</label>
216 <div class="col-sm-10">
217 <p class="form-control no-border">
218 Use path <code>m/0'/0'</code> with hardened addresses.
219 For more info see the <a href="https://github.com/bitcoin/bitcoin/pull/8035" target="_blank">Bitcoin Core BIP32 implementation</a>
220 </p>
221 </div>
222 </div>
207 </form> 223 </form>
208 </div> 224 </div>
209 </div> 225 </div>
diff --git a/src/js/index.js b/src/js/index.js
index f3582b0..8f7d658 100644
--- a/src/js/index.js
+++ b/src/js/index.js
@@ -12,6 +12,7 @@
12 var showPrivKey = true; 12 var showPrivKey = true;
13 13
14 var phraseChangeTimeoutEvent = null; 14 var phraseChangeTimeoutEvent = null;
15 var rootKeyChangedTimeoutEvent = null;
15 16
16 var DOM = {}; 17 var DOM = {};
17 DOM.network = $(".network"); 18 DOM.network = $(".network");
@@ -34,6 +35,7 @@
34 DOM.bip44account = $("#bip44 .account"); 35 DOM.bip44account = $("#bip44 .account");
35 DOM.bip44change = $("#bip44 .change"); 36 DOM.bip44change = $("#bip44 .change");
36 DOM.strength = $(".strength"); 37 DOM.strength = $(".strength");
38 DOM.hardenedAddresses = $(".hardened-addresses");
37 DOM.addresses = $(".addresses"); 39 DOM.addresses = $(".addresses");
38 DOM.rowsToAdd = $(".rows-to-add"); 40 DOM.rowsToAdd = $(".rows-to-add");
39 DOM.more = $(".more"); 41 DOM.more = $(".more");
@@ -50,12 +52,14 @@
50 DOM.passphrase.on("input", delayedPhraseChanged); 52 DOM.passphrase.on("input", delayedPhraseChanged);
51 DOM.generate.on("click", generateClicked); 53 DOM.generate.on("click", generateClicked);
52 DOM.more.on("click", showMore); 54 DOM.more.on("click", showMore);
53 DOM.bip32path.on("input", delayedPhraseChanged); 55 DOM.rootKey.on("input", delayedRootKeyChanged);
54 DOM.bip44purpose.on("input", delayedPhraseChanged); 56 DOM.bip32path.on("input", calcForDerivationPath);
55 DOM.bip44coin.on("input", delayedPhraseChanged); 57 DOM.bip44purpose.on("input", calcForDerivationPath);
56 DOM.bip44account.on("input", delayedPhraseChanged); 58 DOM.bip44coin.on("input", calcForDerivationPath);
57 DOM.bip44change.on("input", delayedPhraseChanged); 59 DOM.bip44account.on("input", calcForDerivationPath);
58 DOM.tab.on("click", delayedPhraseChanged); 60 DOM.bip44change.on("input", calcForDerivationPath);
61 DOM.tab.on("shown.bs.tab", calcForDerivationPath);
62 DOM.hardenedAddresses.on("change", calcForDerivationPath);
59 DOM.indexToggle.on("click", toggleIndexes); 63 DOM.indexToggle.on("click", toggleIndexes);
60 DOM.addressToggle.on("click", toggleAddresses); 64 DOM.addressToggle.on("click", toggleAddresses);
61 DOM.privateKeyToggle.on("click", togglePrivateKeys); 65 DOM.privateKeyToggle.on("click", togglePrivateKeys);
@@ -68,9 +72,14 @@
68 // Event handlers 72 // Event handlers
69 73
70 function networkChanged(e) { 74 function networkChanged(e) {
71 var network = e.target.value; 75 var networkIndex = e.target.value;
72 networks[network].onSelect(); 76 networks[networkIndex].onSelect();
73 delayedPhraseChanged(); 77 if (seed != null) {
78 phraseChanged();
79 }
80 else {
81 rootKeyChanged();
82 }
74 } 83 }
75 84
76 function delayedPhraseChanged() { 85 function delayedPhraseChanged() {
@@ -87,12 +96,57 @@
87 hideValidationError(); 96 hideValidationError();
88 // Get the mnemonic phrase 97 // Get the mnemonic phrase
89 var phrase = DOM.phrase.val(); 98 var phrase = DOM.phrase.val();
90 var passphrase = DOM.passphrase.val();
91 var errorText = findPhraseErrors(phrase); 99 var errorText = findPhraseErrors(phrase);
92 if (errorText) { 100 if (errorText) {
93 showValidationError(errorText); 101 showValidationError(errorText);
94 return; 102 return;
95 } 103 }
104 // Calculate and display
105 var passphrase = DOM.passphrase.val();
106 calcBip32RootKeyFromSeed(phrase, passphrase);
107 calcForDerivationPath();
108 hidePending();
109 }
110
111 function delayedRootKeyChanged() {
112 // Warn if there is an existing mnemonic or passphrase.
113 if (DOM.phrase.val().length > 0 || DOM.passphrase.val().length > 0) {
114 if (!confirm("This will clear existing mnemonic and passphrase")) {
115 DOM.rootKey.val(bip32RootKey);
116 return
117 }
118 }
119 hideValidationError();
120 showPending();
121 // Clear existing mnemonic and passphrase
122 DOM.phrase.val("");
123 DOM.passphrase.val("");
124 seed = null;
125 if (rootKeyChangedTimeoutEvent != null) {
126 clearTimeout(rootKeyChangedTimeoutEvent);
127 }
128 rootKeyChangedTimeoutEvent = setTimeout(rootKeyChanged, 400);
129 }
130
131 function rootKeyChanged() {
132 showPending();
133 hideValidationError();
134 // Validate the root key TODO
135 var rootKeyBase58 = DOM.rootKey.val();
136 var errorText = validateRootKey(rootKeyBase58);
137 if (errorText) {
138 showValidationError(errorText);
139 return;
140 }
141 // Calculate and display
142 calcBip32RootKeyFromBase58(rootKeyBase58);
143 calcForDerivationPath();
144 hidePending();
145 }
146
147 function calcForDerivationPath() {
148 showPending();
149 hideValidationError();
96 // Get the derivation path 150 // Get the derivation path
97 var derivationPath = getDerivationPath(); 151 var derivationPath = getDerivationPath();
98 var errorText = findDerivationPathErrors(derivationPath); 152 var errorText = findDerivationPathErrors(derivationPath);
@@ -100,8 +154,7 @@
100 showValidationError(errorText); 154 showValidationError(errorText);
101 return; 155 return;
102 } 156 }
103 // Calculate and display 157 calcBip32ExtendedKey(derivationPath);
104 calcBip32Seed(phrase, passphrase, derivationPath);
105 displayBip32Info(); 158 displayBip32Info();
106 hidePending(); 159 hidePending();
107 } 160 }
@@ -148,9 +201,16 @@
148 return words; 201 return words;
149 } 202 }
150 203
151 function calcBip32Seed(phrase, passphrase, path) { 204 function calcBip32RootKeyFromSeed(phrase, passphrase) {
152 seed = mnemonic.toSeed(phrase, passphrase); 205 seed = mnemonic.toSeed(phrase, passphrase);
153 bip32RootKey = bitcoin.HDNode.fromSeedHex(seed, network); 206 bip32RootKey = bitcoin.HDNode.fromSeedHex(seed, network);
207 }
208
209 function calcBip32RootKeyFromBase58(rootKeyBase58) {
210 bip32RootKey = bitcoin.HDNode.fromBase58(rootKeyBase58);
211 }
212
213 function calcBip32ExtendedKey(path) {
154 bip32ExtendedKey = bip32RootKey; 214 bip32ExtendedKey = bip32RootKey;
155 // Derive the key from the path 215 // Derive the key from the path
156 var pathBits = path.split("/"); 216 var pathBits = path.split("/");
@@ -213,6 +273,16 @@
213 return false; 273 return false;
214 } 274 }
215 275
276 function validateRootKey(rootKeyBase58) {
277 try {
278 bitcoin.HDNode.fromBase58(rootKeyBase58);
279 }
280 catch (e) {
281 return "Invalid root key";
282 }
283 return "";
284 }
285
216 function getDerivationPath() { 286 function getDerivationPath() {
217 if (DOM.bip44tab.hasClass("active")) { 287 if (DOM.bip44tab.hasClass("active")) {
218 var purpose = parseIntNoNaN(DOM.bip44purpose.val(), 44); 288 var purpose = parseIntNoNaN(DOM.bip44purpose.val(), 44);
@@ -299,16 +369,27 @@
299 369
300 function TableRow(index) { 370 function TableRow(index) {
301 371
372 var useHardenedAddresses = DOM.hardenedAddresses.prop("checked");
373
302 function init() { 374 function init() {
303 calculateValues(); 375 calculateValues();
304 } 376 }
305 377
306 function calculateValues() { 378 function calculateValues() {
307 setTimeout(function() { 379 setTimeout(function() {
308 var key = bip32ExtendedKey.derive(index); 380 var key = "";
381 if (useHardenedAddresses) {
382 key = bip32ExtendedKey.deriveHardened(index);
383 }
384 else {
385 key = bip32ExtendedKey.derive(index);
386 }
309 var address = key.getAddress().toString(); 387 var address = key.getAddress().toString();
310 var privkey = key.privKey.toWIF(network); 388 var privkey = key.privKey.toWIF(network);
311 var indexText = getDerivationPath() + "/" + index; 389 var indexText = getDerivationPath() + "/" + index;
390 if (useHardenedAddresses) {
391 indexText = indexText + "'";
392 }
312 addAddressToList(indexText, address, privkey); 393 addAddressToList(indexText, address, privkey);
313 }, 50) 394 }, 50)
314 } 395 }
diff --git a/tests.js b/tests.js
new file mode 100644
index 0000000..f5ce6d3
--- /dev/null
+++ b/tests.js
@@ -0,0 +1,523 @@
1// Usage:
2// $ phantomjs tests.js
3
4
5var page = require('webpage').create();
6var url = 'src/index.html';
7
8page.onResourceError = function(e) {
9 console.log("Error loading " + e.url);
10 phantom.exit();
11}
12
13function fail() {
14 console.log("Failed");
15 phantom.exit();
16}
17
18function next() {
19 if (tests.length > 0) {
20 var testsStr = tests.length == 1 ? "test" : "tests";
21 console.log(tests.length + " " + testsStr + " remaining");
22 tests.shift()();
23 }
24 else {
25 console.log("Finished with 0 failures");
26 phantom.exit();
27 }
28}
29
30tests = [
31
32// Page loads with status of 'success'
33function() {
34page.open(url, function(status) {
35 if (status != "success") {
36 console.log("Page did not load with status 'success'");
37 fail();
38 }
39 next();
40});
41},
42
43// Page has text
44function() {
45page.open(url, function(status) {
46 var content = page.evaluate(function() {
47 return document.body.textContent.trim();
48 });
49 if (!content) {
50 console.log("Page does not have text");
51 fail();
52 }
53 next();
54});
55},
56
57// Entering mnemonic generates addresses
58function() {
59page.open(url, function(status) {
60 var expected = "1Di3Vp7tBWtyQaDABLAjfWtF6V7hYKJtug";
61 // set the phrase
62 page.evaluate(function() {
63 $(".phrase").val("abandon abandon ability").trigger("input");
64 });
65 // get the address
66 setTimeout(function() {
67 var actual = page.evaluate(function() {
68 return $(".address:first").text();
69 });
70 if (actual != expected) {
71 console.log("Mnemonic did not generate address");
72 console.log("Expected: " + expected);
73 console.log("Got: " + actual);
74 fail();
75 }
76 next();
77 }, 1000);
78});
79},
80
81// Random button generates random mnemonic
82function() {
83page.open(url, function(status) {
84 // check initial phrase is empty
85 var phrase = page.evaluate(function() {
86 return $(".phrase").text();
87 });
88 if (phrase != "") {
89 console.log("Initial phrase is not blank");
90 fail();
91 }
92 // press the 'generate' button
93 page.evaluate(function() {
94 $(".generate").click();
95 });
96 // get the new phrase
97 setTimeout(function() {
98 var phrase = page.evaluate(function() {
99 return $(".phrase").val();
100 });
101 if (phrase.length <= 0) {
102 console.log("Phrase not generated by pressing button");
103 fail();
104 }
105 next();
106 }, 1000);
107});
108},
109
110// Mnemonic length can be customized
111function() {
112page.open(url, function(status) {
113 // set the length to 6
114 var expectedLength = "6";
115 page.evaluate(function() {
116 $(".strength option[selected]").removeAttr("selected");
117 $(".strength option[value=6]").prop("selected", true);
118 });
119 // press the 'generate' button
120 page.evaluate(function() {
121 $(".generate").click();
122 });
123 // check the new phrase is six words long
124 setTimeout(function() {
125 var actualLength = page.evaluate(function() {
126 var words = $(".phrase").val().split(" ");
127 return words.length;
128 });
129 if (actualLength != expectedLength) {
130 console.log("Phrase not generated with correct length");
131 console.log("Expected: " + expectedLength);
132 console.log("Actual: " + actualLength);
133 fail();
134 }
135 next();
136 }, 1000);
137});
138},
139
140// Passphrase can be set
141function() {
142page.open(url, function(status) {
143 // set the phrase and passphrase
144 var expected = "15pJzUWPGzR7avffV9nY5by4PSgSKG9rba";
145 page.evaluate(function() {
146 $(".phrase").val("abandon abandon ability");
147 $(".passphrase").val("secure_passphrase").trigger("input");
148 });
149 // check the address is generated correctly
150 setTimeout(function() {
151 var actual = page.evaluate(function() {
152 return $(".address:first").text();
153 });
154 if (actual != expected) {
155 console.log("Passphrase results in wrong address");
156 console.log("Expected: " + expected);
157 console.log("Actual: " + actual);
158 fail();
159 }
160 next();
161 }, 1000);
162});
163},
164
165// Network can be set to bitcoin testnet
166function() {
167page.open(url, function(status) {
168 // set the phrase and coin
169 var expected = "mucaU5iiDaJDb69BHLeDv8JFfGiyg2nJKi";
170 page.evaluate(function() {
171 $(".phrase").val("abandon abandon ability");
172 $(".phrase").trigger("input");
173 $(".network option[selected]").removeAttr("selected");
174 $(".network option[value=1]").prop("selected", true);
175 $(".network").trigger("change");
176 });
177 // check the address is generated correctly
178 setTimeout(function() {
179 var actual = page.evaluate(function() {
180 return $(".address:first").text();
181 });
182 if (actual != expected) {
183 console.log("Bitcoin testnet address is incorrect");
184 console.log("Expected: " + expected);
185 console.log("Actual: " + actual);
186 fail();
187 }
188 next();
189 }, 1000);
190});
191},
192
193// Network can be set to litecoin
194function() {
195page.open(url, function(status) {
196 // set the phrase and coin
197 var expected = "LQ4XU8RX2ULPmPq9FcUHdVmPVchP9nwXdn";
198 page.evaluate(function() {
199 $(".phrase").val("abandon abandon ability");
200 $(".phrase").trigger("input");
201 $(".network option[selected]").removeAttr("selected");
202 $(".network option[value=2]").prop("selected", true);
203 $(".network").trigger("change");
204 });
205 // check the address is generated correctly
206 setTimeout(function() {
207 var actual = page.evaluate(function() {
208 return $(".address:first").text();
209 });
210 if (actual != expected) {
211 console.log("Litecoin address is incorrect");
212 console.log("Expected: " + expected);
213 console.log("Actual: " + actual);
214 fail();
215 }
216 next();
217 }, 1000);
218});
219},
220
221// Network can be set to dogecoin
222function() {
223page.open(url, function(status) {
224 // set the phrase and coin
225 var expected = "DPQH2AtuzkVSG6ovjKk4jbUmZ6iXLpgbJA";
226 page.evaluate(function() {
227 $(".phrase").val("abandon abandon ability");
228 $(".phrase").trigger("input");
229 $(".network option[selected]").removeAttr("selected");
230 $(".network option[value=3]").prop("selected", true);
231 $(".network").trigger("change");
232 });
233 // check the address is generated correctly
234 setTimeout(function() {
235 var actual = page.evaluate(function() {
236 return $(".address:first").text();
237 });
238 if (actual != expected) {
239 console.log("Dogecoin address is incorrect");
240 console.log("Expected: " + expected);
241 console.log("Actual: " + actual);
242 fail();
243 }
244 next();
245 }, 1000);
246});
247},
248
249// Network can be set to shadowcash
250function() {
251page.open(url, function(status) {
252 // set the phrase and coin
253 var expected = "SiSZtfYAXEFvMm3XM8hmtkGDyViRwErtCG";
254 page.evaluate(function() {
255 $(".phrase").val("abandon abandon ability");
256 $(".phrase").trigger("input");
257 $(".network option[selected]").removeAttr("selected");
258 $(".network option[value=4]").prop("selected", true);
259 $(".network").trigger("change");
260 });
261 // check the address is generated correctly
262 setTimeout(function() {
263 var actual = page.evaluate(function() {
264 return $(".address:first").text();
265 });
266 if (actual != expected) {
267 console.log("Shadowcash address is incorrect");
268 console.log("Expected: " + expected);
269 console.log("Actual: " + actual);
270 fail();
271 }
272 next();
273 }, 1000);
274});
275},
276
277// Network can be set to shadowcash testnet
278function() {
279page.open(url, function(status) {
280 // set the phrase and coin
281 var expected = "tM2EDpVKaTiEg2NZg3yKg8eqjLr55BErHe";
282 page.evaluate(function() {
283 $(".phrase").val("abandon abandon ability");
284 $(".phrase").trigger("input");
285 $(".network option[selected]").removeAttr("selected");
286 $(".network option[value=5]").prop("selected", true);
287 $(".network").trigger("change");
288 });
289 // check the address is generated correctly
290 setTimeout(function() {
291 var actual = page.evaluate(function() {
292 return $(".address:first").text();
293 });
294 if (actual != expected) {
295 console.log("Shadowcash testnet address is incorrect");
296 console.log("Expected: " + expected);
297 console.log("Actual: " + actual);
298 fail();
299 }
300 next();
301 }, 1000);
302});
303},
304
305// Network can be set to viacoin
306function() {
307page.open(url, function(status) {
308 // set the phrase and coin
309 var expected = "Vq9Eq4N5SQnjqZvxtxzo7hZPW5XnyJsmXT";
310 page.evaluate(function() {
311 $(".phrase").val("abandon abandon ability");
312 $(".phrase").trigger("input");
313 $(".network option[selected]").removeAttr("selected");
314 $(".network option[value=6]").prop("selected", true);
315 $(".network").trigger("change");
316 });
317 // check the address is generated correctly
318 setTimeout(function() {
319 var actual = page.evaluate(function() {
320 return $(".address:first").text();
321 });
322 if (actual != expected) {
323 console.log("Viacoin address is incorrect");
324 console.log("Expected: " + expected);
325 console.log("Actual: " + actual);
326 fail();
327 }
328 next();
329 }, 1000);
330});
331},
332
333// Network can be set to viacoin testnet
334function() {
335page.open(url, function(status) {
336 // set the phrase and coin
337 var expected = "tM2EDpVKaTiEg2NZg3yKg8eqjLr55BErHe";
338 page.evaluate(function() {
339 $(".phrase").val("abandon abandon ability");
340 $(".phrase").trigger("input");
341 $(".network option[selected]").removeAttr("selected");
342 $(".network option[value=7]").prop("selected", true);
343 $(".network").trigger("change");
344 });
345 // check the address is generated correctly
346 setTimeout(function() {
347 var actual = page.evaluate(function() {
348 return $(".address:first").text();
349 });
350 if (actual != expected) {
351 console.log("Viacoin testnet address is incorrect");
352 console.log("Expected: " + expected);
353 console.log("Actual: " + actual);
354 fail();
355 }
356 next();
357 }, 1000);
358});
359},
360
361// Network can be set to jumbucks
362function() {
363page.open(url, function(status) {
364 // set the phrase and coin
365 var expected = "JLEXccwDXADK4RxBPkRez7mqsHVoJBEUew";
366 page.evaluate(function() {
367 $(".phrase").val("abandon abandon ability");
368 $(".phrase").trigger("input");
369 $(".network option[selected]").removeAttr("selected");
370 $(".network option[value=8]").prop("selected", true);
371 $(".network").trigger("change");
372 });
373 // check the address is generated correctly
374 setTimeout(function() {
375 var actual = page.evaluate(function() {
376 return $(".address:first").text();
377 });
378 if (actual != expected) {
379 console.log("Jumbucks address is incorrect");
380 console.log("Expected: " + expected);
381 console.log("Actual: " + actual);
382 fail();
383 }
384 next();
385 }, 1000);
386});
387},
388
389// Network can be set to clam
390function() {
391page.open(url, function(status) {
392 // set the phrase and coin
393 var expected = "xCp4sakjVx4pUAZ6cBCtuin8Ddb6U1sk9y";
394 page.evaluate(function() {
395 $(".phrase").val("abandon abandon ability");
396 $(".phrase").trigger("input");
397 $(".network option[selected]").removeAttr("selected");
398 $(".network option[value=9]").prop("selected", true);
399 $(".network").trigger("change");
400 });
401 // check the address is generated correctly
402 setTimeout(function() {
403 var actual = page.evaluate(function() {
404 return $(".address:first").text();
405 });
406 if (actual != expected) {
407 console.log("CLAM address is incorrect");
408 console.log("Expected: " + expected);
409 console.log("Actual: " + actual);
410 fail();
411 }
412 next();
413 }, 1000);
414});
415},
416
417// BIP39 seed is set from phrase
418function() {
419page.open(url, function(status) {
420 // set the phrase
421 var expected = "20da140d3dd1df8713cefcc4d54ce0e445b4151027a1ab567b832f6da5fcc5afc1c3a3f199ab78b8e0ab4652efd7f414ac2c9a3b81bceb879a70f377aa0a58f3";
422 page.evaluate(function() {
423 $(".phrase").val("abandon abandon ability");
424 $(".phrase").trigger("input");
425 });
426 // check the address is generated correctly
427 setTimeout(function() {
428 var actual = page.evaluate(function() {
429 return $(".seed").val();
430 });
431 if (actual != expected) {
432 console.log("BIP39 seed is incorrectly generated from mnemonic");
433 console.log("Expected: " + expected);
434 console.log("Actual: " + actual);
435 fail();
436 }
437 next();
438 }, 1000);
439});
440},
441
442// BIP32 root key is set from phrase
443function() {
444page.open(url, function(status) {
445 // set the phrase
446 var expected = "xprv9s21ZrQH143K2jkGDCeTLgRewT9F2pH5JZs2zDmmjXes34geVnFiuNa8KTvY5WoYvdn4Ag6oYRoB6cXtc43NgJAEqDXf51xPm6fhiMCKwpi";
447 page.evaluate(function() {
448 $(".phrase").val("abandon abandon ability");
449 $(".phrase").trigger("input");
450 });
451 // check the address is generated correctly
452 setTimeout(function() {
453 var actual = page.evaluate(function() {
454 return $(".root-key").val();
455 });
456 if (actual != expected) {
457 console.log("Root key is incorrectly generated from mnemonic");
458 console.log("Expected: " + expected);
459 console.log("Actual: " + actual);
460 fail();
461 }
462 next();
463 }, 1000);
464});
465},
466
467// TODO finish these tests
468
469// Tabs show correct addresses when changed
470
471// BIP44 derivation path is shown
472// BIP44 extended private key is shown
473// BIP44 extended public key is shown
474// BIP44 purpose field changes address list
475// BIP44 coin field changes address list
476// BIP44 account field changes address list
477// BIP44 external/internal field changes address list
478
479// BIP32 derivation path can be set
480// BIP32 can use hardened derivation paths
481// BIP32 extended private key is shown
482// BIP32 extended public key is shown
483
484// Derivation path is shown in table
485// Derivation path for address can be hardened
486// Derivation path visibility can be toggled
487// Address is shown
488// Addresses are shown in order of derivation path
489// Address visibility can be toggled
490// Private key is shown
491// Private key visibility can be toggled
492
493// More addresses can be generated
494// A custom number of additional addresses can be generated
495// Additional addresses are shown in order of derivation path
496
497// BIP32 root key can be set by the user
498// Setting BIP32 root key clears the existing phrase, passphrase and seed
499// Clearing of phrase, passphrase and seed can be cancelled by user
500// Custom BIP32 root key is used when changing the derivation path
501
502// Incorrect mnemonic shows error
503// Incorrect word shows suggested replacement
504// Incorrect BIP32 root key shows error
505// Derivation path not starting with m shows error
506// Derivation path containing invalid characters shows useful error
507
508// Github Issue 11: Default word length is 15
509// https://github.com/dcpos/bip39/issues/11
510
511// Github Issue 12: Generate more rows with private keys hidden
512// https://github.com/dcpos/bip39/issues/12
513
514// Github Issue 19: Mnemonic is not sensitive to whitespace
515// https://github.com/dcpos/bip39/issues/19
516
517// Github Issue 23: Use correct derivation path when changing tabs
518// https://github.com/dcpos/bip39/issues/23
519
520];
521
522console.log("Running tests...");
523next();