diff options
Merge pull request #73 from MetaMask/Bip44
Bip44-compliant HD Tree Generation
-rw-r--r-- | .babelrc | 1 | ||||
-rw-r--r-- | app/manifest.json | 2 | ||||
-rw-r--r-- | app/scripts/lib/idStore.js | 126 | ||||
-rw-r--r-- | package.json | 10 | ||||
-rw-r--r-- | test/helper.js | 4 | ||||
-rw-r--r-- | test/index.html | 29 | ||||
-rw-r--r-- | test/spec/test.js | 11 | ||||
-rw-r--r-- | test/unit/idStore-test.js | 81 |
8 files changed, 156 insertions, 108 deletions
diff --git a/.babelrc b/.babelrc new file mode 100644 index 000000000..9d8d51656 --- /dev/null +++ b/.babelrc @@ -0,0 +1 @@ +{ "presets": ["es2015"] } diff --git a/app/manifest.json b/app/manifest.json index 0777c0c8a..1ca0f55e9 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,6 +1,6 @@ { "name": "__MSG_appName__", - "version": "0.15.0", + "version": "1.0.0", "manifest_version": 2, "description": "__MSG_appDescription__", "icons": { diff --git a/app/scripts/lib/idStore.js b/app/scripts/lib/idStore.js index c4547b07f..e3f2a841d 100644 --- a/app/scripts/lib/idStore.js +++ b/app/scripts/lib/idStore.js @@ -14,23 +14,24 @@ module.exports = IdentityStore inherits(IdentityStore, EventEmitter) function IdentityStore(ethStore) { - const self = this - EventEmitter.call(self) + EventEmitter.call(this) // we just use the ethStore to auto-add accounts - self._ethStore = ethStore + this._ethStore = ethStore // lightwallet key store - self._keyStore = null + this._keyStore = null // lightwallet wrapper - self._idmgmt = null + this._idmgmt = null - self._currentState = { + this.hdPathString = "m/44'/60'/0'/0" + + this._currentState = { selectedAddress: null, identities: {}, unconfTxs: {}, } // not part of serilized metamask state - only kept in memory - self._unconfTxCbs = {} + this._unconfTxCbs = {} } // @@ -51,18 +52,17 @@ IdentityStore.prototype.createNewVault = function(password, entropy, cb){ } IdentityStore.prototype.recoverFromSeed = function(password, seed, cb){ - const self = this - self._createIdmgmt(password, seed, null, function(err){ + this._createIdmgmt(password, seed, null, (err) => { if (err) return cb(err) - self._loadIdentities() - self._didUpdate() + + this._loadIdentities() + this._didUpdate() cb() }) } IdentityStore.prototype.setStore = function(store){ - const self = this - self._ethStore = store + this._ethStore = store } IdentityStore.prototype.clearSeedWordCache = function(cb) { @@ -71,46 +71,40 @@ IdentityStore.prototype.clearSeedWordCache = function(cb) { } IdentityStore.prototype.getState = function(){ - const self = this const cachedSeeds = window.localStorage['seedWords'] - return clone(extend(self._currentState, { + return clone(extend(this._currentState, { isInitialized: !!window.localStorage['lightwallet'] && !cachedSeeds, - isUnlocked: self._isUnlocked(), + isUnlocked: this._isUnlocked(), seedWords: cachedSeeds, })) } IdentityStore.prototype.getSelectedAddress = function(){ - const self = this - return self._currentState.selectedAddress + return this._currentState.selectedAddress } IdentityStore.prototype.setSelectedAddress = function(address){ - const self = this - self._currentState.selectedAddress = address - self._didUpdate() + this._currentState.selectedAddress = address + this._didUpdate() } IdentityStore.prototype.setLocked = function(cb){ - const self = this - delete self._keyStore - delete self._idmgmt + delete this._keyStore + delete this._idmgmt cb() } IdentityStore.prototype.submitPassword = function(password, cb){ - const self = this - self._tryPassword(password, function(err){ + this._tryPassword(password, (err) => { if (err) return cb(err) // load identities before returning... - self._loadIdentities() + this._loadIdentities() cb() }) } // comes from dapp via zero-client hooked-wallet provider IdentityStore.prototype.addUnconfirmedTransaction = function(txParams, cb){ - var self = this // create txData obj with parameters and meta data var time = (new Date()).getTime() @@ -121,56 +115,51 @@ IdentityStore.prototype.addUnconfirmedTransaction = function(txParams, cb){ time: time, status: 'unconfirmed', } - self._currentState.unconfTxs[txId] = txData + this._currentState.unconfTxs[txId] = txData console.log('addUnconfirmedTransaction:', txData) // keep the cb around for after approval (requires user interaction) - self._unconfTxCbs[txId] = cb + this._unconfTxCbs[txId] = cb // signal update - self._didUpdate() + this._didUpdate() return txId } // comes from metamask ui IdentityStore.prototype.approveTransaction = function(txId, cb){ - const self = this - - var txData = self._currentState.unconfTxs[txId] + var txData = this._currentState.unconfTxs[txId] var txParams = txData.txParams - var approvalCb = self._unconfTxCbs[txId] || noop + var approvalCb = this._unconfTxCbs[txId] || noop // accept tx cb() approvalCb(null, true) // clean up - delete self._currentState.unconfTxs[txId] - delete self._unconfTxCbs[txId] - self._didUpdate() + delete this._currentState.unconfTxs[txId] + delete this._unconfTxCbs[txId] + this._didUpdate() } // comes from metamask ui IdentityStore.prototype.cancelTransaction = function(txId){ - const self = this - - var txData = self._currentState.unconfTxs[txId] - var approvalCb = self._unconfTxCbs[txId] || noop + var txData = this._currentState.unconfTxs[txId] + var approvalCb = this._unconfTxCbs[txId] || noop // reject tx approvalCb(null, false) // clean up - delete self._currentState.unconfTxs[txId] - delete self._unconfTxCbs[txId] - self._didUpdate() + delete this._currentState.unconfTxs[txId] + delete this._unconfTxCbs[txId] + this._didUpdate() } // performs the actual signing, no autofill of params IdentityStore.prototype.signTransaction = function(txParams, cb){ - const self = this try { console.log('signing tx...', txParams) - var rawTx = self._idmgmt.signTx(txParams) + var rawTx = this._idmgmt.signTx(txParams) cb(null, rawTx) } catch (err) { cb(err) @@ -182,13 +171,11 @@ IdentityStore.prototype.signTransaction = function(txParams, cb){ // IdentityStore.prototype._didUpdate = function(){ - const self = this - self.emit('update', self.getState()) + this.emit('update', this.getState()) } IdentityStore.prototype._isUnlocked = function(){ - const self = this - var result = Boolean(self._keyStore) && Boolean(self._idmgmt) + var result = Boolean(this._keyStore) && Boolean(this._idmgmt) return result } @@ -198,22 +185,21 @@ IdentityStore.prototype._cacheSeedWordsUntilConfirmed = function(seedWords) { // load identities from keyStoreet IdentityStore.prototype._loadIdentities = function(){ - const self = this - if (!self._isUnlocked()) throw new Error('not unlocked') + if (!this._isUnlocked()) throw new Error('not unlocked') // get addresses and normalize address hexString - var addresses = self._keyStore.getAddresses().map(function(address){ return '0x'+address }) - addresses.forEach(function(address){ + var addresses = this._keyStore.getAddresses(this.hdPathString).map((address) => { return '0x'+address }) + addresses.forEach((address) => { // // add to ethStore - self._ethStore.addAccount(address) + this._ethStore.addAccount(address) // add to identities var identity = { name: 'Wally', img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd', address: address, } - self._currentState.identities[address] = identity + this._currentState.identities[address] = identity }) - self._didUpdate() + this._didUpdate() } // @@ -221,8 +207,7 @@ IdentityStore.prototype._loadIdentities = function(){ // IdentityStore.prototype._tryPassword = function(password, cb){ - const self = this - self._createIdmgmt(password, null, null, cb) + this._createIdmgmt(password, null, null, cb) } IdentityStore.prototype._createIdmgmt = function(password, seed, entropy, cb){ @@ -232,7 +217,7 @@ IdentityStore.prototype._createIdmgmt = function(password, seed, entropy, cb){ var serializedKeystore = window.localStorage['lightwallet'] if (seed) { - this._restoreFromSeed(keyStore, seed, derivedKey) + keyStore = this._restoreFromSeed(password, seed, derivedKey) // returning user, recovering from localStorage } else if (serializedKeystore) { @@ -249,17 +234,22 @@ IdentityStore.prototype._createIdmgmt = function(password, seed, entropy, cb){ this._idmgmt = new IdManagement({ keyStore: keyStore, derivedKey: derivedKey, + hdPathSTring: this.hdPathString, }) cb() }) } -IdentityStore.prototype._restoreFromSeed = function(keyStore, seed, derivedKey) { - keyStore = new LightwalletKeyStore(seed, derivedKey) +IdentityStore.prototype._restoreFromSeed = function(password, seed, derivedKey) { + var keyStore = new LightwalletKeyStore(seed, derivedKey, this.hdPathString) + keyStore.addHdDerivationPath(this.hdPathString, derivedKey, {curve: 'secp256k1', purpose: 'sign'}); + keyStore.setDefaultHdDerivationPath(this.hdPathString) + keyStore.generateNewAddress(derivedKey, 3) window.localStorage['lightwallet'] = keyStore.serialize() console.log('restored from seed. saved to keystore localStorage') + return keyStore } IdentityStore.prototype._loadFromLocalStorage = function(serializedKeystore, derivedKey) { @@ -268,19 +258,23 @@ IdentityStore.prototype._loadFromLocalStorage = function(serializedKeystore, der IdentityStore.prototype._createFirstWallet = function(entropy, derivedKey) { var secretSeed = LightwalletKeyStore.generateRandomSeed(entropy) - var keyStore = new LightwalletKeyStore(secretSeed, derivedKey) + var keyStore = new LightwalletKeyStore(secretSeed, derivedKey, this.hdPathString) + keyStore.addHdDerivationPath(this.hdPathString, derivedKey, {curve: 'secp256k1', purpose: 'sign'}); + keyStore.setDefaultHdDerivationPath(this.hdPathString) + keyStore.generateNewAddress(derivedKey, 3) window.localStorage['lightwallet'] = keyStore.serialize() console.log('saved to keystore localStorage') return keyStore } -function IdManagement( opts = { keyStore: null, derivedKey: null } ) { +function IdManagement( opts = { keyStore: null, derivedKey: null, hdPathString: null } ) { this.keyStore = opts.keyStore this.derivedKey = opts.derivedKey + this.hdPathString = opts.hdPathString this.getAddresses = function(){ - return keyStore.getAddresses().map(function(address){ return '0x'+address }) + return keyStore.getAddresses(this.hdPathString).map(function(address){ return '0x'+address }) } this.signTx = function(txParams){ diff --git a/package.json b/package.json index 9f700e924..f482fd4f6 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "public": false, "private": true, "scripts": { - "start": "gulp dev" + "start": "gulp dev", + "test": "mocha --compilers js:babel-register --recursive" }, "dependencies": { "async": "^1.5.2", @@ -28,6 +29,8 @@ "xtend": "^4.0.1" }, "devDependencies": { + "babel-preset-es2015": "^6.6.0", + "babel-register": "^6.7.2", "browserify": "^13.0.0", "del": "^2.2.0", "gulp": "github:gulpjs/gulp#4.0", @@ -35,8 +38,13 @@ "gulp-sourcemaps": "^1.6.0", "gulp-util": "^3.0.7", "gulp-watch": "^4.3.5", + "jsdom": "^8.1.0", "jshint-stylish": "~0.1.5", "lodash.assign": "^4.0.6", + "mocha": "^2.4.5", + "mocha-jsdom": "^1.1.0", + "mocha-sinon": "^1.1.5", + "sinon": "^1.17.3", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^1.1.0", "watchify": "^3.7.0" diff --git a/test/helper.js b/test/helper.js new file mode 100644 index 000000000..7746b90e0 --- /dev/null +++ b/test/helper.js @@ -0,0 +1,4 @@ +require('mocha-sinon')() +var jsdom = require('mocha-jsdom') +jsdom() + diff --git a/test/index.html b/test/index.html deleted file mode 100644 index 6498d5fcc..000000000 --- a/test/index.html +++ /dev/null @@ -1,29 +0,0 @@ -<!doctype html> -<html> -<head> - <meta charset="utf-8"> - <title>Mocha Spec Runner</title> - <link rel="stylesheet" href="../bower_components/mocha/mocha.css"> -</head> -<body> - <div id="mocha"></div> - <script src="../bower_components/mocha/mocha.js"></script> - <script>mocha.setup('bdd');</script> - <script src="../bower_components/chai/chai.js"></script> - <script> - var assert = chai.assert; - var expect = chai.expect; - var should = chai.should(); - </script> - <!-- bower:js --> - <!-- endbower --> - <!-- include source files here... --> - <!-- include spec files here... --> - <script src="spec/test.js"></script> - <script> - if (navigator.userAgent.indexOf('PhantomJS') === -1) { - mocha.run(); - } - </script> -</body> -</html> diff --git a/test/spec/test.js b/test/spec/test.js deleted file mode 100644 index 0fca0fb57..000000000 --- a/test/spec/test.js +++ /dev/null @@ -1,11 +0,0 @@ -(function () { - 'use strict'; - - describe('Give it some context', function () { - describe('maybe a bit more context here', function () { - it('should run here few assertions', function () { - - }); - }); - }); -})(); diff --git a/test/unit/idStore-test.js b/test/unit/idStore-test.js new file mode 100644 index 000000000..d3fabfe9d --- /dev/null +++ b/test/unit/idStore-test.js @@ -0,0 +1,81 @@ +var assert = require('assert') +var IdentityStore = require('../../app/scripts/lib/idStore') + +describe('IdentityStore', function() { + + describe('#createNewVault', function () { + let idStore + let password = 'password123' + let entropy = 'entripppppyy duuude' + let seedWords + let accounts = [] + let originalKeystore + + before(function(done) { + window.localStorage = {} // Hacking localStorage support into JSDom + + idStore = new IdentityStore({ + addAccount(acct) { accounts.push(acct) }, + }) + + idStore.createNewVault(password, entropy, (err, seeds) => { + seedWords = seeds + originalKeystore = idStore._idmgmt.keyStore + done() + }) + }) + + describe('#recoverFromSeed', function() { + let newAccounts = [] + + before(function() { + window.localStorage = {} // Hacking localStorage support into JSDom + + idStore = new IdentityStore({ + addAccount(acct) { newAccounts.push(acct) }, + }) + }) + + it('should return the expected keystore', function (done) { + + idStore.recoverFromSeed(password, seedWords, (err) => { + assert.ifError(err) + + let newKeystore = idStore._idmgmt.keyStore + assert.equal(newAccounts[0], accounts[0]) + done() + }) + }) + }) + }) + + describe('#recoverFromSeed BIP44 compliance', function() { + let seedWords = 'picnic injury awful upper eagle junk alert toss flower renew silly vague' + let firstAccount = '0x5d8de92c205279c10e5669f797b853ccef4f739a' + + let password = 'secret!' + let accounts = [] + let idStore + + before(function() { + window.localStorage = {} // Hacking localStorage support into JSDom + + idStore = new IdentityStore({ + addAccount(acct) { + accounts.push(acct) + }, + }) + }) + + it('should return the expected first account', function (done) { + + idStore.recoverFromSeed(password, seedWords, (err) => { + assert.ifError(err) + + let newKeystore = idStore._idmgmt.keyStore + assert.equal(accounts[0], firstAccount) + done() + }) + }) + }) +}) |