1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Merge branch 'i328-MultiVault' of github.com:MetaMask/metamask-plugin into i328-MultiVault

This commit is contained in:
Kevin Serrano 2016-10-28 16:19:14 -07:00
commit 9b4f3825e7
No known key found for this signature in database
GPG Key ID: 7CC862A58D2889B4
13 changed files with 465 additions and 30 deletions

View File

@ -11,8 +11,10 @@ const createId = require('web3-provider-engine/util/random-id')
// Keyrings:
const SimpleKeyring = require('./keyrings/simple')
const HdKeyring = require('./keyrings/hd')
const keyringTypes = [
SimpleKeyring,
HdKeyring,
]
module.exports = class KeyringController extends EventEmitter {
@ -67,7 +69,12 @@ module.exports = class KeyringController extends EventEmitter {
})
.then((encryptedString) => {
this.configManager.setVault(encryptedString)
cb(null, this.getState())
// TEMPORARY SINGLE-KEYRING CONFIG:
this.addNewKeyring('HD Key Tree', null, cb)
// NORMAL BEHAVIOR:
// cb(null, this.getState())
})
.catch((err) => {
cb(err)
@ -97,25 +104,35 @@ module.exports = class KeyringController extends EventEmitter {
}
addNewKeyring(type, opts, cb) {
const i = this.getAccounts().length
const Keyring = this.getKeyringClassForType(type)
const keyring = new Keyring(opts)
const accounts = keyring.addAccounts(1)
accounts.forEach((account) => {
this.loadBalanceAndNickname(account, i)
})
this.setupAccounts(accounts)
this.keyrings.push(keyring)
this.persistAllKeyrings()
.then(() => {
cb(this.getState())
cb(null, this.getState())
})
.catch((reason) => {
cb(reason)
})
}
addNewAccount(keyRingNum = 0, cb) {
const ring = this.keyrings[keyRingNum]
const accounts = ring.addAccounts(1)
this.setupAccounts(accounts)
cb(null, this.getState())
}
setupAccounts(accounts) {
const i = this.getAccounts().length
accounts.forEach((account) => {
this.loadBalanceAndNickname(account, i)
})
}
// Takes an account address and an iterator representing
// the current number of named accounts.
loadBalanceAndNickname(account, i) {

View File

@ -0,0 +1,86 @@
const EventEmitter = require('events').EventEmitter
const hdkey = require('ethereumjs-wallet/hdkey')
const bip39 = require('bip39')
const ethUtil = require('ethereumjs-util')
const type = 'HD Key Tree'
const sigUtil = require('../lib/sig-util')
const hdPathString = `m/44'/60'/0'/0`
module.exports = class HdKeyring extends EventEmitter {
static type() {
return type
}
constructor(opts) {
super()
this.type = type
this.opts = opts || {}
this.wallets = []
this.mnemonic = null
}
deserialize({ mnemonic, n }) {
this.initFromMnemonic(mnemonic || bip39.generateMnemonic())
this.addAccounts(n)
}
initFromMnemonic(mnemonic) {
const seed = bip39.mnemonicToSeed(mnemonic)
this.mnemonic = mnemonic
this.hdWallet = hdkey.fromMasterSeed(seed)
this.root = this.hdWallet.derivePath(hdPathString)
}
serialize() {
return {
mnemonic: this.mnemonic,
n: this.wallets.length,
}
}
addAccounts(n = 1) {
if (!this.root) {
this.initFromMnemonic(bip39.generateMnemonic())
}
const oldLen = this.wallets.length
const newWallets = []
for (let i = oldLen; i < n + oldLen; i++) {
const child = this.root.deriveChild(i)
const wallet = child.getWallet()
newWallets.push(wallet)
this.wallets.push(wallet)
}
return newWallets.map(w => w.getAddress().toString('hex'))
}
getAccounts() {
return this.wallets.map(w => w.getAddress().toString('hex'))
}
// tx is an instance of the ethereumjs-transaction class.
signTransaction(address, tx) {
const wallet = this.getWalletForAccount(address)
var privKey = wallet.getPrivateKey()
tx.sign(privKey)
return tx
}
// For eth_sign, we need to sign transactions:
signMessage(withAccount, data) {
const wallet = this.getWalletForAccount(withAccount)
const message = ethUtil.removeHexPrefix(data)
var privKey = wallet.getPrivateKey()
var msgSig = ethUtil.ecsign(new Buffer(message, 'hex'), privKey)
var rawMsgSig = ethUtil.bufferToHex(sigUtil.concatSig(msgSig.v, msgSig.r, msgSig.s))
return rawMsgSig
}
getWalletForAccount(account) {
return this.wallets.find(w => w.getAddress().toString('hex') === account)
}
}

View File

@ -61,6 +61,7 @@ module.exports = class MetamaskController {
// forward directly to keyringController
createNewVault: keyringController.createNewVault.bind(keyringController),
addNewKeyring: keyringController.addNewKeyring.bind(keyringController),
addNewAccount: keyringController.addNewAccount.bind(keyringController),
submitPassword: keyringController.submitPassword.bind(keyringController),
setSelectedAddress: keyringController.setSelectedAddress.bind(keyringController),
approveTransaction: keyringController.approveTransaction.bind(keyringController),

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,39 @@
{
"metamask": {
"isInitialized": false,
"isUnlocked": false,
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {},
"unconfTxs": {},
"currentFiat": "USD",
"conversionRate": 11.47635827,
"conversionDate": 1477606503,
"network": null,
"accounts": {},
"transactions": [],
"isConfirmed": true,
"unconfMsgs": {},
"messages": [],
"shapeShiftTxList": [],
"keyringTypes": [
"Simple Key Pair"
],
"provider": {
"type": "testnet"
}
},
"appState": {
"menuOpen": false,
"currentView": {
"name": "accounts",
"detailView": null
},
"accountDetail": {
"subview": "transactions"
},
"transForward": true,
"isLoading": false,
"warning": null
},
"identities": {}
}

View File

@ -34,6 +34,7 @@
},
"dependencies": {
"async": "^1.5.2",
"bip39": "^2.2.0",
"browserify-derequire": "^0.9.4",
"clone": "^1.0.2",
"copy-to-clipboard": "^2.0.0",
@ -55,7 +56,7 @@
"iframe": "^1.0.0",
"iframe-stream": "^1.0.2",
"inject-css": "^0.1.1",
"jazzicon": "^1.1.3",
"jazzicon": "^1.2.0",
"menu-droppo": "^1.1.0",
"metamask-logo": "^2.1.2",
"mississippi": "^1.2.0",

View File

@ -0,0 +1,91 @@
const async = require('async')
const assert = require('assert')
const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN
const configManagerGen = require('../lib/mock-config-manager')
const delegateCallCode = require('../lib/example-code.json').delegateCallCode
// The old way:
const IdentityStore = require('../../app/scripts/lib/idStore')
// The new ways:
var KeyringController = require('../../app/scripts/keyring-controller')
const mockEncryptor = require('../lib/mock-encryptor')
const MockSimpleKeychain = require('../lib/mock-simple-keychain')
const sinon = require('sinon')
const mockVault = {
seed: 'picnic injury awful upper eagle junk alert toss flower renew silly vague',
account: '0x5d8de92c205279c10e5669f797b853ccef4f739a',
}
describe('IdentityStore to KeyringController migration', function() {
// The stars of the show:
let idStore, keyringController, seedWords
let password = 'password123'
let entropy = 'entripppppyy duuude'
let accounts = []
let newAccounts = []
let originalKeystore
// This is a lot of setup, I know!
// We have to create an old style vault, populate it,
// and THEN create a new one, before we can run tests on it.
beforeEach(function(done) {
this.sinon = sinon.sandbox.create()
window.localStorage = {} // Hacking localStorage support into JSDom
var configManager = configManagerGen()
window.localStorage = {} // Hacking localStorage support into JSDom
idStore = new IdentityStore({
configManager: configManagerGen(),
ethStore: {
addAccount(acct) { accounts.push(ethUtil.addHexPrefix(acct)) },
del(acct) { delete accounts[acct] },
},
})
idStore._createVault(password, mockVault.seed, null, function (err, seeds) {
assert.ifError(err, 'createNewVault threw error')
originalKeystore = idStore._idmgmt.keyStore
idStore.setLocked(function(err) {
assert.ifError(err, 'createNewVault threw error')
keyringController = new KeyringController({
configManager,
ethStore: {
addAccount(acct) { newAccounts.push(ethUtil.addHexPrefix(acct)) },
},
})
// Stub out the browser crypto for a mock encryptor.
// Browser crypto is tested in the integration test suite.
keyringController.encryptor = mockEncryptor
done()
})
})
})
describe('creating new vault type', function() {
it('should use the password to migrate the old vault', function(done) {
keyringController.createNewVault(password, null, function (err, state) {
assert.ifError(err, 'createNewVault threw error')
let newAccounts = keyringController.getAccounts()
let newAccount = ethUtil.addHexPrefix(newAccounts[0])
assert.equal(newAccount, accounts[0], 'restored the account')
assert.equal(newAccount, mockVault.account, 'restored the correct account')
const newSeed = keyringController.keyrings[0].mnemonic
assert.equal(newSeed, mockVault.seed, 'seed phrase transferred.')
assert(configManager.getVault(), 'new type of vault is persisted')
done()
})
})
})
})

View File

@ -0,0 +1,97 @@
const assert = require('assert')
const extend = require('xtend')
const HdKeyring = require('../../../app/scripts/keyrings/hd')
// Sample account:
const privKeyHex = 'b8a9c05beeedb25df85f8d641538cbffedf67216048de9c678ee26260eb91952'
const sampleMnemonic = 'finish oppose decorate face calm tragic certain desk hour urge dinosaur mango'
const firstAcct = '1c96099350f13d558464ec79b9be4445aa0ef579'
const secondAcct = '1b00aed43a693f3a957f9feb5cc08afa031e37a0'
describe('simple-keyring', function() {
let keyring
beforeEach(function() {
keyring = new HdKeyring()
})
describe('Keyring.type()', function() {
it('is a class method that returns the type string.', function() {
const type = HdKeyring.type()
assert.equal(typeof type, 'string')
})
})
describe('#type', function() {
it('returns the correct value', function() {
const type = keyring.type
const correct = HdKeyring.type()
assert.equal(type, correct)
})
})
describe('#serialize empty wallets.', function() {
it('serializes a new mnemonic', function() {
const output = keyring.serialize()
assert.equal(output.n, 0)
assert.equal(output.mnemonic, null)
})
})
describe('#deserialize a private key', function() {
it('serializes what it deserializes', function() {
keyring.deserialize({
mnemonic: sampleMnemonic,
n: 1
})
assert.equal(keyring.wallets.length, 1, 'restores two accounts')
keyring.addAccounts(1)
const accounts = keyring.getAccounts()
assert.equal(accounts[0], firstAcct)
assert.equal(accounts[1], secondAcct)
assert.equal(accounts.length, 2)
const serialized = keyring.serialize()
assert.equal(serialized.mnemonic, sampleMnemonic)
})
})
describe('#addAccounts', function() {
describe('with no arguments', function() {
it('creates a single wallet', function() {
keyring.addAccounts()
assert.equal(keyring.wallets.length, 1)
})
})
describe('with a numeric argument', function() {
it('creates that number of wallets', function() {
keyring.addAccounts(3)
assert.equal(keyring.wallets.length, 3)
})
})
})
describe('#getAccounts', function() {
it('calls getAddress on each wallet', function() {
// Push a mock wallet
const desiredOutput = 'foo'
keyring.wallets.push({
getAddress() {
return {
toString() {
return desiredOutput
}
}
}
})
const output = keyring.getAccounts()
assert.equal(output[0], desiredOutput)
assert.equal(output.length, 1)
})
})
})

View File

@ -87,7 +87,7 @@ AccountsScreen.prototype.render = function () {
h('div.footer.hover-white.pointer', {
key: 'reveal-account-bar',
onClick: () => {
this.addNewKeyring()
this.addNewAccount()
},
style: {
display: 'flex',
@ -146,8 +146,8 @@ AccountsScreen.prototype.onShowDetail = function (address, event) {
this.props.dispatch(actions.showAccountDetail(address))
}
AccountsScreen.prototype.addNewKeyring = function () {
this.props.dispatch(actions.addNewKeyring('Simple Key Pair'))
AccountsScreen.prototype.addNewAccount = function () {
this.props.dispatch(actions.addNewAccount(0))
}
AccountsScreen.prototype.goHome = function () {

View File

@ -25,7 +25,8 @@ var actions = {
showInitializeMenu: showInitializeMenu,
createNewVault: createNewVault,
createNewVaultInProgress: createNewVaultInProgress,
addNewKeyring: addNewKeyring,
addNewKeyring,
addNewAccount,
showNewVaultSeed: showNewVaultSeed,
showInfoPage: showInfoPage,
// unlock screen
@ -178,6 +179,7 @@ function createNewVault (password, entropy) {
if (err) {
return dispatch(actions.showWarning(err.message))
}
dispatch(this.updateMetamaskState(newState))
dispatch(this.showAccountsPage())
dispatch(this.hideLoadingIndication())
@ -199,6 +201,19 @@ function addNewKeyring (type, opts) {
}
}
function addNewAccount (ringNumber = 0) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
background.addNewAccount(ringNumber, (err, newState) => {
dispatch(this.hideLoadingIndication())
if (err) {
return dispatch(actions.showWarning(err))
}
dispatch(this.updateMetamaskState(newState))
})
}
}
function showInfoPage () {
return {
type: actions.SHOW_INFO_PAGE,

View File

@ -16,8 +16,8 @@ function IdenticonComponent () {
}
IdenticonComponent.prototype.render = function () {
var state = this.props
var diameter = state.diameter || this.defaultDiameter
var props = this.props
var diameter = props.diameter || this.defaultDiameter
return (
h('div', {
key: 'identicon-' + this.props.address,
@ -33,15 +33,14 @@ IdenticonComponent.prototype.render = function () {
}
IdenticonComponent.prototype.componentDidMount = function () {
var state = this.props
var address = state.address
var props = this.props
var address = props.address
if (!address) return
var container = findDOMNode(this)
var diameter = state.diameter || this.defaultDiameter
var imageify = state.imageify === undefined ? true : state.imageify
var img = iconFactory.iconForAddress(address, diameter, imageify)
var diameter = props.diameter || this.defaultDiameter
var img = iconFactory.iconForAddress(address, diameter, false)
container.appendChild(img)
}

View File

@ -5,6 +5,8 @@ const connect = require('react-redux').connect
const h = require('react-hyperscript')
const Mascot = require('../components/mascot')
const actions = require('../actions')
const Tooltip = require('../components/tooltip')
const getCaretCoordinates = require('textarea-caret')
module.exports = connect(mapStateToProps)(InitializeMenuScreen)
@ -54,18 +56,73 @@ InitializeMenuScreen.prototype.renderMenu = function () {
},
}, 'MetaMask'),
h('div', [
h('h3', {
style: {
fontSize: '0.8em',
color: '#7F8082',
display: 'inline',
},
}, 'Encrypt your new DEN'),
h(Tooltip, {
title: 'Your DEN is your password-encrypted storage within MetaMask.',
}, [
h('i.fa.fa-question-circle.pointer', {
style: {
fontSize: '18px',
position: 'relative',
color: 'rgb(247, 134, 28)',
top: '2px',
marginLeft: '4px',
},
}),
]),
]),
// password
h('input.large-input.letter-spacey', {
type: 'password',
id: 'password-box',
placeholder: 'New Password (min 8 chars)',
onInput: this.inputChanged.bind(this),
style: {
width: 260,
marginTop: 12,
},
}),
// confirm password
h('input.large-input.letter-spacey', {
type: 'password',
id: 'password-box-confirm',
placeholder: 'Confirm Password',
onKeyPress: this.createVaultOnEnter.bind(this),
onInput: this.inputChanged.bind(this),
style: {
width: 260,
marginTop: 16,
},
}),
h('button.primary', {
onClick: this.showCreateVault.bind(this),
onClick: this.createNewVault.bind(this),
style: {
margin: 12,
},
}, 'Create New Vault'),
}, 'Create'),
/*
h('.flex-row.flex-center.flex-grow', [
h('hr'),
h('div', 'Advanced (Eventually?)'),
h('hr'),
h('p.pointer', {
style: {
fontSize: '0.8em',
color: 'rgb(247, 134, 28)',
textDecoration: 'underline',
},
}, 'I already have a DEN that I would like to import'),
]),
*/
@ -73,7 +130,42 @@ InitializeMenuScreen.prototype.renderMenu = function () {
)
}
InitializeMenuScreen.prototype.showCreateVault = function () {
this.props.dispatch(actions.showCreateVault())
InitializeMenuScreen.prototype.createVaultOnEnter = function (event) {
if (event.key === 'Enter') {
event.preventDefault()
this.createNewVault()
}
}
InitializeMenuScreen.prototype.createNewVault = function () {
var passwordBox = document.getElementById('password-box')
var password = passwordBox.value
var passwordConfirmBox = document.getElementById('password-box-confirm')
var passwordConfirm = passwordConfirmBox.value
// var entropy = document.getElementById('entropy-text-entry').value
if (password.length < 8) {
this.warning = 'password not long enough'
this.props.dispatch(actions.displayWarning(this.warning))
return
}
if (password !== passwordConfirm) {
this.warning = 'passwords don\'t match'
this.props.dispatch(actions.displayWarning(this.warning))
return
}
this.props.dispatch(actions.createNewVault(password, ''/* entropy*/))
}
InitializeMenuScreen.prototype.inputChanged = function (event) {
// tell mascot to look at page action
var element = event.target
var boundingRect = element.getBoundingClientRect()
var coordinates = getCaretCoordinates(element, element.selectionEnd)
this.animationEventEmitter.emit('point', {
x: boundingRect.left + coordinates.left - element.scrollLeft,
y: boundingRect.top + coordinates.top - element.scrollTop,
})
}

View File

@ -104,6 +104,3 @@ UnlockScreen.prototype.inputChanged = function (event) {
})
}
UnlockScreen.prototype.emitAnim = function (name, a, b, c) {
this.animationEventEmitter.emit(name, a, b, c)
}