Merge pull request #73 from MetaMask/Bip44

Bip44-compliant HD Tree Generation
feature/default_network_editable
Dan Finlay 9 years ago
commit 9fbf40e702
  1. 1
      .babelrc
  2. 2
      app/manifest.json
  3. 126
      app/scripts/lib/idStore.js
  4. 10
      package.json
  5. 4
      test/helper.js
  6. 29
      test/index.html
  7. 11
      test/spec/test.js
  8. 81
      test/unit/idStore-test.js

@ -0,0 +1 @@
{ "presets": ["es2015"] }

@ -1,6 +1,6 @@
{ {
"name": "__MSG_appName__", "name": "__MSG_appName__",
"version": "0.15.0", "version": "1.0.0",
"manifest_version": 2, "manifest_version": 2,
"description": "__MSG_appDescription__", "description": "__MSG_appDescription__",
"icons": { "icons": {

@ -14,23 +14,24 @@ module.exports = IdentityStore
inherits(IdentityStore, EventEmitter) inherits(IdentityStore, EventEmitter)
function IdentityStore(ethStore) { function IdentityStore(ethStore) {
const self = this EventEmitter.call(this)
EventEmitter.call(self)
// we just use the ethStore to auto-add accounts // we just use the ethStore to auto-add accounts
self._ethStore = ethStore this._ethStore = ethStore
// lightwallet key store // lightwallet key store
self._keyStore = null this._keyStore = null
// lightwallet wrapper // lightwallet wrapper
self._idmgmt = null this._idmgmt = null
self._currentState = { this.hdPathString = "m/44'/60'/0'/0"
this._currentState = {
selectedAddress: null, selectedAddress: null,
identities: {}, identities: {},
unconfTxs: {}, unconfTxs: {},
} }
// not part of serilized metamask state - only kept in memory // 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){ IdentityStore.prototype.recoverFromSeed = function(password, seed, cb){
const self = this this._createIdmgmt(password, seed, null, (err) => {
self._createIdmgmt(password, seed, null, function(err){
if (err) return cb(err) if (err) return cb(err)
self._loadIdentities()
self._didUpdate() this._loadIdentities()
this._didUpdate()
cb() cb()
}) })
} }
IdentityStore.prototype.setStore = function(store){ IdentityStore.prototype.setStore = function(store){
const self = this this._ethStore = store
self._ethStore = store
} }
IdentityStore.prototype.clearSeedWordCache = function(cb) { IdentityStore.prototype.clearSeedWordCache = function(cb) {
@ -71,46 +71,40 @@ IdentityStore.prototype.clearSeedWordCache = function(cb) {
} }
IdentityStore.prototype.getState = function(){ IdentityStore.prototype.getState = function(){
const self = this
const cachedSeeds = window.localStorage['seedWords'] const cachedSeeds = window.localStorage['seedWords']
return clone(extend(self._currentState, { return clone(extend(this._currentState, {
isInitialized: !!window.localStorage['lightwallet'] && !cachedSeeds, isInitialized: !!window.localStorage['lightwallet'] && !cachedSeeds,
isUnlocked: self._isUnlocked(), isUnlocked: this._isUnlocked(),
seedWords: cachedSeeds, seedWords: cachedSeeds,
})) }))
} }
IdentityStore.prototype.getSelectedAddress = function(){ IdentityStore.prototype.getSelectedAddress = function(){
const self = this return this._currentState.selectedAddress
return self._currentState.selectedAddress
} }
IdentityStore.prototype.setSelectedAddress = function(address){ IdentityStore.prototype.setSelectedAddress = function(address){
const self = this this._currentState.selectedAddress = address
self._currentState.selectedAddress = address this._didUpdate()
self._didUpdate()
} }
IdentityStore.prototype.setLocked = function(cb){ IdentityStore.prototype.setLocked = function(cb){
const self = this delete this._keyStore
delete self._keyStore delete this._idmgmt
delete self._idmgmt
cb() cb()
} }
IdentityStore.prototype.submitPassword = function(password, cb){ IdentityStore.prototype.submitPassword = function(password, cb){
const self = this this._tryPassword(password, (err) => {
self._tryPassword(password, function(err){
if (err) return cb(err) if (err) return cb(err)
// load identities before returning... // load identities before returning...
self._loadIdentities() this._loadIdentities()
cb() cb()
}) })
} }
// comes from dapp via zero-client hooked-wallet provider // comes from dapp via zero-client hooked-wallet provider
IdentityStore.prototype.addUnconfirmedTransaction = function(txParams, cb){ IdentityStore.prototype.addUnconfirmedTransaction = function(txParams, cb){
var self = this
// create txData obj with parameters and meta data // create txData obj with parameters and meta data
var time = (new Date()).getTime() var time = (new Date()).getTime()
@ -121,56 +115,51 @@ IdentityStore.prototype.addUnconfirmedTransaction = function(txParams, cb){
time: time, time: time,
status: 'unconfirmed', status: 'unconfirmed',
} }
self._currentState.unconfTxs[txId] = txData this._currentState.unconfTxs[txId] = txData
console.log('addUnconfirmedTransaction:', txData) console.log('addUnconfirmedTransaction:', txData)
// keep the cb around for after approval (requires user interaction) // keep the cb around for after approval (requires user interaction)
self._unconfTxCbs[txId] = cb this._unconfTxCbs[txId] = cb
// signal update // signal update
self._didUpdate() this._didUpdate()
return txId return txId
} }
// comes from metamask ui // comes from metamask ui
IdentityStore.prototype.approveTransaction = function(txId, cb){ IdentityStore.prototype.approveTransaction = function(txId, cb){
const self = this var txData = this._currentState.unconfTxs[txId]
var txData = self._currentState.unconfTxs[txId]
var txParams = txData.txParams var txParams = txData.txParams
var approvalCb = self._unconfTxCbs[txId] || noop var approvalCb = this._unconfTxCbs[txId] || noop
// accept tx // accept tx
cb() cb()
approvalCb(null, true) approvalCb(null, true)
// clean up // clean up
delete self._currentState.unconfTxs[txId] delete this._currentState.unconfTxs[txId]
delete self._unconfTxCbs[txId] delete this._unconfTxCbs[txId]
self._didUpdate() this._didUpdate()
} }
// comes from metamask ui // comes from metamask ui
IdentityStore.prototype.cancelTransaction = function(txId){ IdentityStore.prototype.cancelTransaction = function(txId){
const self = this var txData = this._currentState.unconfTxs[txId]
var approvalCb = this._unconfTxCbs[txId] || noop
var txData = self._currentState.unconfTxs[txId]
var approvalCb = self._unconfTxCbs[txId] || noop
// reject tx // reject tx
approvalCb(null, false) approvalCb(null, false)
// clean up // clean up
delete self._currentState.unconfTxs[txId] delete this._currentState.unconfTxs[txId]
delete self._unconfTxCbs[txId] delete this._unconfTxCbs[txId]
self._didUpdate() this._didUpdate()
} }
// performs the actual signing, no autofill of params // performs the actual signing, no autofill of params
IdentityStore.prototype.signTransaction = function(txParams, cb){ IdentityStore.prototype.signTransaction = function(txParams, cb){
const self = this
try { try {
console.log('signing tx...', txParams) console.log('signing tx...', txParams)
var rawTx = self._idmgmt.signTx(txParams) var rawTx = this._idmgmt.signTx(txParams)
cb(null, rawTx) cb(null, rawTx)
} catch (err) { } catch (err) {
cb(err) cb(err)
@ -182,13 +171,11 @@ IdentityStore.prototype.signTransaction = function(txParams, cb){
// //
IdentityStore.prototype._didUpdate = function(){ IdentityStore.prototype._didUpdate = function(){
const self = this this.emit('update', this.getState())
self.emit('update', self.getState())
} }
IdentityStore.prototype._isUnlocked = function(){ IdentityStore.prototype._isUnlocked = function(){
const self = this var result = Boolean(this._keyStore) && Boolean(this._idmgmt)
var result = Boolean(self._keyStore) && Boolean(self._idmgmt)
return result return result
} }
@ -198,22 +185,21 @@ IdentityStore.prototype._cacheSeedWordsUntilConfirmed = function(seedWords) {
// load identities from keyStoreet // load identities from keyStoreet
IdentityStore.prototype._loadIdentities = function(){ IdentityStore.prototype._loadIdentities = function(){
const self = this if (!this._isUnlocked()) throw new Error('not unlocked')
if (!self._isUnlocked()) throw new Error('not unlocked')
// get addresses and normalize address hexString // get addresses and normalize address hexString
var addresses = self._keyStore.getAddresses().map(function(address){ return '0x'+address }) var addresses = this._keyStore.getAddresses(this.hdPathString).map((address) => { return '0x'+address })
addresses.forEach(function(address){ addresses.forEach((address) => {
// // add to ethStore // // add to ethStore
self._ethStore.addAccount(address) this._ethStore.addAccount(address)
// add to identities // add to identities
var identity = { var identity = {
name: 'Wally', name: 'Wally',
img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd', img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd',
address: address, 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){ IdentityStore.prototype._tryPassword = function(password, cb){
const self = this this._createIdmgmt(password, null, null, cb)
self._createIdmgmt(password, null, null, cb)
} }
IdentityStore.prototype._createIdmgmt = function(password, seed, entropy, 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'] var serializedKeystore = window.localStorage['lightwallet']
if (seed) { if (seed) {
this._restoreFromSeed(keyStore, seed, derivedKey) keyStore = this._restoreFromSeed(password, seed, derivedKey)
// returning user, recovering from localStorage // returning user, recovering from localStorage
} else if (serializedKeystore) { } else if (serializedKeystore) {
@ -249,17 +234,22 @@ IdentityStore.prototype._createIdmgmt = function(password, seed, entropy, cb){
this._idmgmt = new IdManagement({ this._idmgmt = new IdManagement({
keyStore: keyStore, keyStore: keyStore,
derivedKey: derivedKey, derivedKey: derivedKey,
hdPathSTring: this.hdPathString,
}) })
cb() cb()
}) })
} }
IdentityStore.prototype._restoreFromSeed = function(keyStore, seed, derivedKey) { IdentityStore.prototype._restoreFromSeed = function(password, seed, derivedKey) {
keyStore = new LightwalletKeyStore(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) keyStore.generateNewAddress(derivedKey, 3)
window.localStorage['lightwallet'] = keyStore.serialize() window.localStorage['lightwallet'] = keyStore.serialize()
console.log('restored from seed. saved to keystore localStorage') console.log('restored from seed. saved to keystore localStorage')
return keyStore
} }
IdentityStore.prototype._loadFromLocalStorage = function(serializedKeystore, derivedKey) { IdentityStore.prototype._loadFromLocalStorage = function(serializedKeystore, derivedKey) {
@ -268,19 +258,23 @@ IdentityStore.prototype._loadFromLocalStorage = function(serializedKeystore, der
IdentityStore.prototype._createFirstWallet = function(entropy, derivedKey) { IdentityStore.prototype._createFirstWallet = function(entropy, derivedKey) {
var secretSeed = LightwalletKeyStore.generateRandomSeed(entropy) 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) keyStore.generateNewAddress(derivedKey, 3)
window.localStorage['lightwallet'] = keyStore.serialize() window.localStorage['lightwallet'] = keyStore.serialize()
console.log('saved to keystore localStorage') console.log('saved to keystore localStorage')
return keyStore return keyStore
} }
function IdManagement( opts = { keyStore: null, derivedKey: null } ) { function IdManagement( opts = { keyStore: null, derivedKey: null, hdPathString: null } ) {
this.keyStore = opts.keyStore this.keyStore = opts.keyStore
this.derivedKey = opts.derivedKey this.derivedKey = opts.derivedKey
this.hdPathString = opts.hdPathString
this.getAddresses = function(){ 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){ this.signTx = function(txParams){

@ -4,7 +4,8 @@
"public": false, "public": false,
"private": true, "private": true,
"scripts": { "scripts": {
"start": "gulp dev" "start": "gulp dev",
"test": "mocha --compilers js:babel-register --recursive"
}, },
"dependencies": { "dependencies": {
"async": "^1.5.2", "async": "^1.5.2",
@ -28,6 +29,8 @@
"xtend": "^4.0.1" "xtend": "^4.0.1"
}, },
"devDependencies": { "devDependencies": {
"babel-preset-es2015": "^6.6.0",
"babel-register": "^6.7.2",
"browserify": "^13.0.0", "browserify": "^13.0.0",
"del": "^2.2.0", "del": "^2.2.0",
"gulp": "github:gulpjs/gulp#4.0", "gulp": "github:gulpjs/gulp#4.0",
@ -35,8 +38,13 @@
"gulp-sourcemaps": "^1.6.0", "gulp-sourcemaps": "^1.6.0",
"gulp-util": "^3.0.7", "gulp-util": "^3.0.7",
"gulp-watch": "^4.3.5", "gulp-watch": "^4.3.5",
"jsdom": "^8.1.0",
"jshint-stylish": "~0.1.5", "jshint-stylish": "~0.1.5",
"lodash.assign": "^4.0.6", "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-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0", "vinyl-source-stream": "^1.1.0",
"watchify": "^3.7.0" "watchify": "^3.7.0"

@ -0,0 +1,4 @@
require('mocha-sinon')()
var jsdom = require('mocha-jsdom')
jsdom()

@ -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>

@ -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 () {
});
});
});
})();

@ -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()
})
})
})
})
Loading…
Cancel
Save