diff options
Diffstat (limited to 'app/scripts/lib')
-rw-r--r-- | app/scripts/lib/account-tracker.js | 143 | ||||
-rw-r--r-- | app/scripts/lib/auto-reload.js | 6 | ||||
-rw-r--r-- | app/scripts/lib/config-manager.js | 254 | ||||
-rw-r--r-- | app/scripts/lib/createErrorMiddleware.js | 67 | ||||
-rw-r--r-- | app/scripts/lib/events-proxy.js | 42 | ||||
-rw-r--r-- | app/scripts/lib/inpage-provider.js | 125 | ||||
-rw-r--r-- | app/scripts/lib/ipfsContent.js | 4 | ||||
-rw-r--r-- | app/scripts/lib/message-manager.js | 33 | ||||
-rw-r--r-- | app/scripts/lib/personal-message-manager.js | 37 | ||||
-rw-r--r-- | app/scripts/lib/port-stream.js | 80 | ||||
-rw-r--r-- | app/scripts/lib/setupRaven.js | 4 | ||||
-rw-r--r-- | app/scripts/lib/typed-message-manager.js | 99 | ||||
-rw-r--r-- | app/scripts/lib/util.js | 14 |
13 files changed, 238 insertions, 670 deletions
diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js index 0f7b3d865..2e9340018 100644 --- a/app/scripts/lib/account-tracker.js +++ b/app/scripts/lib/account-tracker.js @@ -7,14 +7,13 @@ * on each new block. */ -const async = require('async') const EthQuery = require('eth-query') const ObservableStore = require('obs-store') -const EventEmitter = require('events').EventEmitter -function noop () {} +const log = require('loglevel') +const pify = require('pify') -class AccountTracker extends EventEmitter { +class AccountTracker { /** * This module is responsible for tracking any number of accounts and caching their current balances & transaction @@ -35,8 +34,6 @@ class AccountTracker extends EventEmitter { * */ constructor (opts = {}) { - super() - const initState = { accounts: {}, currentBlockGasLimit: '', @@ -44,12 +41,29 @@ class AccountTracker extends EventEmitter { this.store = new ObservableStore(initState) this._provider = opts.provider - this._query = new EthQuery(this._provider) + this._query = pify(new EthQuery(this._provider)) this._blockTracker = opts.blockTracker - // subscribe to latest block - this._blockTracker.on('block', this._updateForBlock.bind(this)) // blockTracker.currentBlock may be null - this._currentBlockNumber = this._blockTracker.currentBlock + this._currentBlockNumber = this._blockTracker.getCurrentBlock() + this._blockTracker.once('latest', blockNumber => { + this._currentBlockNumber = blockNumber + }) + // bind function for easier listener syntax + this._updateForBlock = this._updateForBlock.bind(this) + } + + start () { + // remove first to avoid double add + this._blockTracker.removeListener('latest', this._updateForBlock) + // add listener + this._blockTracker.addListener('latest', this._updateForBlock) + // fetch account balances + this._updateAccounts() + } + + stop () { + // remove listener + this._blockTracker.removeListener('latest', this._updateForBlock) } /** @@ -67,49 +81,57 @@ class AccountTracker extends EventEmitter { const accounts = this.store.getState().accounts const locals = Object.keys(accounts) - const toAdd = [] + const accountsToAdd = [] addresses.forEach((upstream) => { if (!locals.includes(upstream)) { - toAdd.push(upstream) + accountsToAdd.push(upstream) } }) - const toRemove = [] + const accountsToRemove = [] locals.forEach((local) => { if (!addresses.includes(local)) { - toRemove.push(local) + accountsToRemove.push(local) } }) - toAdd.forEach(upstream => this.addAccount(upstream)) - toRemove.forEach(local => this.removeAccount(local)) - this._updateAccounts() + this.addAccounts(accountsToAdd) + this.removeAccount(accountsToRemove) } /** - * Adds a new address to this AccountTracker's accounts object, which points to an empty object. This object will be + * Adds new addresses to track the balances of * given a balance as long this._currentBlockNumber is defined. * - * @param {string} address A hex address of a new account to store in this AccountTracker's accounts object + * @param {array} addresses An array of hex addresses of new accounts to track * */ - addAccount (address) { + addAccounts (addresses) { const accounts = this.store.getState().accounts - accounts[address] = {} + // add initial state for addresses + addresses.forEach(address => { + accounts[address] = {} + }) + // save accounts state this.store.updateState({ accounts }) + // fetch balances for the accounts if there is block number ready if (!this._currentBlockNumber) return - this._updateAccount(address) + addresses.forEach(address => this._updateAccount(address)) } /** - * Removes an account from this AccountTracker's accounts object + * Removes accounts from being tracked * - * @param {string} address A hex address of a the account to remove + * @param {array} an array of hex addresses to stop tracking * */ - removeAccount (address) { + removeAccount (addresses) { const accounts = this.store.getState().accounts - delete accounts[address] + // remove each state object + addresses.forEach(address => { + delete accounts[address] + }) + // save accounts state this.store.updateState({ accounts }) } @@ -118,71 +140,56 @@ class AccountTracker extends EventEmitter { * via EthQuery * * @private - * @param {object} block Data about the block that contains the data to update to. + * @param {number} blockNumber the block number to update to. * @fires 'block' The updated state, if all account updates are successful * */ - _updateForBlock (block) { - this._currentBlockNumber = block.number - const currentBlockGasLimit = block.gasLimit + async _updateForBlock (blockNumber) { + this._currentBlockNumber = blockNumber + // block gasLimit polling shouldn't be in account-tracker shouldn't be here... + const currentBlock = await this._query.getBlockByNumber(blockNumber, false) + if (!currentBlock) return + const currentBlockGasLimit = currentBlock.gasLimit this.store.updateState({ currentBlockGasLimit }) - async.parallel([ - this._updateAccounts.bind(this), - ], (err) => { - if (err) return console.error(err) - this.emit('block', this.store.getState()) - }) + try { + await this._updateAccounts() + } catch (err) { + log.error(err) + } } /** * Calls this._updateAccount for each account in this.store * - * @param {Function} cb A callback to pass to this._updateAccount, called after each account is successfully updated + * @returns {Promise} after all account balances updated * */ - _updateAccounts (cb = noop) { + async _updateAccounts () { const accounts = this.store.getState().accounts const addresses = Object.keys(accounts) - async.each(addresses, this._updateAccount.bind(this), cb) + await Promise.all(addresses.map(this._updateAccount.bind(this))) } /** - * Updates the current balance of an account. Gets an updated balance via this._getAccount. + * Updates the current balance of an account. * * @private * @param {string} address A hex address of a the account to be updated - * @param {Function} cb A callback to call once the account at address is successfully update + * @returns {Promise} after the account balance is updated * */ - _updateAccount (address, cb = noop) { - this._getAccount(address, (err, result) => { - if (err) return cb(err) - result.address = address - const accounts = this.store.getState().accounts - // only populate if the entry is still present - if (accounts[address]) { - accounts[address] = result - this.store.updateState({ accounts }) - } - cb(null, result) - }) - } - - /** - * Gets the current balance of an account via EthQuery. - * - * @private - * @param {string} address A hex address of a the account to query - * @param {Function} cb A callback to call once the account at address is successfully update - * - */ - _getAccount (address, cb = noop) { - const query = this._query - async.parallel({ - balance: query.getBalance.bind(query, address), - }, cb) + async _updateAccount (address) { + // query balance + const balance = await this._query.getBalance(address) + const result = { address, balance } + // update accounts state + const { accounts } = this.store.getState() + // only populate if the entry is still present + if (!accounts[address]) return + accounts[address] = result + this.store.updateState({ accounts }) } } diff --git a/app/scripts/lib/auto-reload.js b/app/scripts/lib/auto-reload.js index cce31c3d2..558391a06 100644 --- a/app/scripts/lib/auto-reload.js +++ b/app/scripts/lib/auto-reload.js @@ -2,18 +2,12 @@ module.exports = setupDappAutoReload function setupDappAutoReload (web3, observable) { // export web3 as a global, checking for usage - let hasBeenWarned = false let reloadInProgress = false let lastTimeUsed let lastSeenNetwork global.web3 = new Proxy(web3, { get: (_web3, key) => { - // show warning once on web3 access - if (!hasBeenWarned && key !== 'currentProvider') { - console.warn('MetaMask: web3 will be deprecated in the near future in favor of the ethereumProvider \nhttps://github.com/MetaMask/faq/blob/master/detecting_metamask.md#web3-deprecation') - hasBeenWarned = true - } // get the time of use lastTimeUsed = Date.now() // return value normally diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js deleted file mode 100644 index 221746467..000000000 --- a/app/scripts/lib/config-manager.js +++ /dev/null @@ -1,254 +0,0 @@ -const ethUtil = require('ethereumjs-util') -const normalize = require('eth-sig-util').normalize -const { - MAINNET_RPC_URL, - ROPSTEN_RPC_URL, - KOVAN_RPC_URL, - RINKEBY_RPC_URL, -} = require('../controllers/network/enums') - -/* The config-manager is a convenience object - * wrapping a pojo-migrator. - * - * It exists mostly to allow the creation of - * convenience methods to access and persist - * particular portions of the state. - */ -module.exports = ConfigManager -function ConfigManager (opts) { - // ConfigManager is observable and will emit updates - this._subs = [] - this.store = opts.store -} - -ConfigManager.prototype.setConfig = function (config) { - var data = this.getData() - data.config = config - this.setData(data) - this._emitUpdates(config) -} - -ConfigManager.prototype.getConfig = function () { - var data = this.getData() - return data.config -} - -ConfigManager.prototype.setData = function (data) { - this.store.putState(data) -} - -ConfigManager.prototype.getData = function () { - return this.store.getState() -} - -ConfigManager.prototype.setPasswordForgotten = function (passwordForgottenState) { - const data = this.getData() - data.forgottenPassword = passwordForgottenState - this.setData(data) -} - -ConfigManager.prototype.getPasswordForgotten = function (passwordForgottenState) { - const data = this.getData() - return data.forgottenPassword -} - -ConfigManager.prototype.setWallet = function (wallet) { - var data = this.getData() - data.wallet = wallet - this.setData(data) -} - -ConfigManager.prototype.setVault = function (encryptedString) { - var data = this.getData() - data.vault = encryptedString - this.setData(data) -} - -ConfigManager.prototype.getVault = function () { - var data = this.getData() - return data.vault -} - -ConfigManager.prototype.getKeychains = function () { - return this.getData().keychains || [] -} - -ConfigManager.prototype.setKeychains = function (keychains) { - var data = this.getData() - data.keychains = keychains - this.setData(data) -} - -ConfigManager.prototype.getSelectedAccount = function () { - var config = this.getConfig() - return config.selectedAccount -} - -ConfigManager.prototype.setSelectedAccount = function (address) { - var config = this.getConfig() - config.selectedAccount = ethUtil.addHexPrefix(address) - this.setConfig(config) -} - -ConfigManager.prototype.getWallet = function () { - return this.getData().wallet -} - -// Takes a boolean -ConfigManager.prototype.setShowSeedWords = function (should) { - var data = this.getData() - data.showSeedWords = should - this.setData(data) -} - - -ConfigManager.prototype.getShouldShowSeedWords = function () { - var data = this.getData() - return data.showSeedWords -} - -ConfigManager.prototype.setSeedWords = function (words) { - var data = this.getData() - data.seedWords = words - this.setData(data) -} - -ConfigManager.prototype.getSeedWords = function () { - var data = this.getData() - return data.seedWords -} -ConfigManager.prototype.setRpcTarget = function (rpcUrl) { - var config = this.getConfig() - config.provider = { - type: 'rpc', - rpcTarget: rpcUrl, - } - this.setConfig(config) -} - -ConfigManager.prototype.setProviderType = function (type) { - var config = this.getConfig() - config.provider = { - type: type, - } - this.setConfig(config) -} - -ConfigManager.prototype.useEtherscanProvider = function () { - var config = this.getConfig() - config.provider = { - type: 'etherscan', - } - this.setConfig(config) -} - -ConfigManager.prototype.getProvider = function () { - var config = this.getConfig() - return config.provider -} - -ConfigManager.prototype.getCurrentRpcAddress = function () { - var provider = this.getProvider() - if (!provider) return null - switch (provider.type) { - - case 'mainnet': - return MAINNET_RPC_URL - - case 'ropsten': - return ROPSTEN_RPC_URL - - case 'kovan': - return KOVAN_RPC_URL - - case 'rinkeby': - return RINKEBY_RPC_URL - - default: - return provider && provider.rpcTarget ? provider.rpcTarget : RINKEBY_RPC_URL - } -} - -// -// Tx -// - -ConfigManager.prototype.getTxList = function () { - var data = this.getData() - if (data.transactions !== undefined) { - return data.transactions - } else { - return [] - } -} - -ConfigManager.prototype.setTxList = function (txList) { - var data = this.getData() - data.transactions = txList - this.setData(data) -} - - -// wallet nickname methods - -ConfigManager.prototype.getWalletNicknames = function () { - var data = this.getData() - const nicknames = ('walletNicknames' in data) ? data.walletNicknames : {} - return nicknames -} - -ConfigManager.prototype.nicknameForWallet = function (account) { - const address = normalize(account) - const nicknames = this.getWalletNicknames() - return nicknames[address] -} - -ConfigManager.prototype.setNicknameForWallet = function (account, nickname) { - const address = normalize(account) - const nicknames = this.getWalletNicknames() - nicknames[address] = nickname - var data = this.getData() - data.walletNicknames = nicknames - this.setData(data) -} - -// observable - -ConfigManager.prototype.getSalt = function () { - var data = this.getData() - return data.salt -} - -ConfigManager.prototype.setSalt = function (salt) { - var data = this.getData() - data.salt = salt - this.setData(data) -} - -ConfigManager.prototype.subscribe = function (fn) { - this._subs.push(fn) - var unsubscribe = this.unsubscribe.bind(this, fn) - return unsubscribe -} - -ConfigManager.prototype.unsubscribe = function (fn) { - var index = this._subs.indexOf(fn) - if (index !== -1) this._subs.splice(index, 1) -} - -ConfigManager.prototype._emitUpdates = function (state) { - this._subs.forEach(function (handler) { - handler(state) - }) -} - -ConfigManager.prototype.setLostAccounts = function (lostAccounts) { - var data = this.getData() - data.lostAccounts = lostAccounts - this.setData(data) -} - -ConfigManager.prototype.getLostAccounts = function () { - var data = this.getData() - return data.lostAccounts || [] -} diff --git a/app/scripts/lib/createErrorMiddleware.js b/app/scripts/lib/createErrorMiddleware.js deleted file mode 100644 index 7f6a4bd73..000000000 --- a/app/scripts/lib/createErrorMiddleware.js +++ /dev/null @@ -1,67 +0,0 @@ -const log = require('loglevel') - -/** - * JSON-RPC error object - * - * @typedef {Object} RpcError - * @property {number} code - Indicates the error type that occurred - * @property {Object} [data] - Contains additional information about the error - * @property {string} [message] - Short description of the error - */ - -/** - * Middleware configuration object - * - * @typedef {Object} MiddlewareConfig - * @property {boolean} [override] - Use RPC_ERRORS message in place of provider message - */ - -/** - * Map of standard and non-standard RPC error codes to messages - */ -const RPC_ERRORS = { - 1: 'An unauthorized action was attempted.', - 2: 'A disallowed action was attempted.', - 3: 'An execution error occurred.', - [-32600]: 'The JSON sent is not a valid Request object.', - [-32601]: 'The method does not exist / is not available.', - [-32602]: 'Invalid method parameter(s).', - [-32603]: 'Internal JSON-RPC error.', - [-32700]: 'Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.', - internal: 'Internal server error.', - unknown: 'Unknown JSON-RPC error.', -} - -/** - * Modifies a JSON-RPC error object in-place to add a human-readable message, - * optionally overriding any provider-supplied message - * - * @param {RpcError} error - JSON-RPC error object - * @param {boolean} override - Use RPC_ERRORS message in place of provider message - */ -function sanitizeRPCError (error, override) { - if (error.message && !override) { return error } - const message = error.code > -31099 && error.code < -32100 ? RPC_ERRORS.internal : RPC_ERRORS[error.code] - error.message = message || RPC_ERRORS.unknown -} - -/** - * json-rpc-engine middleware that both logs standard and non-standard error - * messages and ends middleware stack traversal if an error is encountered - * - * @param {MiddlewareConfig} [config={override:true}] - Middleware configuration - * @returns {Function} json-rpc-engine middleware function - */ -function createErrorMiddleware ({ override = true } = {}) { - return (req, res, next) => { - next(done => { - const { error } = res - if (!error) { return done() } - sanitizeRPCError(error) - log.error(`MetaMask - RPC Error: ${error.message}`, error) - done() - }) - } -} - -module.exports = createErrorMiddleware diff --git a/app/scripts/lib/events-proxy.js b/app/scripts/lib/events-proxy.js deleted file mode 100644 index f83773ccc..000000000 --- a/app/scripts/lib/events-proxy.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Returns an EventEmitter that proxies events from the given event emitter - * @param {any} eventEmitter - * @param {object} listeners - The listeners to proxy to - * @returns {any} - */ -module.exports = function createEventEmitterProxy (eventEmitter, listeners) { - let target = eventEmitter - const eventHandlers = listeners || {} - const proxy = /** @type {any} */ (new Proxy({}, { - get: (_, name) => { - // intercept listeners - if (name === 'on') return addListener - if (name === 'setTarget') return setTarget - if (name === 'proxyEventHandlers') return eventHandlers - return (/** @type {any} */ (target))[name] - }, - set: (_, name, value) => { - target[name] = value - return true - }, - })) - function setTarget (/** @type {EventEmitter} */ eventEmitter) { - target = eventEmitter - // migrate listeners - Object.keys(eventHandlers).forEach((name) => { - /** @type {Array<Function>} */ (eventHandlers[name]).forEach((handler) => target.on(name, handler)) - }) - } - /** - * Attaches a function to be called whenever the specified event is emitted - * @param {string} name - * @param {Function} handler - */ - function addListener (name, handler) { - if (!eventHandlers[name]) eventHandlers[name] = [] - eventHandlers[name].push(handler) - target.on(name, handler) - } - if (listeners) proxy.setTarget(eventEmitter) - return proxy -} diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js deleted file mode 100644 index 6ef511453..000000000 --- a/app/scripts/lib/inpage-provider.js +++ /dev/null @@ -1,125 +0,0 @@ -const pump = require('pump') -const RpcEngine = require('json-rpc-engine') -const createErrorMiddleware = require('./createErrorMiddleware') -const createIdRemapMiddleware = require('json-rpc-engine/src/idRemapMiddleware') -const createStreamMiddleware = require('json-rpc-middleware-stream') -const LocalStorageStore = require('obs-store') -const asStream = require('obs-store/lib/asStream') -const ObjectMultiplex = require('obj-multiplex') - -module.exports = MetamaskInpageProvider - -function MetamaskInpageProvider (connectionStream) { - const self = this - - // setup connectionStream multiplexing - const mux = self.mux = new ObjectMultiplex() - pump( - connectionStream, - mux, - connectionStream, - (err) => logStreamDisconnectWarning('MetaMask', err) - ) - - // subscribe to metamask public config (one-way) - self.publicConfigStore = new LocalStorageStore({ storageKey: 'MetaMask-Config' }) - - pump( - mux.createStream('publicConfig'), - asStream(self.publicConfigStore), - (err) => logStreamDisconnectWarning('MetaMask PublicConfigStore', err) - ) - - // ignore phishing warning message (handled elsewhere) - mux.ignoreStream('phishing') - - // connect to async provider - const streamMiddleware = createStreamMiddleware() - pump( - streamMiddleware.stream, - mux.createStream('provider'), - streamMiddleware.stream, - (err) => logStreamDisconnectWarning('MetaMask RpcProvider', err) - ) - - // handle sendAsync requests via dapp-side rpc engine - const rpcEngine = new RpcEngine() - rpcEngine.push(createIdRemapMiddleware()) - rpcEngine.push(createErrorMiddleware()) - rpcEngine.push(streamMiddleware) - self.rpcEngine = rpcEngine -} - -// handle sendAsync requests via asyncProvider -// also remap ids inbound and outbound -MetamaskInpageProvider.prototype.sendAsync = function (payload, cb) { - const self = this - - if (payload.method === 'eth_signTypedData') { - console.warn('MetaMask: This experimental version of eth_signTypedData will be deprecated in the next release in favor of the standard as defined in EIP-712. See https://git.io/fNzPl for more information on the new standard.') - } - - self.rpcEngine.handle(payload, cb) -} - - -MetamaskInpageProvider.prototype.send = function (payload) { - const self = this - - let selectedAddress - let result = null - switch (payload.method) { - - case 'eth_accounts': - // read from localStorage - selectedAddress = self.publicConfigStore.getState().selectedAddress - result = selectedAddress ? [selectedAddress] : [] - break - - case 'eth_coinbase': - // read from localStorage - selectedAddress = self.publicConfigStore.getState().selectedAddress - result = selectedAddress || null - break - - case 'eth_uninstallFilter': - self.sendAsync(payload, noop) - result = true - break - - case 'net_version': - const networkVersion = self.publicConfigStore.getState().networkVersion - result = networkVersion || null - break - - // throw not-supported Error - default: - var link = 'https://github.com/MetaMask/faq/blob/master/DEVELOPERS.md#dizzy-all-async---think-of-metamask-as-a-light-client' - var message = `The MetaMask Web3 object does not support synchronous methods like ${payload.method} without a callback parameter. See ${link} for details.` - throw new Error(message) - - } - - // return the result - return { - id: payload.id, - jsonrpc: payload.jsonrpc, - result: result, - } -} - -MetamaskInpageProvider.prototype.isConnected = function () { - return true -} - -MetamaskInpageProvider.prototype.isMetaMask = true - -// util - -function logStreamDisconnectWarning (remoteLabel, err) { - let warningMsg = `MetamaskInpageProvider - lost connection to ${remoteLabel}` - if (err) warningMsg += '\n' + err.stack - console.warn(warningMsg) -} - -function noop () {} diff --git a/app/scripts/lib/ipfsContent.js b/app/scripts/lib/ipfsContent.js index 5db63f47d..8b08453c4 100644 --- a/app/scripts/lib/ipfsContent.js +++ b/app/scripts/lib/ipfsContent.js @@ -5,6 +5,8 @@ module.exports = function (provider) { function ipfsContent (details) { const name = details.url.substring(7, details.url.length - 1) let clearTime = null + if (/^.+\.eth$/.test(name) === false) return + extension.tabs.query({active: true}, tab => { extension.tabs.update(tab.id, { url: 'loading.html' }) @@ -34,7 +36,7 @@ module.exports = function (provider) { return { cancel: true } } - extension.webRequest.onErrorOccurred.addListener(ipfsContent, {urls: ['*://*.eth/', '*://*.test/']}) + extension.webRequest.onErrorOccurred.addListener(ipfsContent, {urls: ['*://*.eth/'], types: ['main_frame']}) return { remove () { diff --git a/app/scripts/lib/message-manager.js b/app/scripts/lib/message-manager.js index 901367f04..e86629590 100644 --- a/app/scripts/lib/message-manager.js +++ b/app/scripts/lib/message-manager.js @@ -69,10 +69,39 @@ module.exports = class MessageManager extends EventEmitter { * new Message to this.messages, and to save the unapproved Messages from that list to this.memStore. * * @param {Object} msgParams The params for the eth_sign call to be made after the message is approved. + * @param {Object} req (optional) The original request object possibly containing the origin + * @returns {promise} after signature has been + * + */ + addUnapprovedMessageAsync (msgParams, req) { + return new Promise((resolve, reject) => { + const msgId = this.addUnapprovedMessage(msgParams, req) + // await finished + this.once(`${msgId}:finished`, (data) => { + switch (data.status) { + case 'signed': + return resolve(data.rawSig) + case 'rejected': + return reject(new Error('MetaMask Message Signature: User denied message signature.')) + default: + return reject(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) + } + }) + }) + } + + /** + * Creates a new Message with an 'unapproved' status using the passed msgParams. this.addMsg is called to add the + * new Message to this.messages, and to save the unapproved Messages from that list to this.memStore. + * + * @param {Object} msgParams The params for the eth_sign call to be made after the message is approved. + * @param {Object} req (optional) The original request object where the origin may be specificied * @returns {number} The id of the newly created message. * */ - addUnapprovedMessage (msgParams) { + addUnapprovedMessage (msgParams, req) { + // add origin from request + if (req) msgParams.origin = req.origin msgParams.data = normalizeMsgData(msgParams.data) // create txData obj with parameters and meta data var time = (new Date()).getTime() @@ -243,6 +272,6 @@ function normalizeMsgData (data) { return data } else { // data is unicode, convert to hex - return ethUtil.bufferToHex(new Buffer(data, 'utf8')) + return ethUtil.bufferToHex(Buffer.from(data, 'utf8')) } } diff --git a/app/scripts/lib/personal-message-manager.js b/app/scripts/lib/personal-message-manager.js index e96ced1f2..fdb94f5ec 100644 --- a/app/scripts/lib/personal-message-manager.js +++ b/app/scripts/lib/personal-message-manager.js @@ -73,11 +73,43 @@ module.exports = class PersonalMessageManager extends EventEmitter { * this.memStore. * * @param {Object} msgParams The params for the eth_sign call to be made after the message is approved. + * @param {Object} req (optional) The original request object possibly containing the origin + * @returns {promise} When the message has been signed or rejected + * + */ + addUnapprovedMessageAsync (msgParams, req) { + return new Promise((resolve, reject) => { + if (!msgParams.from) { + reject(new Error('MetaMask Message Signature: from field is required.')) + } + const msgId = this.addUnapprovedMessage(msgParams, req) + this.once(`${msgId}:finished`, (data) => { + switch (data.status) { + case 'signed': + return resolve(data.rawSig) + case 'rejected': + return reject(new Error('MetaMask Message Signature: User denied message signature.')) + default: + return reject(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) + } + }) + }) + } + + /** + * Creates a new PersonalMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add + * the new PersonalMessage to this.messages, and to save the unapproved PersonalMessages from that list to + * this.memStore. + * + * @param {Object} msgParams The params for the eth_sign call to be made after the message is approved. + * @param {Object} req (optional) The original request object possibly containing the origin * @returns {number} The id of the newly created PersonalMessage. * */ - addUnapprovedMessage (msgParams) { + addUnapprovedMessage (msgParams, req) { log.debug(`PersonalMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`) + // add origin from request + if (req) msgParams.origin = req.origin msgParams.data = this.normalizeMsgData(msgParams.data) // create txData obj with parameters and meta data var time = (new Date()).getTime() @@ -253,8 +285,7 @@ module.exports = class PersonalMessageManager extends EventEmitter { log.debug(`Message was not hex encoded, interpreting as utf8.`) } - return ethUtil.bufferToHex(new Buffer(data, 'utf8')) + return ethUtil.bufferToHex(Buffer.from(data, 'utf8')) } } - diff --git a/app/scripts/lib/port-stream.js b/app/scripts/lib/port-stream.js deleted file mode 100644 index fd65d94f3..000000000 --- a/app/scripts/lib/port-stream.js +++ /dev/null @@ -1,80 +0,0 @@ -const Duplex = require('readable-stream').Duplex -const inherits = require('util').inherits -const noop = function () {} - -module.exports = PortDuplexStream - -inherits(PortDuplexStream, Duplex) - -/** - * Creates a stream that's both readable and writable. - * The stream supports arbitrary objects. - * - * @class - * @param {Object} port Remote Port object - */ -function PortDuplexStream (port) { - Duplex.call(this, { - objectMode: true, - }) - this._port = port - port.onMessage.addListener(this._onMessage.bind(this)) - port.onDisconnect.addListener(this._onDisconnect.bind(this)) -} - -/** - * Callback triggered when a message is received from - * the remote Port associated with this Stream. - * - * @private - * @param {Object} msg - Payload from the onMessage listener of Port - */ -PortDuplexStream.prototype._onMessage = function (msg) { - if (Buffer.isBuffer(msg)) { - delete msg._isBuffer - var data = new Buffer(msg) - this.push(data) - } else { - this.push(msg) - } -} - -/** - * Callback triggered when the remote Port - * associated with this Stream disconnects. - * - * @private - */ -PortDuplexStream.prototype._onDisconnect = function () { - this.destroy() -} - -/** - * Explicitly sets read operations to a no-op - */ -PortDuplexStream.prototype._read = noop - - -/** - * Called internally when data should be written to - * this writable stream. - * - * @private - * @param {*} msg Arbitrary object to write - * @param {string} encoding Encoding to use when writing payload - * @param {Function} cb Called when writing is complete or an error occurs - */ -PortDuplexStream.prototype._write = function (msg, encoding, cb) { - try { - if (Buffer.isBuffer(msg)) { - var data = msg.toJSON() - data._isBuffer = true - this._port.postMessage(data) - } else { - this._port.postMessage(msg) - } - } catch (err) { - return cb(new Error('PortDuplexStream - disconnected')) - } - cb() -} diff --git a/app/scripts/lib/setupRaven.js b/app/scripts/lib/setupRaven.js index 3651524f1..e6e511640 100644 --- a/app/scripts/lib/setupRaven.js +++ b/app/scripts/lib/setupRaven.js @@ -70,11 +70,11 @@ function simplifyErrorMessages (report) { function rewriteErrorMessages (report, rewriteFn) { // rewrite top level message - if (report.message) report.message = rewriteFn(report.message) + if (typeof report.message === 'string') report.message = rewriteFn(report.message) // rewrite each exception message if (report.exception && report.exception.values) { report.exception.values.forEach(item => { - item.value = rewriteFn(item.value) + if (typeof item.value === 'string') item.value = rewriteFn(item.value) }) } } diff --git a/app/scripts/lib/typed-message-manager.js b/app/scripts/lib/typed-message-manager.js index c58921610..b10145f3b 100644 --- a/app/scripts/lib/typed-message-manager.js +++ b/app/scripts/lib/typed-message-manager.js @@ -4,6 +4,7 @@ const createId = require('./random-id') const assert = require('assert') const sigUtil = require('eth-sig-util') const log = require('loglevel') +const jsonschema = require('jsonschema') /** * Represents, and contains data about, an 'eth_signTypedData' type signature request. These are created when a @@ -17,7 +18,7 @@ const log = require('loglevel') * @property {Object} msgParams.from The address that is making the signature request. * @property {string} msgParams.data A hex string conversion of the raw buffer data of the signature request * @property {number} time The epoch time at which the this message was created - * @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed' or 'rejected' + * @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed', 'rejected', or 'errored' * @property {string} type The json-prc signing method for which a signature request has been made. A 'Message' will * always have a 'eth_signTypedData' type. * @@ -26,17 +27,10 @@ const log = require('loglevel') module.exports = class TypedMessageManager extends EventEmitter { /** * Controller in charge of managing - storing, adding, removing, updating - TypedMessage. - * - * @typedef {Object} TypedMessage - * @param {Object} opts @deprecated - * @property {Object} memStore The observable store where TypedMessage are saved. - * @property {Object} memStore.unapprovedTypedMessages A collection of all TypedMessages in the 'unapproved' state - * @property {number} memStore.unapprovedTypedMessagesCount The count of all TypedMessages in this.memStore.unapprobedMsgs - * @property {array} messages Holds all messages that have been created by this TypedMessage - * */ - constructor (opts) { + constructor ({ networkController }) { super() + this.networkController = networkController this.memStore = new ObservableStore({ unapprovedTypedMessages: {}, unapprovedTypedMessagesCount: 0, @@ -72,11 +66,43 @@ module.exports = class TypedMessageManager extends EventEmitter { * this.memStore. Before any of this is done, msgParams are validated * * @param {Object} msgParams The params for the eth_sign call to be made after the message is approved. + * @param {Object} req (optional) The original request object possibly containing the origin + * @returns {promise} When the message has been signed or rejected + * + */ + addUnapprovedMessageAsync (msgParams, req, version) { + return new Promise((resolve, reject) => { + const msgId = this.addUnapprovedMessage(msgParams, req, version) + this.once(`${msgId}:finished`, (data) => { + switch (data.status) { + case 'signed': + return resolve(data.rawSig) + case 'rejected': + return reject(new Error('MetaMask Message Signature: User denied message signature.')) + case 'errored': + return reject(new Error(`MetaMask Message Signature: ${data.error}`)) + default: + return reject(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) + } + }) + }) + } + + /** + * Creates a new TypedMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add + * the new TypedMessage to this.messages, and to save the unapproved TypedMessages from that list to + * this.memStore. Before any of this is done, msgParams are validated + * + * @param {Object} msgParams The params for the eth_sign call to be made after the message is approved. + * @param {Object} req (optional) The original request object possibly containing the origin * @returns {number} The id of the newly created TypedMessage. * */ - addUnapprovedMessage (msgParams) { + addUnapprovedMessage (msgParams, req, version) { + msgParams.version = version this.validateParams(msgParams) + // add origin from request + if (req) msgParams.origin = req.origin log.debug(`TypedMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`) // create txData obj with parameters and meta data @@ -103,14 +129,33 @@ module.exports = class TypedMessageManager extends EventEmitter { * */ validateParams (params) { - assert.equal(typeof params, 'object', 'Params should ben an object.') - assert.ok('data' in params, 'Params must include a data field.') - assert.ok('from' in params, 'Params must include a from field.') - assert.ok(Array.isArray(params.data), 'Data should be an array.') - assert.equal(typeof params.from, 'string', 'From field must be a string.') - assert.doesNotThrow(() => { - sigUtil.typedSignatureHash(params.data) - }, 'Expected EIP712 typed data') + switch (params.version) { + case 'V1': + assert.equal(typeof params, 'object', 'Params should ben an object.') + assert.ok('data' in params, 'Params must include a data field.') + assert.ok('from' in params, 'Params must include a from field.') + assert.ok(Array.isArray(params.data), 'Data should be an array.') + assert.equal(typeof params.from, 'string', 'From field must be a string.') + assert.doesNotThrow(() => { + sigUtil.typedSignatureHash(params.data) + }, 'Expected EIP712 typed data') + break + case 'V3': + let data + assert.equal(typeof params, 'object', 'Params should be an object.') + assert.ok('data' in params, 'Params must include a data field.') + assert.ok('from' in params, 'Params must include a from field.') + assert.equal(typeof params.from, 'string', 'From field must be a string.') + assert.equal(typeof params.data, 'string', 'Data must be passed as a valid JSON string.') + assert.doesNotThrow(() => { data = JSON.parse(params.data) }, 'Data must be passed as a valid JSON string.') + const validation = jsonschema.validate(data, sigUtil.TYPED_MESSAGE_SCHEMA) + assert.ok(data.primaryType in data.types, `Primary type of "${data.primaryType}" has no type definition.`) + assert.equal(validation.errors.length, 0, 'Data must conform to EIP-712 schema. See https://git.io/fNtcx.') + const chainId = data.domain.chainId + const activeChainId = parseInt(this.networkController.getNetworkState()) + chainId && assert.equal(chainId, activeChainId, `Provided chainId (${chainId}) must match the active chainId (${activeChainId})`) + break + } } /** @@ -185,6 +230,7 @@ module.exports = class TypedMessageManager extends EventEmitter { */ prepMsgForSigning (msgParams) { delete msgParams.metamaskId + delete msgParams.version return Promise.resolve(msgParams) } @@ -198,6 +244,19 @@ module.exports = class TypedMessageManager extends EventEmitter { this._setMsgStatus(msgId, 'rejected') } + /** + * Sets a TypedMessage status to 'errored' via a call to this._setMsgStatus. + * + * @param {number} msgId The id of the TypedMessage to error + * + */ + errorMessage (msgId, error) { + const msg = this.getMsg(msgId) + msg.error = error + this._updateMsg(msg) + this._setMsgStatus(msgId, 'errored') + } + // // PRIVATE METHODS // @@ -221,7 +280,7 @@ module.exports = class TypedMessageManager extends EventEmitter { msg.status = status this._updateMsg(msg) this.emit(`${msgId}:${status}`, msg) - if (status === 'rejected' || status === 'signed') { + if (status === 'rejected' || status === 'signed' || status === 'errored') { this.emit(`${msgId}:finished`, msg) } } diff --git a/app/scripts/lib/util.js b/app/scripts/lib/util.js index d7423f2ad..ea13b26be 100644 --- a/app/scripts/lib/util.js +++ b/app/scripts/lib/util.js @@ -127,7 +127,21 @@ function BnMultiplyByFraction (targetBN, numerator, denominator) { return targetBN.mul(numBN).div(denomBN) } +function applyListeners (listeners, emitter) { + Object.keys(listeners).forEach((key) => { + emitter.on(key, listeners[key]) + }) +} + +function removeListeners (listeners, emitter) { + Object.keys(listeners).forEach((key) => { + emitter.removeListener(key, listeners[key]) + }) +} + module.exports = { + removeListeners, + applyListeners, getPlatform, getStack, getEnvironmentType, |