diff options
Diffstat (limited to 'app/scripts/keyring-controller.js')
-rw-r--r-- | app/scripts/keyring-controller.js | 347 |
1 files changed, 329 insertions, 18 deletions
diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js index d25dddba1..28c920d22 100644 --- a/app/scripts/keyring-controller.js +++ b/app/scripts/keyring-controller.js @@ -1,22 +1,44 @@ +const async = require('async') const EventEmitter = require('events').EventEmitter const encryptor = require('./lib/encryptor') const messageManager = require('./lib/message-manager') +const ethUtil = require('ethereumjs-util') +const ethBinToOps = require('eth-bin-to-ops') +const EthQuery = require('eth-query') +const BN = ethUtil.BN +const Transaction = require('ethereumjs-tx') +const createId = require('web3-provider-engine/util/random-id') +// Keyrings: +const SimpleKeyring = require('./keyrings/simple') +const keyringTypes = [ + SimpleKeyring, +] module.exports = class KeyringController extends EventEmitter { constructor (opts) { super() + this.web3 = opts.web3 this.configManager = opts.configManager this.ethStore = opts.ethStore - this.keyChains = [] + this.encryptor = encryptor + this.keyringTypes = keyringTypes + + this.keyrings = [] + this.identities = {} // Essentially a name hash + + this._unconfTxCbs = {} + this._unconfMsgCbs = {} + + this.network = null } getState() { return { isInitialized: !!this.configManager.getVault(), isUnlocked: !!this.key, - isConfirmed: true, // this.configManager.getConfirmed(), + isConfirmed: true, // AUDIT this.configManager.getConfirmed(), isEthConfirmed: this.configManager.getShouldntShowWarning(), unconfTxs: this.configManager.unconfirmedTxs(), transactions: this.configManager.getTxList(), @@ -27,6 +49,9 @@ module.exports = class KeyringController extends EventEmitter { currentFiat: this.configManager.getCurrentFiat(), conversionRate: this.configManager.getConversionRate(), conversionDate: this.configManager.getConversionDate(), + keyringTypes: this.keyringTypes.map((krt) => krt.type()), + identities: this.identities, + network: this.network, } } @@ -35,11 +60,11 @@ module.exports = class KeyringController extends EventEmitter { } createNewVault(password, entropy, cb) { - const salt = generateSalt() + const salt = this.encryptor.generateSalt() this.configManager.setSalt(salt) this.loadKey(password) .then((key) => { - return encryptor.encryptWithKey(key, {}) + return this.encryptor.encryptWithKey(key, []) }) .then((encryptedString) => { this.configManager.setVault(encryptedString) @@ -53,6 +78,9 @@ module.exports = class KeyringController extends EventEmitter { submitPassword(password, cb) { this.loadKey(password) .then((key) => { + return this.unlockKeyrings(key) + }) + .then(() => { cb(null, this.getState()) }) .catch((err) => { @@ -61,31 +89,295 @@ module.exports = class KeyringController extends EventEmitter { } loadKey(password) { - const salt = this.configManager.getSalt() - return encryptor.keyFromPassword(password + salt) + const salt = this.configManager.getSalt() || this.encryptor.generateSalt() + return this.encryptor.keyFromPassword(password + salt) .then((key) => { this.key = key return key }) } + 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.keyrings.push(keyring) + this.persistAllKeyrings() + .then(() => { + cb(this.getState()) + }) + .catch((reason) => { + cb(reason) + }) + } + + // Takes an account address and an iterator representing + // the current number of named accounts. + loadBalanceAndNickname(account, i) { + const address = ethUtil.addHexPrefix(account) + this.ethStore.addAccount(address) + const oldNickname = this.configManager.nicknameForWallet(address) + const name = oldNickname || `Account ${++i}` + this.identities[address] = { + address, + name, + } + this.saveAccountLabel(address, name) + } + + saveAccountLabel (account, label, cb) { + const address = ethUtil.addHexPrefix(account) + const configManager = this.configManager + configManager.setNicknameForWallet(address, label) + if (cb) { + cb(null, label) + } + } + + persistAllKeyrings() { + const serialized = this.keyrings.map((k) => { + return { + type: k.type, + // keyring.serialize() must return a JSON-encodable object. + data: k.serialize(), + } + }) + return this.encryptor.encryptWithKey(this.key, serialized) + .then((encryptedString) => { + this.configManager.setVault(encryptedString) + return true + }) + .catch((reason) => { + console.error('Failed to persist keyrings.', reason) + }) + } + + unlockKeyrings(key) { + const encryptedVault = this.configManager.getVault() + return this.encryptor.decryptWithKey(key, encryptedVault) + .then((vault) => { + this.keyrings = vault.map(this.restoreKeyring.bind(this, 0)) + return this.keyrings + }) + } + + restoreKeyring(i, serialized) { + const { type, data } = serialized + const Keyring = this.getKeyringClassForType(type) + const keyring = new Keyring() + keyring.deserialize(data) + + keyring.getAccounts().forEach((account) => { + this.loadBalanceAndNickname(account, i) + }) + + return keyring + } + + getKeyringClassForType(type) { + const Keyring = this.keyringTypes.reduce((res, kr) => { + if (kr.type() === type) { + return kr + } else { + return res + } + }) + return Keyring + } + + getAccounts() { + const keyrings = this.keyrings || [] + return keyrings.map(kr => kr.getAccounts()) + .reduce((res, arr) => { + return res.concat(arr) + }, []) + } + setSelectedAddress(address, cb) { - this.selectedAddress = address + this.configManager.setSelectedAccount(address) cb(null, address) } + addUnconfirmedTransaction(txParams, onTxDoneCb, cb) { + var self = this + const configManager = this.configManager + + // create txData obj with parameters and meta data + var time = (new Date()).getTime() + var txId = createId() + txParams.metamaskId = txId + txParams.metamaskNetworkId = this.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. + this._unconfTxCbs[txId] = onTxDoneCb + + var provider = this.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, function (err, result) { + if (err) return cb(err) + var code = ethUtil.toBuffer(result) + if (code !== '0x') { + var ops = ethBinToOps(code) + var containsDelegateCall = ops.some((op) => op.name === 'DELEGATECALL') + txData.containsDelegateCall = containsDelegateCall + cb() + } else { + cb() + } + }) + } else { + cb() + } + } + + function estimateGas(cb){ + query.estimateGas(txParams, function(err, result){ + if (err) return cb(err) + txData.estimatedGas = self.addGasBuffer(result) + cb() + }) + } + + function didComplete (err) { + if (err) return cb(err) + configManager.addTx(txData) + // signal update + self.emit('update') + // signal completion of add tx + cb(null, txData) + } + } + + addUnconfirmedMessage(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.emit('update') + return msgId + } + approveTransaction(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.emit('update') } cancelTransaction(txId, cb) { + const configManager = this.configManager + var approvalCb = this._unconfTxCbs[txId] || noop + + // reject tx + approvalCb(null, false) + // clean up + configManager.rejectTx(txId) + delete this._unconfTxCbs[txId] + if (cb && typeof cb === 'function') { cb() } } + signTransaction(txParams, cb) { + try { + const address = ethUtil.addHexPrefix(txParams.from.toLowercase()) + const keyring = this.getKeyringForAccount(address) + + // Handle gas pricing + var gasMultiplier = this.configManager.getGasMultiplier() || 1 + var gasPrice = new BN(ethUtil.stripHexPrefix(txParams.gasPrice), 16) + gasPrice = gasPrice.mul(new BN(gasMultiplier * 100, 10)).div(new BN(100, 10)) + txParams.gasPrice = ethUtil.intToHex(gasPrice.toNumber()) + + // normalize values + txParams.to = ethUtil.addHexPrefix(txParams.to.toLowerCase()) + txParams.from = ethUtil.addHexPrefix(txParams.from.toLowerCase()) + txParams.value = ethUtil.addHexPrefix(txParams.value) + txParams.data = ethUtil.addHexPrefix(txParams.data) + txParams.gasLimit = ethUtil.addHexPrefix(txParams.gasLimit || txParams.gas) + txParams.nonce = ethUtil.addHexPrefix(txParams.nonce) + + let tx = new Transaction(txParams) + tx = keyring.signTransaction(address, tx) + + // Add the tx hash to the persisted meta-tx object + var txHash = ethUtil.bufferToHex(tx.hash()) + var metaTx = this.configManager.getTx(txParams.metamaskId) + metaTx.hash = txHash + this.configManager.updateTx(metaTx) + + // return raw serialized tx + var rawTx = ethUtil.bufferToHex(tx.serialize()) + cb(null, rawTx) + } catch (e) { + cb(e) + } + } + signMessage(msgParams, cb) { - cb() + try { + const keyring = this.getKeyringForAccount(msgParams.from) + const address = ethUtil.addHexPrefix(msgParams.from.toLowercase()) + const rawSig = keyring.signMessage(address, msgParams.data) + cb(null, rawSig) + } catch (e) { + cb(e) + } + } + + getKeyringForAccount(address) { + const hexed = ethUtil.addHexPrefix(address.toLowerCase()) + return this.keyrings.find((ring) => { + return ring.getAccounts() + .map(acct => ethUtil.addHexPrefix(acct.toLowerCase())) + .includes(hexed) + }) } cancelMessage(msgId, cb) { @@ -95,6 +387,8 @@ module.exports = class KeyringController extends EventEmitter { } setLocked(cb) { + this.key = null + this.keyrings = [] cb() } @@ -102,19 +396,36 @@ module.exports = class KeyringController extends EventEmitter { cb(null, '0xPrivateKey') } - saveAccountLabel(account, label, cb) { - cb(/* null, label */) - } - tryPassword(password, cb) { cb() } -} + getNetwork(err) { + if (err) { + this.network = 'loading' + this.emit('update') + } + + this.web3.version.getNetwork((err, network) => { + if (err) { + this.network = 'loading' + return this.emit('update') + } + if (global.METAMASK_DEBUG) { + console.log('web3.getNetwork returned ' + network) + } + this.network = network + this.emit('update') + }) + } + + addGasBuffer(gasHex) { + var gas = new BN(gasHex, 16) + var buffer = new BN('100000', 10) + var result = gas.add(buffer) + return ethUtil.addHexPrefix(result.toString(16)) + } -function generateSalt (byteCount) { - var view = new Uint8Array(32) - global.crypto.getRandomValues(view) - var b64encoded = btoa(String.fromCharCode.apply(null, view)) - return b64encoded } + +function noop () {} |