mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-24 02:58:09 +01:00
ebeaf3b3d6
Migrator now returns a lostAccount array that includes objects these objects include keys of address and privateKey, this allows the MetamaskController to restore the lost accounts even without customizing the idStore or the KeyringController. Also includes a patch that allows idStore to synchronously export private keys.
603 lines
16 KiB
JavaScript
603 lines
16 KiB
JavaScript
const EventEmitter = require('events').EventEmitter
|
|
const inherits = require('util').inherits
|
|
const async = require('async')
|
|
const ethUtil = require('ethereumjs-util')
|
|
const BN = ethUtil.BN
|
|
const EthQuery = require('eth-query')
|
|
const KeyStore = require('eth-lightwallet').keystore
|
|
const clone = require('clone')
|
|
const extend = require('xtend')
|
|
const createId = require('./random-id')
|
|
const ethBinToOps = require('eth-bin-to-ops')
|
|
const autoFaucet = require('./auto-faucet')
|
|
const messageManager = require('./message-manager')
|
|
const DEFAULT_RPC = 'https://testrpc.metamask.io/'
|
|
const IdManagement = require('./id-management')
|
|
|
|
module.exports = IdentityStore
|
|
|
|
inherits(IdentityStore, EventEmitter)
|
|
function IdentityStore (opts = {}) {
|
|
EventEmitter.call(this)
|
|
|
|
// we just use the ethStore to auto-add accounts
|
|
this._ethStore = opts.ethStore
|
|
this.configManager = opts.configManager
|
|
// lightwallet key store
|
|
this._keyStore = null
|
|
// lightwallet wrapper
|
|
this._idmgmt = null
|
|
|
|
this.hdPathString = "m/44'/60'/0'/0"
|
|
|
|
this._currentState = {
|
|
selectedAddress: null,
|
|
identities: {},
|
|
}
|
|
|
|
// not part of serilized metamask state - only kept in memory
|
|
this._unconfTxCbs = {}
|
|
this._unconfMsgCbs = {}
|
|
}
|
|
|
|
//
|
|
// public
|
|
//
|
|
|
|
IdentityStore.prototype.createNewVault = function (password, cb) {
|
|
delete this._keyStore
|
|
var serializedKeystore = this.configManager.getWallet()
|
|
|
|
if (serializedKeystore) {
|
|
this.configManager.setData({})
|
|
}
|
|
|
|
this.purgeCache()
|
|
this._createVault(password, null, (err) => {
|
|
if (err) return cb(err)
|
|
|
|
this._autoFaucet()
|
|
|
|
this.configManager.setShowSeedWords(true)
|
|
var seedWords = this._idmgmt.getSeed()
|
|
|
|
this._loadIdentities()
|
|
|
|
cb(null, seedWords)
|
|
})
|
|
}
|
|
|
|
IdentityStore.prototype.recoverSeed = function (cb) {
|
|
this.configManager.setShowSeedWords(true)
|
|
if (!this._idmgmt) return cb(new Error('Unauthenticated. Please sign in.'))
|
|
var seedWords = this._idmgmt.getSeed()
|
|
cb(null, seedWords)
|
|
}
|
|
|
|
IdentityStore.prototype.recoverFromSeed = function (password, seed, cb) {
|
|
this.purgeCache()
|
|
|
|
this._createVault(password, seed, (err) => {
|
|
if (err) return cb(err)
|
|
|
|
this._loadIdentities()
|
|
cb(null, this.getState())
|
|
})
|
|
}
|
|
|
|
IdentityStore.prototype.setStore = function (store) {
|
|
this._ethStore = store
|
|
}
|
|
|
|
IdentityStore.prototype.clearSeedWordCache = function (cb) {
|
|
const configManager = this.configManager
|
|
configManager.setShowSeedWords(false)
|
|
cb(null, configManager.getSelectedAccount())
|
|
}
|
|
|
|
IdentityStore.prototype.getState = function () {
|
|
const configManager = this.configManager
|
|
var seedWords = this.getSeedIfUnlocked()
|
|
return clone(extend(this._currentState, {
|
|
isInitialized: !!configManager.getWallet() && !seedWords,
|
|
isUnlocked: this._isUnlocked(),
|
|
seedWords: seedWords,
|
|
isDisclaimerConfirmed: configManager.getConfirmedDisclaimer(),
|
|
unconfTxs: configManager.unconfirmedTxs(),
|
|
transactions: configManager.getTxList(),
|
|
unconfMsgs: messageManager.unconfirmedMsgs(),
|
|
messages: messageManager.getMsgList(),
|
|
selectedAddress: configManager.getSelectedAccount(),
|
|
shapeShiftTxList: configManager.getShapeShiftTxList(),
|
|
currentFiat: configManager.getCurrentFiat(),
|
|
conversionRate: configManager.getConversionRate(),
|
|
conversionDate: configManager.getConversionDate(),
|
|
gasMultiplier: configManager.getGasMultiplier(),
|
|
}))
|
|
}
|
|
|
|
IdentityStore.prototype.getSeedIfUnlocked = function () {
|
|
const configManager = this.configManager
|
|
var showSeed = configManager.getShouldShowSeedWords()
|
|
var idmgmt = this._idmgmt
|
|
var shouldShow = showSeed && !!idmgmt
|
|
var seedWords = shouldShow ? idmgmt.getSeed() : null
|
|
return seedWords
|
|
}
|
|
|
|
IdentityStore.prototype.getSelectedAddress = function () {
|
|
const configManager = this.configManager
|
|
return configManager.getSelectedAccount()
|
|
}
|
|
|
|
IdentityStore.prototype.setSelectedAddressSync = function (address) {
|
|
const configManager = this.configManager
|
|
if (!address) {
|
|
var addresses = this._getAddresses()
|
|
address = addresses[0]
|
|
}
|
|
|
|
configManager.setSelectedAccount(address)
|
|
return address
|
|
}
|
|
|
|
IdentityStore.prototype.setSelectedAddress = function (address, cb) {
|
|
const resultAddress = this.setSelectedAddressSync(address)
|
|
if (cb) return cb(null, resultAddress)
|
|
}
|
|
|
|
IdentityStore.prototype.revealAccount = function (cb) {
|
|
const derivedKey = this._idmgmt.derivedKey
|
|
const keyStore = this._keyStore
|
|
const configManager = this.configManager
|
|
|
|
keyStore.setDefaultHdDerivationPath(this.hdPathString)
|
|
keyStore.generateNewAddress(derivedKey, 1)
|
|
const addresses = keyStore.getAddresses()
|
|
const address = addresses[ addresses.length - 1 ]
|
|
|
|
this._ethStore.addAccount(ethUtil.addHexPrefix(address))
|
|
|
|
configManager.setWallet(keyStore.serialize())
|
|
|
|
this._loadIdentities()
|
|
this._didUpdate()
|
|
cb(null)
|
|
}
|
|
|
|
IdentityStore.prototype.getNetwork = function (err) {
|
|
if (err) {
|
|
this._currentState.network = 'loading'
|
|
this._didUpdate()
|
|
}
|
|
|
|
this.web3.version.getNetwork((err, network) => {
|
|
if (err) {
|
|
this._currentState.network = 'loading'
|
|
return this._didUpdate()
|
|
}
|
|
if (global.METAMASK_DEBUG) {
|
|
console.log('web3.getNetwork returned ' + network)
|
|
}
|
|
this._currentState.network = network
|
|
this._didUpdate()
|
|
})
|
|
}
|
|
|
|
IdentityStore.prototype.setLocked = function (cb) {
|
|
delete this._keyStore
|
|
delete this._idmgmt
|
|
cb()
|
|
}
|
|
|
|
IdentityStore.prototype.submitPassword = function (password, cb) {
|
|
const configManager = this.configManager
|
|
this.tryPassword(password, (err) => {
|
|
if (err) return cb(err)
|
|
// load identities before returning...
|
|
this._loadIdentities()
|
|
cb(null, configManager.getSelectedAccount())
|
|
})
|
|
}
|
|
|
|
IdentityStore.prototype.exportAccount = function (address, cb) {
|
|
var privateKey = this._idmgmt.exportPrivateKey(address)
|
|
if (cb) cb(null, privateKey)
|
|
return privateKey
|
|
}
|
|
|
|
//
|
|
// Transactions
|
|
//
|
|
|
|
// comes from dapp via zero-client hooked-wallet provider
|
|
IdentityStore.prototype.addUnconfirmedTransaction = function (txParams, onTxDoneCb, cb) {
|
|
const configManager = this.configManager
|
|
|
|
var self = this
|
|
// create txData obj with parameters and meta data
|
|
var time = (new Date()).getTime()
|
|
var txId = createId()
|
|
txParams.metamaskId = txId
|
|
txParams.metamaskNetworkId = self._currentState.network
|
|
var txData = {
|
|
id: txId,
|
|
txParams: txParams,
|
|
time: time,
|
|
status: 'unconfirmed',
|
|
gasMultiplier: configManager.getGasMultiplier() || 1,
|
|
}
|
|
|
|
console.log('addUnconfirmedTransaction:', txData)
|
|
|
|
// keep the onTxDoneCb around for after approval/denial (requires user interaction)
|
|
// This onTxDoneCb fires completion to the Dapp's write operation.
|
|
self._unconfTxCbs[txId] = onTxDoneCb
|
|
|
|
var provider = self._ethStore._query.currentProvider
|
|
var query = new EthQuery(provider)
|
|
|
|
// calculate metadata for tx
|
|
async.parallel([
|
|
analyzeForDelegateCall,
|
|
estimateGas,
|
|
], didComplete)
|
|
|
|
// perform static analyis on the target contract code
|
|
function analyzeForDelegateCall (cb) {
|
|
if (txParams.to) {
|
|
query.getCode(txParams.to, (err, result) => {
|
|
if (err) return cb(err.message || err)
|
|
var containsDelegateCall = self.checkForDelegateCall(result)
|
|
txData.containsDelegateCall = containsDelegateCall
|
|
cb()
|
|
})
|
|
} else {
|
|
cb()
|
|
}
|
|
}
|
|
|
|
function estimateGas (cb) {
|
|
var estimationParams = extend(txParams)
|
|
query.getBlockByNumber('latest', true, function (err, block) {
|
|
if (err) return cb(err)
|
|
// check if gasLimit is already specified
|
|
const gasLimitSpecified = Boolean(txParams.gas)
|
|
// if not, fallback to block gasLimit
|
|
if (!gasLimitSpecified) {
|
|
estimationParams.gas = block.gasLimit
|
|
}
|
|
// run tx, see if it will OOG
|
|
query.estimateGas(estimationParams, function (err, estimatedGasHex) {
|
|
if (err) return cb(err.message || err)
|
|
// all gas used - must be an error
|
|
if (estimatedGasHex === estimationParams.gas) {
|
|
txData.simulationFails = true
|
|
txData.estimatedGas = estimatedGasHex
|
|
txData.txParams.gas = estimatedGasHex
|
|
cb()
|
|
return
|
|
}
|
|
// otherwise, did not use all gas, must be ok
|
|
|
|
// if specified gasLimit and no error, we're done
|
|
if (gasLimitSpecified) {
|
|
txData.estimatedGas = txParams.gas
|
|
cb()
|
|
return
|
|
}
|
|
|
|
// try adding an additional gas buffer to our estimation for safety
|
|
const estimatedGasBn = new BN(ethUtil.stripHexPrefix(estimatedGasHex), 16)
|
|
const blockGasLimitBn = new BN(ethUtil.stripHexPrefix(block.gasLimit), 16)
|
|
const estimationWithBuffer = self.addGasBuffer(estimatedGasBn)
|
|
// added gas buffer is too high
|
|
if (estimationWithBuffer.gt(blockGasLimitBn)) {
|
|
txData.estimatedGas = estimatedGasHex
|
|
txData.txParams.gas = estimatedGasHex
|
|
// added gas buffer is safe
|
|
} else {
|
|
const gasWithBufferHex = ethUtil.intToHex(estimationWithBuffer)
|
|
txData.estimatedGas = gasWithBufferHex
|
|
txData.txParams.gas = gasWithBufferHex
|
|
}
|
|
cb()
|
|
return
|
|
})
|
|
})
|
|
}
|
|
|
|
function didComplete (err) {
|
|
if (err) return cb(err.message || err)
|
|
configManager.addTx(txData)
|
|
// signal update
|
|
self._didUpdate()
|
|
// signal completion of add tx
|
|
cb(null, txData)
|
|
}
|
|
}
|
|
|
|
IdentityStore.prototype.checkForDelegateCall = function (codeHex) {
|
|
const code = ethUtil.toBuffer(codeHex)
|
|
if (code !== '0x') {
|
|
const ops = ethBinToOps(code)
|
|
const containsDelegateCall = ops.some((op) => op.name === 'DELEGATECALL')
|
|
return containsDelegateCall
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
IdentityStore.prototype.addGasBuffer = function (gasBn) {
|
|
// add 20% to specified gas
|
|
const gasBuffer = gasBn.div(new BN('5', 10))
|
|
const gasWithBuffer = gasBn.add(gasBuffer)
|
|
return gasWithBuffer
|
|
}
|
|
|
|
// comes from metamask ui
|
|
IdentityStore.prototype.approveTransaction = function (txId, cb) {
|
|
const configManager = this.configManager
|
|
var approvalCb = this._unconfTxCbs[txId] || noop
|
|
|
|
// accept tx
|
|
cb()
|
|
approvalCb(null, true)
|
|
// clean up
|
|
configManager.confirmTx(txId)
|
|
delete this._unconfTxCbs[txId]
|
|
this._didUpdate()
|
|
}
|
|
|
|
// comes from metamask ui
|
|
IdentityStore.prototype.cancelTransaction = function (txId) {
|
|
const configManager = this.configManager
|
|
var approvalCb = this._unconfTxCbs[txId] || noop
|
|
|
|
// reject tx
|
|
approvalCb(null, false)
|
|
// clean up
|
|
configManager.rejectTx(txId)
|
|
delete this._unconfTxCbs[txId]
|
|
this._didUpdate()
|
|
}
|
|
|
|
// performs the actual signing, no autofill of params
|
|
IdentityStore.prototype.signTransaction = function (txParams, cb) {
|
|
try {
|
|
console.log('signing tx...', txParams)
|
|
var rawTx = this._idmgmt.signTx(txParams)
|
|
cb(null, rawTx)
|
|
} catch (err) {
|
|
cb(err)
|
|
}
|
|
}
|
|
|
|
//
|
|
// Messages
|
|
//
|
|
|
|
// comes from dapp via zero-client hooked-wallet provider
|
|
IdentityStore.prototype.addUnconfirmedMessage = function (msgParams, cb) {
|
|
// create txData obj with parameters and meta data
|
|
var time = (new Date()).getTime()
|
|
var msgId = createId()
|
|
var msgData = {
|
|
id: msgId,
|
|
msgParams: msgParams,
|
|
time: time,
|
|
status: 'unconfirmed',
|
|
}
|
|
messageManager.addMsg(msgData)
|
|
console.log('addUnconfirmedMessage:', msgData)
|
|
|
|
// keep the cb around for after approval (requires user interaction)
|
|
// This cb fires completion to the Dapp's write operation.
|
|
this._unconfMsgCbs[msgId] = cb
|
|
|
|
// signal update
|
|
this._didUpdate()
|
|
|
|
return msgId
|
|
}
|
|
|
|
// comes from metamask ui
|
|
IdentityStore.prototype.approveMessage = function (msgId, cb) {
|
|
var approvalCb = this._unconfMsgCbs[msgId] || noop
|
|
|
|
// accept msg
|
|
cb()
|
|
approvalCb(null, true)
|
|
// clean up
|
|
messageManager.confirmMsg(msgId)
|
|
delete this._unconfMsgCbs[msgId]
|
|
this._didUpdate()
|
|
}
|
|
|
|
// comes from metamask ui
|
|
IdentityStore.prototype.cancelMessage = function (msgId) {
|
|
var approvalCb = this._unconfMsgCbs[msgId] || noop
|
|
|
|
// reject tx
|
|
approvalCb(null, false)
|
|
// clean up
|
|
messageManager.rejectMsg(msgId)
|
|
delete this._unconfTxCbs[msgId]
|
|
this._didUpdate()
|
|
}
|
|
|
|
// performs the actual signing, no autofill of params
|
|
IdentityStore.prototype.signMessage = function (msgParams, cb) {
|
|
try {
|
|
console.log('signing msg...', msgParams.data)
|
|
var rawMsg = this._idmgmt.signMsg(msgParams.from, msgParams.data)
|
|
if ('metamaskId' in msgParams) {
|
|
var id = msgParams.metamaskId
|
|
delete msgParams.metamaskId
|
|
|
|
this.approveMessage(id, cb)
|
|
} else {
|
|
cb(null, rawMsg)
|
|
}
|
|
} catch (err) {
|
|
cb(err)
|
|
}
|
|
}
|
|
|
|
//
|
|
// private
|
|
//
|
|
|
|
IdentityStore.prototype._didUpdate = function () {
|
|
this.emit('update', this.getState())
|
|
}
|
|
|
|
IdentityStore.prototype._isUnlocked = function () {
|
|
var result = Boolean(this._keyStore) && Boolean(this._idmgmt)
|
|
return result
|
|
}
|
|
|
|
// load identities from keyStoreet
|
|
IdentityStore.prototype._loadIdentities = function () {
|
|
const configManager = this.configManager
|
|
if (!this._isUnlocked()) throw new Error('not unlocked')
|
|
|
|
var addresses = this._getAddresses()
|
|
addresses.forEach((address, i) => {
|
|
// // add to ethStore
|
|
if (this._ethStore) {
|
|
this._ethStore.addAccount(ethUtil.addHexPrefix(address))
|
|
}
|
|
// add to identities
|
|
const defaultLabel = 'Account ' + (i + 1)
|
|
const nickname = configManager.nicknameForWallet(address)
|
|
var identity = {
|
|
name: nickname || defaultLabel,
|
|
address: address,
|
|
mayBeFauceting: this._mayBeFauceting(i),
|
|
}
|
|
this._currentState.identities[address] = identity
|
|
})
|
|
this._didUpdate()
|
|
}
|
|
|
|
IdentityStore.prototype.saveAccountLabel = function (account, label, cb) {
|
|
const configManager = this.configManager
|
|
configManager.setNicknameForWallet(account, label)
|
|
this._loadIdentities()
|
|
cb(null, label)
|
|
}
|
|
|
|
// mayBeFauceting
|
|
// If on testnet, index 0 may be fauceting.
|
|
// The UI will have to check the balance to know.
|
|
// If there is no balance and it mayBeFauceting,
|
|
// then it is in fact fauceting.
|
|
IdentityStore.prototype._mayBeFauceting = function (i) {
|
|
const configManager = this.configManager
|
|
var config = configManager.getProvider()
|
|
if (i === 0 &&
|
|
config.type === 'rpc' &&
|
|
config.rpcTarget === DEFAULT_RPC) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
//
|
|
// keyStore managment - unlocking + deserialization
|
|
//
|
|
|
|
IdentityStore.prototype.tryPassword = function (password, cb) {
|
|
var serializedKeystore = this.configManager.getWallet()
|
|
var keyStore = KeyStore.deserialize(serializedKeystore)
|
|
|
|
keyStore.keyFromPassword(password, (err, pwDerivedKey) => {
|
|
if (err) return cb(err)
|
|
|
|
const isCorrect = keyStore.isDerivedKeyCorrect(pwDerivedKey)
|
|
if (!isCorrect) return cb(new Error('Lightwallet - password incorrect'))
|
|
|
|
this._keyStore = keyStore
|
|
this._createIdMgmt(pwDerivedKey)
|
|
cb()
|
|
})
|
|
}
|
|
|
|
IdentityStore.prototype._createVault = function (password, seedPhrase, cb) {
|
|
const opts = {
|
|
password,
|
|
hdPathString: this.hdPathString,
|
|
}
|
|
|
|
if (seedPhrase) {
|
|
opts.seedPhrase = seedPhrase
|
|
}
|
|
|
|
KeyStore.createVault(opts, (err, keyStore) => {
|
|
if (err) return cb(err)
|
|
|
|
this._keyStore = keyStore
|
|
|
|
keyStore.keyFromPassword(password, (err, derivedKey) => {
|
|
if (err) return cb(err)
|
|
|
|
this.purgeCache()
|
|
|
|
keyStore.addHdDerivationPath(this.hdPathString, derivedKey, {curve: 'secp256k1', purpose: 'sign'})
|
|
|
|
this._createFirstWallet(derivedKey)
|
|
this._createIdMgmt(derivedKey)
|
|
this.setSelectedAddressSync()
|
|
|
|
cb()
|
|
})
|
|
})
|
|
}
|
|
|
|
IdentityStore.prototype._createIdMgmt = function (derivedKey) {
|
|
this._idmgmt = new IdManagement({
|
|
keyStore: this._keyStore,
|
|
derivedKey: derivedKey,
|
|
configManager: this.configManager,
|
|
})
|
|
}
|
|
|
|
IdentityStore.prototype.purgeCache = function () {
|
|
this._currentState.identities = {}
|
|
let accounts
|
|
try {
|
|
accounts = Object.keys(this._ethStore._currentState.accounts)
|
|
} catch (e) {
|
|
accounts = []
|
|
}
|
|
accounts.forEach((address) => {
|
|
this._ethStore.removeAccount(address)
|
|
})
|
|
}
|
|
|
|
IdentityStore.prototype._createFirstWallet = function (derivedKey) {
|
|
const keyStore = this._keyStore
|
|
keyStore.setDefaultHdDerivationPath(this.hdPathString)
|
|
keyStore.generateNewAddress(derivedKey, 1)
|
|
this.configManager.setWallet(keyStore.serialize())
|
|
var addresses = keyStore.getAddresses()
|
|
this._ethStore.addAccount(ethUtil.addHexPrefix(addresses[0]))
|
|
}
|
|
|
|
// get addresses and normalize address hexString
|
|
IdentityStore.prototype._getAddresses = function () {
|
|
return this._keyStore.getAddresses(this.hdPathString).map((address) => {
|
|
return ethUtil.addHexPrefix(address)
|
|
})
|
|
}
|
|
|
|
IdentityStore.prototype._autoFaucet = function () {
|
|
var addresses = this._getAddresses()
|
|
autoFaucet(addresses[0])
|
|
}
|
|
|
|
// util
|
|
|
|
function noop () {}
|