1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00
This commit is contained in:
Alexander Tseung 2018-04-05 23:17:22 -07:00
commit 4c15c0d3ba
17 changed files with 315 additions and 49 deletions

View File

@ -20,6 +20,7 @@ const reportFailedTxToSentry = require('./lib/reportFailedTxToSentry')
const setupMetamaskMeshMetrics = require('./lib/setupMetamaskMeshMetrics') const setupMetamaskMeshMetrics = require('./lib/setupMetamaskMeshMetrics')
const EdgeEncryptor = require('./edge-encryptor') const EdgeEncryptor = require('./edge-encryptor')
const getFirstPreferredLangCode = require('./lib/get-first-preferred-lang-code') const getFirstPreferredLangCode = require('./lib/get-first-preferred-lang-code')
const getObjStructure = require('./lib/getObjStructure')
const STORAGE_KEY = 'metamask-config' const STORAGE_KEY = 'metamask-config'
const METAMASK_DEBUG = process.env.METAMASK_DEBUG const METAMASK_DEBUG = process.env.METAMASK_DEBUG
@ -77,6 +78,16 @@ async function loadStateFromPersistence () {
diskStore.getState() || diskStore.getState() ||
migrator.generateInitialState(firstTimeState) migrator.generateInitialState(firstTimeState)
// report migration errors to sentry
migrator.on('error', (err) => {
// get vault structure without secrets
const vaultStructure = getObjStructure(versionedData)
raven.captureException(err, {
// "extra" key is required by Sentry
extra: { vaultStructure },
})
})
// migrate data // migrate data
versionedData = await migrator.migrateData(versionedData) versionedData = await migrator.migrateData(versionedData)
if (!versionedData) { if (!versionedData) {
@ -84,7 +95,14 @@ async function loadStateFromPersistence () {
} }
// write to disk // write to disk
if (localStore.isSupported) localStore.set(versionedData) if (localStore.isSupported) {
localStore.set(versionedData)
} else {
// throw in setTimeout so as to not block boot
setTimeout(() => {
throw new Error('MetaMask - Localstore not supported')
})
}
// return just the data // return just the data
return versionedData.data return versionedData.data

View File

@ -185,10 +185,10 @@ module.exports = class TransactionController extends EventEmitter {
async addUnapprovedTransaction (txParams) { async addUnapprovedTransaction (txParams) {
// validate // validate
this._validateTxParams(txParams) const normalizedTxParams = this._normalizeTxParams(txParams)
this._normalizeTxParams(txParams) this._validateTxParams(normalizedTxParams)
// construct txMeta // construct txMeta
let txMeta = this.txStateManager.generateTxMeta({txParams}) let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams })
this.addTx(txMeta) this.addTx(txMeta)
this.emit('newUnapprovedTx', txMeta) this.emit('newUnapprovedTx', txMeta)
// add default tx params // add default tx params
@ -315,25 +315,24 @@ module.exports = class TransactionController extends EventEmitter {
// //
_normalizeTxParams (txParams) { _normalizeTxParams (txParams) {
delete txParams.chainId // functions that handle normalizing of that key in txParams
const whiteList = {
if ( !txParams.to ) { from: from => ethUtil.addHexPrefix(from).toLowerCase(),
delete txParams.to to: to => ethUtil.addHexPrefix(txParams.to).toLowerCase(),
} else { nonce: nonce => ethUtil.addHexPrefix(nonce),
txParams.to = ethUtil.addHexPrefix(txParams.to) value: value => ethUtil.addHexPrefix(value),
} data: data => ethUtil.addHexPrefix(data),
txParams.from = ethUtil.addHexPrefix(txParams.from).toLowerCase() gas: gas => ethUtil.addHexPrefix(gas),
gasPrice: gasPrice => ethUtil.addHexPrefix(gasPrice),
if (!txParams.data) {
delete txParams.data
} else {
txParams.data = ethUtil.addHexPrefix(txParams.data)
} }
if (txParams.value) txParams.value = ethUtil.addHexPrefix(txParams.value) // apply only keys in the whiteList
const normalizedTxParams = {}
Object.keys(whiteList).forEach((key) => {
if (txParams[key]) normalizedTxParams[key] = whiteList[key](txParams[key])
})
if (txParams.gas) txParams.gas = ethUtil.addHexPrefix(txParams.gas) return normalizedTxParams
if (txParams.gasPrice) txParams.gas = ethUtil.addHexPrefix(txParams.gas)
} }
_validateTxParams (txParams) { _validateTxParams (txParams) {

View File

@ -2,14 +2,16 @@ const extension = require('extensionizer')
const promisify = require('pify') const promisify = require('pify')
const allLocales = require('../../_locales/index.json') const allLocales = require('../../_locales/index.json')
const existingLocaleCodes = allLocales.map(locale => locale.code) const existingLocaleCodes = allLocales.map(locale => locale.code.toLowerCase().replace('_', '-'))
async function getFirstPreferredLangCode () { async function getFirstPreferredLangCode () {
const userPreferredLocaleCodes = await promisify( const userPreferredLocaleCodes = await promisify(
extension.i18n.getAcceptLanguages, extension.i18n.getAcceptLanguages,
{ errorFirst: false } { errorFirst: false }
)() )()
const firstPreferredLangCode = userPreferredLocaleCodes.find(code => existingLocaleCodes.includes(code)) const firstPreferredLangCode = userPreferredLocaleCodes
.map(code => code.toLowerCase())
.find(code => existingLocaleCodes.includes(code))
return firstPreferredLangCode || 'en' return firstPreferredLangCode || 'en'
} }

View File

@ -0,0 +1,33 @@
const clone = require('clone')
module.exports = getObjStructure
// This will create an object that represents the structure of the given object
// it replaces all values with the result of their type
// {
// "data": {
// "CurrencyController": {
// "conversionDate": "number",
// "conversionRate": "number",
// "currentCurrency": "string"
// }
// }
function getObjStructure(obj) {
const structure = clone(obj)
return deepMap(structure, (value) => {
return value === null ? 'null' : typeof value
})
}
function deepMap(target = {}, visit) {
Object.entries(target).forEach(([key, value]) => {
if (typeof value === 'object' && value !== null) {
target[key] = deepMap(value, visit)
} else {
target[key] = visit(value)
}
})
return target
}

View File

@ -1,6 +1,9 @@
class Migrator { const EventEmitter = require('events')
class Migrator extends EventEmitter {
constructor (opts = {}) { constructor (opts = {}) {
super()
const migrations = opts.migrations || [] const migrations = opts.migrations || []
// sort migrations by version // sort migrations by version
this.migrations = migrations.sort((a, b) => a.version - b.version) this.migrations = migrations.sort((a, b) => a.version - b.version)
@ -12,13 +15,29 @@ class Migrator {
// run all pending migrations on meta in place // run all pending migrations on meta in place
async migrateData (versionedData = this.generateInitialState()) { async migrateData (versionedData = this.generateInitialState()) {
// get all migrations that have not yet been run
const pendingMigrations = this.migrations.filter(migrationIsPending) const pendingMigrations = this.migrations.filter(migrationIsPending)
// perform each migration
for (const index in pendingMigrations) { for (const index in pendingMigrations) {
const migration = pendingMigrations[index] const migration = pendingMigrations[index]
versionedData = await migration.migrate(versionedData) try {
if (!versionedData.data) throw new Error('Migrator - migration returned empty data') // attempt migration and validate
if (versionedData.version !== undefined && versionedData.meta.version !== migration.version) throw new Error('Migrator - Migration did not update version number correctly') const migratedData = await migration.migrate(versionedData)
if (!migratedData.data) throw new Error('Migrator - migration returned empty data')
if (migratedData.version !== undefined && migratedData.meta.version !== migration.version) throw new Error('Migrator - Migration did not update version number correctly')
// accept the migration as good
versionedData = migratedData
} catch (err) {
// rewrite error message to add context without clobbering stack
const originalErrorMessage = err.message
err.message = `MetaMask Migration Error #${migration.version}: ${originalErrorMessage}`
console.warn(err.stack)
// emit error instead of throw so as to not break the run (gracefully fail)
this.emit('error', err)
// stop migrating and use state as is
return versionedData
}
} }
return versionedData return versionedData

View File

@ -108,6 +108,10 @@ module.exports = class TransactionStateManager extends EventEmitter {
updateTx (txMeta, note) { updateTx (txMeta, note) {
// validate txParams // validate txParams
if (txMeta.txParams) { if (txMeta.txParams) {
if (typeof txMeta.txParams.data === 'undefined') {
delete txMeta.txParams.data
}
this.validateTxParams(txMeta.txParams) this.validateTxParams(txMeta.txParams)
} }

View File

@ -13,24 +13,20 @@ const clone = require('clone')
module.exports = { module.exports = {
version, version,
migrate: function (originalVersionedData) { migrate: async function (originalVersionedData) {
const versionedData = clone(originalVersionedData) const versionedData = clone(originalVersionedData)
versionedData.meta.version = version versionedData.meta.version = version
try { const state = versionedData.data
const state = versionedData.data const newState = transformState(state)
const newState = transformState(state) versionedData.data = newState
versionedData.data = newState return versionedData
} catch (err) {
console.warn(`MetaMask Migration #${version}` + err.stack)
}
return Promise.resolve(versionedData)
}, },
} }
function transformState (state) { function transformState (state) {
const newState = state const newState = state
if (!newState.TransactionController) return newState
const transactions = newState.TransactionController.transactions const transactions = newState.TransactionController.transactions
newState.TransactionController.transactions = transactions.map((txMeta, _, txList) => { newState.TransactionController.transactions = transactions.map((txMeta, _, txList) => {
if ( if (
txMeta.status === 'unapproved' && txMeta.status === 'unapproved' &&

View File

@ -0,0 +1,61 @@
// next version number
const version = 25
/*
normalizes txParams on unconfirmed txs
*/
const ethUtil = require('ethereumjs-util')
const clone = require('clone')
module.exports = {
version,
migrate: async function (originalVersionedData) {
const versionedData = clone(originalVersionedData)
versionedData.meta.version = version
const state = versionedData.data
const newState = transformState(state)
versionedData.data = newState
return versionedData
},
}
function transformState (state) {
const newState = state
if (newState.TransactionController) {
if (newState.TransactionController.transactions) {
const transactions = newState.TransactionController.transactions
newState.TransactionController.transactions = transactions.map((txMeta) => {
if (txMeta.status !== 'unapproved') return txMeta
txMeta.txParams = normalizeTxParams(txMeta.txParams)
return txMeta
})
}
}
return newState
}
function normalizeTxParams (txParams) {
// functions that handle normalizing of that key in txParams
const whiteList = {
from: from => ethUtil.addHexPrefix(from).toLowerCase(),
to: to => ethUtil.addHexPrefix(txParams.to).toLowerCase(),
nonce: nonce => ethUtil.addHexPrefix(nonce),
value: value => ethUtil.addHexPrefix(value),
data: data => ethUtil.addHexPrefix(data),
gas: gas => ethUtil.addHexPrefix(gas),
gasPrice: gasPrice => ethUtil.addHexPrefix(gasPrice),
}
// apply only keys in the whiteList
const normalizedTxParams = {}
Object.keys(whiteList).forEach((key) => {
if (txParams[key]) normalizedTxParams[key] = whiteList[key](txParams[key])
})
return normalizedTxParams
}

View File

@ -35,4 +35,5 @@ module.exports = [
require('./022'), require('./022'),
require('./023'), require('./023'),
require('./024'), require('./024'),
require('./025'),
] ]

View File

@ -0,0 +1,29 @@
// next version number
const version = 0
/*
description of migration and what it does
*/
const clone = require('clone')
module.exports = {
version,
migrate: async function (originalVersionedData) {
const versionedData = clone(originalVersionedData)
versionedData.meta.version = version
const state = versionedData.data
const newState = transformState(state)
versionedData.data = newState
return versionedData
},
}
function transformState (state) {
const newState = state
// transform state here
return newState
}

View File

@ -1,5 +1,9 @@
const assert = require('assert') const assert = require('assert')
const migration24 = require('../../../app/scripts/migrations/024') const migration24 = require('../../../app/scripts/migrations/024')
const firstTimeState = {
meta: {},
data: require('../../../app/scripts/first-time-state'),
}
const properTime = (new Date()).getTime() const properTime = (new Date()).getTime()
const storage = { const storage = {
"meta": {}, "meta": {},
@ -34,4 +38,12 @@ describe('storage is migrated successfully and the txParams.from are lowercase',
done() done()
}).catch(done) }).catch(done)
}) })
it('should migrate first time state', (done) => {
migration24.migrate(firstTimeState)
.then((migratedData) => {
assert.equal(migratedData.meta.version, 24)
done()
}).catch(done)
})
}) })

View File

@ -0,0 +1,49 @@
const assert = require('assert')
const migration25 = require('../../../app/scripts/migrations/025')
const firstTimeState = {
meta: {},
data: require('../../../app/scripts/first-time-state'),
}
const storage = {
"meta": {},
"data": {
"TransactionController": {
"transactions": [
]
},
},
}
const transactions = []
while (transactions.length <= 10) {
transactions.push({ txParams: { from: '0x8aCce2391c0d510a6c5E5d8f819a678f79b7e675', random: 'stuff', chainId: 2 }, status: 'unapproved' })
transactions.push({ txParams: { from: '0x8aCce2391c0d510a6c5E5d8f819a678f79b7e675' }, status: 'confirmed' })
}
storage.data.TransactionController.transactions = transactions
describe('storage is migrated successfully and the txParams.from are lowercase', () => {
it('should lowercase the from for unapproved txs', (done) => {
migration25.migrate(storage)
.then((migratedData) => {
const migratedTransactions = migratedData.data.TransactionController.transactions
migratedTransactions.forEach((tx) => {
if (tx.status === 'unapproved') assert(!tx.txParams.random)
if (tx.status === 'unapproved') assert(!tx.txParams.chainId)
})
done()
}).catch(done)
})
it('should migrate first time state', (done) => {
migration25.migrate(firstTimeState)
.then((migratedData) => {
assert.equal(migratedData.meta.version, 25)
done()
}).catch(done)
})
})

View File

@ -0,0 +1,17 @@
const assert = require('assert')
const migrationTemplate = require('../../../app/scripts/migrations/template')
const properTime = (new Date()).getTime()
const storage = {
meta: {},
data: {},
}
describe('storage is migrated successfully', () => {
it('should work', (done) => {
migrationTemplate.migrate(storage)
.then((migratedData) => {
assert.equal(migratedData.meta.version, 0)
done()
}).catch(done)
})
})

View File

@ -1,7 +1,8 @@
const assert = require('assert') const assert = require('assert')
const clone = require('clone') const clone = require('clone')
const Migrator = require('../../app/scripts/lib/migrator/') const Migrator = require('../../app/scripts/lib/migrator/')
const migrations = [ const liveMigrations = require('../../app/scripts/migrations/')
const stubMigrations = [
{ {
version: 1, version: 1,
migrate: (data) => { migrate: (data) => {
@ -29,13 +30,31 @@ const migrations = [
}, },
] ]
const versionedData = {meta: {version: 0}, data: {hello: 'world'}} const versionedData = {meta: {version: 0}, data: {hello: 'world'}}
const firstTimeState = {
meta: { version: 0 },
data: require('../../app/scripts/first-time-state'),
}
describe('Migrator', () => { describe('Migrator', () => {
const migrator = new Migrator({ migrations }) const migrator = new Migrator({ migrations: stubMigrations })
it('migratedData version should be version 3', (done) => { it('migratedData version should be version 3', (done) => {
migrator.migrateData(versionedData) migrator.migrateData(versionedData)
.then((migratedData) => { .then((migratedData) => {
assert.equal(migratedData.meta.version, migrations[2].version) assert.equal(migratedData.meta.version, stubMigrations[2].version)
done() done()
}).catch(done) }).catch(done)
}) })
it('should match the last version in live migrations', (done) => {
const migrator = new Migrator({ migrations: liveMigrations })
migrator.migrateData(firstTimeState)
.then((migratedData) => {
console.log(migratedData)
const last = liveMigrations.length - 1
assert.equal(migratedData.meta.version, liveMigrations[last].version)
done()
}).catch(done)
})
}) })

View File

@ -239,19 +239,20 @@ describe('Transaction Controller', function () {
from: 'a7df1beDBF813f57096dF77FCd515f0B3900e402', from: 'a7df1beDBF813f57096dF77FCd515f0B3900e402',
to: null, to: null,
data: '68656c6c6f20776f726c64', data: '68656c6c6f20776f726c64',
random: 'hello world',
} }
txController._normalizeTxParams(txParams) let normalizedTxParams = txController._normalizeTxParams(txParams)
assert(!txParams.chainId, 'their should be no chainId') assert(!normalizedTxParams.chainId, 'their should be no chainId')
assert(!txParams.to, 'their should be no to address if null') assert(!normalizedTxParams.to, 'their should be no to address if null')
assert.equal(txParams.from.slice(0, 2), '0x', 'from should be hexPrefixd') assert.equal(normalizedTxParams.from.slice(0, 2), '0x', 'from should be hexPrefixd')
assert.equal(txParams.data.slice(0, 2), '0x', 'data should be hexPrefixd') assert.equal(normalizedTxParams.data.slice(0, 2), '0x', 'data should be hexPrefixd')
assert(!('random' in normalizedTxParams), 'their should be no random key in normalizedTxParams')
txParams.to = 'a7df1beDBF813f57096dF77FCd515f0B3900e402' txParams.to = 'a7df1beDBF813f57096dF77FCd515f0B3900e402'
normalizedTxParams = txController._normalizeTxParams(txParams)
txController._normalizeTxParams(txParams) assert.equal(normalizedTxParams.to.slice(0, 2), '0x', 'to should be hexPrefixd')
assert.equal(txParams.to.slice(0, 2), '0x', 'to should be hexPrefixd')
}) })
}) })

View File

@ -35,6 +35,7 @@ function mapDispatchToProps (dispatch) {
inherits(PrivateKeyImportView, Component) inherits(PrivateKeyImportView, Component)
function PrivateKeyImportView () { function PrivateKeyImportView () {
this.createKeyringOnEnter = this.createKeyringOnEnter.bind(this)
Component.call(this) Component.call(this)
} }
@ -51,7 +52,7 @@ PrivateKeyImportView.prototype.render = function () {
h('input.new-account-import-form__input-password', { h('input.new-account-import-form__input-password', {
type: 'password', type: 'password',
id: 'private-key-box', id: 'private-key-box',
onKeyPress: () => this.createKeyringOnEnter(), onKeyPress: e => this.createKeyringOnEnter(e),
}), }),
]), ]),

View File

@ -577,12 +577,17 @@ SendTransactionScreen.prototype.getEditedTx = function () {
data, data,
}) })
} else { } else {
const data = unapprovedTxs[editingTransactionId].txParams.data const { data } = unapprovedTxs[editingTransactionId].txParams
Object.assign(editingTx.txParams, { Object.assign(editingTx.txParams, {
value: ethUtil.addHexPrefix(amount), value: ethUtil.addHexPrefix(amount),
to: ethUtil.addHexPrefix(to), to: ethUtil.addHexPrefix(to),
data, data,
}) })
if (typeof editingTx.txParams.data === 'undefined') {
delete editingTx.txParams.data
}
} }
return editingTx return editingTx