Merge pull request #73 from MetaMask/Bip44

Bip44-compliant HD Tree Generation
This commit is contained in:
Dan Finlay 2016-03-25 18:57:40 -07:00
commit 9fbf40e702
8 changed files with 156 additions and 108 deletions

1
.babelrc Normal file
View File

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

View File

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

View File

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

View File

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

4
test/helper.js Normal file
View File

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

View File

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

View File

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

81
test/unit/idStore-test.js Normal file
View File

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