Merge branch 'dev' into TxManager

feature/default_network_editable
Frankie 8 years ago
commit fa3e708f34
  1. 49
      app/scripts/keyring-controller.js
  2. 2
      app/scripts/lib/config-manager.js
  3. 7
      app/scripts/lib/idStore-migrator.js
  4. 3
      app/scripts/lib/idStore.js
  5. 4
      app/scripts/lib/inpage-provider.js
  6. 80
      app/scripts/metamask-controller.js
  7. 91
      development/states/lost-accounts.json
  8. 2
      test/integration/lib/first-time.js
  9. 74
      test/integration/lib/idStore-migrator-test.js
  10. 126
      test/integration/lib/keyring-controller-test.js
  11. 2
      test/unit/idStore-migration-test.js
  12. 12
      ui/app/actions.js
  13. 18
      ui/app/app.js
  14. 20
      ui/app/components/notice.js
  15. 23
      ui/lib/lost-accounts-notice.js

@ -6,7 +6,6 @@ const encryptor = require('browser-passworder')
const normalize = require('./lib/sig-util').normalize
const messageManager = require('./lib/message-manager')
const IdStoreMigrator = require('./lib/idStore-migrator')
const BN = ethUtil.BN
// Keyrings:
@ -40,11 +39,6 @@ module.exports = class KeyringController extends EventEmitter {
this._unconfMsgCbs = {}
this.getNetwork = opts.getNetwork
// TEMPORARY UNTIL FULL DEPRECATION:
this.idStoreMigrator = new IdStoreMigrator({
configManager: this.configManager,
})
}
// Set Store
@ -107,7 +101,6 @@ module.exports = class KeyringController extends EventEmitter {
conversionDate: this.configManager.getConversionDate(),
keyringTypes: this.keyringTypes.map(krt => krt.type),
identities: this.identities,
lostAccounts: this.configManager.getLostAccounts(),
}
}
@ -215,10 +208,7 @@ module.exports = class KeyringController extends EventEmitter {
// Temporarily also migrates any old-style vaults first, as well.
// (Pre MetaMask 3.0.0)
submitPassword (password) {
return this.migrateOldVaultIfAny(password)
.then(() => {
return this.unlockKeyrings(password)
})
.then((keyrings) => {
this.keyrings = keyrings
return this.fullUpdate()
@ -420,41 +410,6 @@ module.exports = class KeyringController extends EventEmitter {
// THESE METHODS ARE ONLY USED INTERNALLY TO THE KEYRING-CONTROLLER
// AND SO MAY BE CHANGED MORE LIBERALLY THAN THE ABOVE METHODS.
// Migrate Old Vault If Any
// @string password
//
// returns Promise()
//
// Temporary step used when logging in.
// Checks if old style (pre-3.0.0) Metamask Vault exists.
// If so, persists that vault in the new vault format
// with the provided password, so the other unlock steps
// may be completed without interruption.
migrateOldVaultIfAny (password) {
const shouldMigrate = !!this.configManager.getWallet() && !this.configManager.getVault()
if (!shouldMigrate) {
return Promise.resolve()
}
return this.idStoreMigrator.migratedVaultForPassword(password)
.then((result) => {
this.password = password
if (result && shouldMigrate) {
const { serialized, lostAccounts } = result
this.configManager.setLostAccounts(lostAccounts)
return this.restoreKeyring(serialized)
.then(keyring => keyring.getAccounts())
.then((accounts) => {
this.configManager.setSelectedAccount(accounts[0])
return this.persistAllKeyrings()
})
} else {
return Promise.resolve()
}
})
}
// Create First Key Tree
// returns @Promise
//
@ -575,6 +530,10 @@ module.exports = class KeyringController extends EventEmitter {
// initializing the persisted keyrings to RAM.
unlockKeyrings (password) {
const encryptedVault = this.configManager.getVault()
if (!encryptedVault) {
throw new Error('Cannot unlock without a previous vault.')
}
return this.encryptor.decrypt(password, encryptedVault)
.then((vault) => {
this.password = password

@ -118,7 +118,7 @@ ConfigManager.prototype.setVault = function (encryptedString) {
ConfigManager.prototype.getVault = function () {
var data = this.getData()
return ('vault' in data) && data.vault
return data.vault
}
ConfigManager.prototype.getKeychains = function () {

@ -63,7 +63,12 @@ module.exports = class IdentityStoreMigrator {
return {
serialized,
lostAccounts,
lostAccounts: lostAccounts.map((address) => {
return {
address,
privateKey: this.idStore.exportAccount(address),
}
}),
}
})
}

@ -190,7 +190,8 @@ IdentityStore.prototype.submitPassword = function (password, cb) {
IdentityStore.prototype.exportAccount = function (address, cb) {
var privateKey = this._idmgmt.exportPrivateKey(address)
cb(null, privateKey)
if (cb) cb(null, privateKey)
return privateKey
}
// private

@ -72,13 +72,13 @@ MetamaskInpageProvider.prototype.send = function (payload) {
case 'eth_accounts':
// read from localStorage
selectedAccount = self.publicConfigStore.get('selectedAddress')
selectedAccount = self.publicConfigStore.get('selectedAccount')
result = selectedAccount ? [selectedAccount] : []
break
case 'eth_coinbase':
// read from localStorage
selectedAccount = self.publicConfigStore.get('selectedAddress')
selectedAccount = self.publicConfigStore.get('selectedAccount')
result = selectedAccount || '0x0000000000000000000000000000000000000000'
break

@ -11,6 +11,7 @@ const ConfigManager = require('./lib/config-manager')
const extension = require('./lib/extension')
const autoFaucet = require('./lib/auto-faucet')
const nodeify = require('./lib/nodeify')
const IdStoreMigrator = require('./lib/idStore-migrator')
module.exports = class MetamaskController {
@ -54,6 +55,11 @@ module.exports = class MetamaskController {
this.checkTOSChange()
this.scheduleConversionInterval()
// TEMPORARY UNTIL FULL DEPRECATION:
this.idStoreMigrator = new IdStoreMigrator({
configManager: this.configManager,
})
}
getState () {
@ -63,7 +69,9 @@ module.exports = class MetamaskController {
this.configManager.getConfig(),
this.keyringController.getState(),
this.txManager.getState(),
this.noticeController.getState()
this.noticeController.getState(), {
lostAccounts: this.configManager.getLostAccounts(),
}
)
}
@ -83,6 +91,7 @@ module.exports = class MetamaskController {
setTOSHash: this.setTOSHash.bind(this),
checkTOSChange: this.checkTOSChange.bind(this),
setGasMultiplier: this.setGasMultiplier.bind(this),
markAccountsFound: this.markAccountsFound.bind(this),
// forward directly to keyringController
createNewVaultAndKeychain: nodeify(keyringController.createNewVaultAndKeychain).bind(keyringController),
@ -90,7 +99,12 @@ module.exports = class MetamaskController {
placeSeedWords: nodeify(keyringController.placeSeedWords).bind(keyringController),
clearSeedWordCache: nodeify(keyringController.clearSeedWordCache).bind(keyringController),
setLocked: nodeify(keyringController.setLocked).bind(keyringController),
submitPassword: nodeify(keyringController.submitPassword).bind(keyringController),
submitPassword: (password, cb) => {
this.migrateOldVaultIfAny(password)
.then(keyringController.submitPassword.bind(keyringController))
.then((newState) => { cb(null, newState) })
.catch((reason) => { cb(reason) })
},
addNewKeyring: nodeify(keyringController.addNewKeyring).bind(keyringController),
addNewAccount: nodeify(keyringController.addNewAccount).bind(keyringController),
setSelectedAccount: nodeify(keyringController.setSelectedAccount).bind(keyringController),
@ -429,4 +443,66 @@ module.exports = class MetamaskController {
getStateNetwork () {
return this.state.network
}
markAccountsFound(cb) {
this.configManager.setLostAccounts([])
this.sendUpdate()
cb(null, this.getState())
}
// Migrate Old Vault If Any
// @string password
//
// returns Promise()
//
// Temporary step used when logging in.
// Checks if old style (pre-3.0.0) Metamask Vault exists.
// If so, persists that vault in the new vault format
// with the provided password, so the other unlock steps
// may be completed without interruption.
migrateOldVaultIfAny (password) {
if (!this.checkIfShouldMigrate()) {
return Promise.resolve(password)
}
const keyringController = this.keyringController
return this.idStoreMigrator.migratedVaultForPassword(password)
.then(this.restoreOldVaultAccounts.bind(this))
.then(this.restoreOldLostAccounts.bind(this))
.then(keyringController.persistAllKeyrings.bind(keyringController))
.then(() => password)
}
checkIfShouldMigrate() {
return !!this.configManager.getWallet() && !this.configManager.getVault()
}
restoreOldVaultAccounts(migratorOutput) {
const { serialized } = migratorOutput
return this.keyringController.restoreKeyring(serialized)
.then(() => migratorOutput)
}
restoreOldLostAccounts(migratorOutput) {
const { lostAccounts } = migratorOutput
if (lostAccounts) {
this.configManager.setLostAccounts(lostAccounts.map(acct => acct.address))
return this.importLostAccounts(migratorOutput)
}
return Promise.resolve(migratorOutput)
}
// IMPORT LOST ACCOUNTS
// @Object with key lostAccounts: @Array accounts <{ address, privateKey }>
// Uses the array's private keys to create a new Simple Key Pair keychain
// and add it to the keyring controller.
importLostAccounts ({ lostAccounts }) {
const privKeys = lostAccounts.map(acct => acct.privateKey)
return this.keyringController.restoreKeyring({
type: 'Simple Key Pair',
data: privKeys,
})
}
}

@ -0,0 +1,91 @@
{
"metamask": {
"currentFiat": "USD",
"lostAccounts": [
"0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b"
],
"conversionRate": 11.06608791,
"conversionDate": 1470421024,
"isInitialized": true,
"isUnlocked": true,
"currentDomain": "example.com",
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {
"0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc": {
"name": "Wallet 1",
"address": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"mayBeFauceting": false
},
"0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b": {
"name": "Wallet 2",
"address": "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b",
"mayBeFauceting": false
},
"0xeb9e64b93097bc15f01f13eae97015c57ab64823": {
"name": "Wallet 3",
"address": "0xeb9e64b93097bc15f01f13eae97015c57ab64823",
"mayBeFauceting": false
},
"0x704107d04affddd9b66ab9de3dd7b095852e9b69": {
"name": "Wallet 4",
"address": "0x704107d04affddd9b66ab9de3dd7b095852e9b69",
"mayBeFauceting": false
}
},
"unconfTxs": {},
"accounts": {
"0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc": {
"code": "0x",
"balance": "0x100000000000",
"nonce": "0x0",
"address": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc"
},
"0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b": {
"code": "0x",
"nonce": "0x0",
"balance": "0x100000000000",
"address": "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b"
},
"0xeb9e64b93097bc15f01f13eae97015c57ab64823": {
"code": "0x",
"nonce": "0x0",
"balance": "0x100000000000",
"address": "0xeb9e64b93097bc15f01f13eae97015c57ab64823"
},
"0x704107d04affddd9b66ab9de3dd7b095852e9b69": {
"code": "0x",
"balance": "0x0",
"nonce": "0x0",
"address": "0x704107d04affddd9b66ab9de3dd7b095852e9b69"
}
},
"transactions": [],
"selectedAccount": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"network": "2",
"seedWords": null,
"isDisclaimerConfirmed": true,
"unconfMsgs": {},
"messages": [],
"provider": {
"type": "testnet"
},
"selectedAccount": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc"
},
"appState": {
"menuOpen": false,
"currentView": {
"name": "accountDetail",
"detailView": null,
"context": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc"
},
"accountDetail": {
"subview": "transactions"
},
"currentDomain": "127.0.0.1:9966",
"transForward": true,
"isLoading": false,
"warning": null
},
"identities": {}
}

@ -79,7 +79,7 @@ QUnit.test('agree to terms', function (assert) {
var createButton = app.find('button.primary')[0]
createButton.click()
return wait(1500)
return wait(1000)
}).then(function() {
var detail = app.find('.account-detail-section')[0]

@ -0,0 +1,74 @@
var KeyringController = require('../../../app/scripts/keyring-controller')
var ConfigManager = require('../../../app/scripts/lib/config-manager')
var IdStoreMigrator = require('../../../app/scripts/lib/idStore-migrator')
var oldStyleVault = require('../mocks/oldVault.json')
var badStyleVault = require('../mocks/badVault.json')
var STORAGE_KEY = 'metamask-config'
var PASSWORD = '12345678'
var FIRST_ADDRESS = '0x4dd5d356c5A016A220bCD69e82e5AF680a430d00'.toLowerCase()
var SEED = 'fringe damage bounce extend tunnel afraid alert sound all soldier all dinner'
var BAD_STYLE_FIRST_ADDRESS = '0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9'
QUnit.module('Old Style Vaults', {
beforeEach: function () {
window.localStorage[STORAGE_KEY] = JSON.stringify(oldStyleVault)
this.configManager = new ConfigManager({
loadData: () => { return JSON.parse(window.localStorage[STORAGE_KEY]) },
setData: (data) => { window.localStorage[STORAGE_KEY] = JSON.stringify(data) },
})
this.migrator = new IdStoreMigrator({
configManager: this.configManager,
})
}
})
QUnit.test('migrator:isInitialized', function (assert) {
assert.ok(this.migrator)
})
QUnit.test('migrator:migratedVaultForPassword', function (assert) {
var done = assert.async()
this.migrator.migratedVaultForPassword(PASSWORD)
.then((result) => {
const { serialized, lostAccounts } = result
assert.equal(serialized.data.mnemonic, SEED, 'seed phrase recovered')
assert.equal(lostAccounts.length, 0, 'no lost accounts')
done()
})
})
QUnit.module('Old Style Vaults with bad HD seed', {
beforeEach: function () {
window.localStorage[STORAGE_KEY] = JSON.stringify(badStyleVault)
this.configManager = new ConfigManager({
loadData: () => { return JSON.parse(window.localStorage[STORAGE_KEY]) },
setData: (data) => { window.localStorage[STORAGE_KEY] = JSON.stringify(data) },
})
this.migrator = new IdStoreMigrator({
configManager: this.configManager,
})
}
})
QUnit.test('migrator:migratedVaultForPassword', function (assert) {
var done = assert.async()
this.migrator.migratedVaultForPassword(PASSWORD)
.then((result) => {
const { serialized, lostAccounts } = result
assert.equal(lostAccounts.length, 1, 'one lost account')
assert.equal(lostAccounts[0].address, '0xe15D894BeCB0354c501AE69429B05143679F39e0'.toLowerCase())
assert.ok(lostAccounts[0].privateKey, 'private key exported')
done()
})
})

@ -1,126 +0,0 @@
var KeyringController = require('../../../app/scripts/keyring-controller')
var ConfigManager = require('../../../app/scripts/lib/config-manager')
var oldStyleVault = require('../mocks/oldVault.json')
var badStyleVault = require('../mocks/badVault.json')
var STORAGE_KEY = 'metamask-config'
var PASSWORD = '12345678'
var FIRST_ADDRESS = '0x4dd5d356c5A016A220bCD69e82e5AF680a430d00'.toLowerCase()
var BAD_STYLE_FIRST_ADDRESS = '0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9'
QUnit.module('Old Style Vaults', {
beforeEach: function () {
window.localStorage[STORAGE_KEY] = JSON.stringify(oldStyleVault)
this.configManager = new ConfigManager({
loadData: () => { return JSON.parse(window.localStorage[STORAGE_KEY]) },
setData: (data) => { window.localStorage[STORAGE_KEY] = JSON.stringify(data) },
})
this.keyringController = new KeyringController({
configManager: this.configManager,
getNetwork: () => { return '2' },
txManager: {
getTxList: () => [],
getUnapprovedTxList: () => []
},
})
this.ethStore = {
addAccount: () => {},
removeAccount: () => {},
}
this.keyringController.setStore(this.ethStore)
}
})
QUnit.test('keyringController:isInitialized', function (assert) {
assert.ok(this.keyringController.getState().isInitialized)
})
QUnit.test('keyringController:submitPassword', function (assert) {
var done = assert.async()
this.keyringController.submitPassword(PASSWORD)
.then((state) => {
assert.ok(state.identities[FIRST_ADDRESS])
assert.equal(state.lostAccounts.length, 0, 'no lost accounts')
done()
})
})
QUnit.test('keyringController:setLocked', function (assert) {
var done = assert.async()
var self = this
this.keyringController.setLocked()
.then(function() {
assert.notOk(self.keyringController.password, 'password should be deallocated')
assert.deepEqual(self.keyringController.keyrings, [], 'keyrings should be deallocated')
done()
})
.catch((reason) => {
assert.ifError(reason)
done()
})
})
QUnit.module('Old Style Vaults with bad HD seed', {
beforeEach: function () {
window.localStorage[STORAGE_KEY] = JSON.stringify(badStyleVault)
this.configManager = new ConfigManager({
loadData: () => { return JSON.parse(window.localStorage[STORAGE_KEY]) },
setData: (data) => { window.localStorage[STORAGE_KEY] = JSON.stringify(data) },
})
this.keyringController = new KeyringController({
configManager: this.configManager,
getNetwork: () => { return '2' },
})
this.ethStore = {
addAccount: () => {},
removeAccount: () => {},
}
this.keyringController.setStore(this.ethStore)
}
})
QUnit.test('keyringController:isInitialized', function (assert) {
assert.ok(this.keyringController.getState().isInitialized, 'vault is initialized')
})
QUnit.test('keyringController:submitPassword', function (assert) {
var done = assert.async()
this.keyringController.submitPassword(PASSWORD)
.then((state) => {
assert.ok(state.identities[BAD_STYLE_FIRST_ADDRESS])
assert.equal(state.lostAccounts.length, 1, 'one lost account')
assert.equal(state.lostAccounts[0], '0xe15D894BeCB0354c501AE69429B05143679F39e0'.toLowerCase())
assert.deepEqual(this.configManager.getLostAccounts(), state.lostAccounts, 'persisted')
done()
})
})
QUnit.test('keyringController:setLocked', function (assert) {
var done = assert.async()
var self = this
this.keyringController.setLocked()
.then(function() {
assert.notOk(self.keyringController.password, 'password should be deallocated')
assert.deepEqual(self.keyringController.keyrings, [], 'keyrings should be deallocated')
done()
})
.catch((reason) => {
assert.ifError(reason)
done()
})
})

@ -87,7 +87,7 @@ describe('IdentityStore to KeyringController migration', function() {
keyringController.configManager.setWallet('something')
const state = keyringController.getState()
assert(state.isInitialized, 'old vault counted as initialized.')
assert.equal(state.lostAccounts.length, 0, 'no lost accounts')
assert(!state.lostAccounts, 'no lost accounts')
})
})
})

@ -20,6 +20,7 @@ var actions = {
showNotice: showNotice,
CLEAR_NOTICES: 'CLEAR_NOTICES',
clearNotices: clearNotices,
markAccountsFound,
// intialize screen
AGREE_TO_DISCLAIMER: 'AGREE_TO_DISCLAIMER',
agreeToDisclaimer: agreeToDisclaimer,
@ -591,6 +592,17 @@ function clearNotices () {
}
}
function markAccountsFound() {
return (dispatch) => {
dispatch(this.showLoadingIndication())
background.markAccountsFound((err, newState) => {
dispatch(this.hideLoadingIndication())
if (err) return dispatch(this.showWarning(err.message))
dispatch(actions.updateMetamaskState(newState))
})
}
}
//
// config
//

@ -16,7 +16,8 @@ const AccountDetailScreen = require('./account-detail')
const SendTransactionScreen = require('./send')
const ConfirmTxScreen = require('./conf-tx')
// notice
const NoticeScreen = require('./notice')
const NoticeScreen = require('./components/notice')
const generateLostAccountsNotice = require('../lib/lost-accounts-notice')
// other views
const ConfigScreen = require('./config')
const InfoScreen = require('./info')
@ -55,6 +56,8 @@ function mapStateToProps (state) {
network: state.metamask.network,
provider: state.metamask.provider,
forgottenPassword: state.appState.forgottenPassword,
lastUnreadNotice: state.metamask.lastUnreadNotice,
lostAccounts: state.metamask.lostAccounts,
}
}
@ -366,8 +369,19 @@ App.prototype.renderPrimary = function () {
}
}
// notices
if (!props.noActiveNotices) {
return h(NoticeScreen, {key: 'NoticeScreen'})
return h(NoticeScreen, {
notice: props.lastUnreadNotice,
key: 'NoticeScreen',
onConfirm: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)),
})
} else if (props.lostAccounts && props.lostAccounts.length > 0) {
return h(NoticeScreen, {
notice: generateLostAccountsNotice(props.lostAccounts),
key: 'LostAccountsNotice',
onConfirm: () => props.dispatch(actions.markAccountsFound()),
})
}
// show current view

@ -2,18 +2,10 @@ const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const ReactMarkdown = require('react-markdown')
const connect = require('react-redux').connect
const actions = require('./actions')
const linker = require('extension-link-enabler')
const findDOMNode = require('react-dom').findDOMNode
module.exports = connect(mapStateToProps)(Notice)
function mapStateToProps (state) {
return {
lastUnreadNotice: state.metamask.lastUnreadNotice,
}
}
module.exports = Notice
inherits(Notice, Component)
function Notice () {
@ -21,9 +13,8 @@ function Notice () {
}
Notice.prototype.render = function () {
const props = this.props
const title = props.lastUnreadNotice.title
const date = props.lastUnreadNotice.date
const { notice, onConfirm } = this.props
const { title, date, body } = notice
return (
h('.flex-column.flex-center.flex-grow', [
@ -59,6 +50,7 @@ Notice.prototype.render = function () {
.markdown {
overflow-x: hidden;
}
.markdown h1, .markdown h2, .markdown h3 {
margin: 10px 0;
font-weight: bold;
@ -92,13 +84,13 @@ Notice.prototype.render = function () {
},
}, [
h(ReactMarkdown, {
source: props.lastUnreadNotice.body,
source: body,
skipHtml: true,
}),
]),
h('button', {
onClick: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)),
onClick: onConfirm,
style: {
marginTop: '18px',
},

@ -0,0 +1,23 @@
const summary = require('../app/util').addressSummary
module.exports = function (lostAccounts) {
return {
date: new Date().toDateString(),
title: 'Account Problem Caught',
body: `MetaMask has fixed a bug where some accounts were previously mis-generated. This was a rare issue, but you were affected!
We have successfully imported the accounts that were mis-generated, but they will no longer be recovered with your normal seed phrase.
We have marked the affected accounts as "Loose", and recommend you transfer ether and tokens away from those accounts, or export & back them up elsewhere.
Your affected accounts are:
${lostAccounts.map(acct => ` - ${summary(acct)}`).join('\n')}
These accounts have been marked as "Loose" so they will be easy to recognize in the account list.
For more information, please read [our blog post.][1]
[1]: https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd#.7d8ktj4h3
`,
}
}
Loading…
Cancel
Save