From da7471e0958d30b4465ef35090dbfa05b85471f0 Mon Sep 17 00:00:00 2001 From: kumavis Date: Thu, 3 Aug 2017 14:48:19 -0700 Subject: lint fixes --- app/scripts/contentscript.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/scripts') diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 291b922e8..4b674dcc0 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -81,7 +81,7 @@ function suffixCheck () { var currentUrl = window.location.href var currentRegex for (let i = 0; i < prohibitedTypes.length; i++) { - currentRegex = new RegExp(`\.${prohibitedTypes[i]}$`) + currentRegex = new RegExp(`\\.${prohibitedTypes[i]}$`) if (currentRegex.test(currentUrl)) { return false } -- cgit From da16f396266daa5ab0acc8f0c04d3e25b98c39f0 Mon Sep 17 00:00:00 2001 From: kumavis Date: Thu, 3 Aug 2017 15:05:32 -0700 Subject: Merge branch 'master' of github.com:MetaMask/metamask-extension into greenkeeper/initial --- app/scripts/background.js | 10 +- app/scripts/blacklister.js | 13 -- app/scripts/contentscript.js | 22 +- app/scripts/controllers/blacklist.js | 58 ++++++ app/scripts/controllers/infura.js | 2 +- app/scripts/controllers/network.js | 5 +- app/scripts/controllers/transactions.js | 351 +++++++++++++++++--------------- app/scripts/inpage.js | 1 + app/scripts/lib/inpage-provider.js | 3 + app/scripts/lib/nonce-tracker.js | 28 ++- app/scripts/lib/obj-multiplex.js | 16 +- app/scripts/lib/tx-utils.js | 61 ++---- app/scripts/lib/util.js | 8 + app/scripts/metamask-controller.js | 55 +++-- 14 files changed, 350 insertions(+), 283 deletions(-) delete mode 100644 app/scripts/blacklister.js create mode 100644 app/scripts/controllers/blacklist.js create mode 100644 app/scripts/lib/util.js (limited to 'app/scripts') diff --git a/app/scripts/background.js b/app/scripts/background.js index e8987394f..f077ca7a8 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -90,12 +90,12 @@ function setupController (initState) { extension.runtime.onConnect.addListener(connectRemote) function connectRemote (remotePort) { - var isMetaMaskInternalProcess = remotePort.name === 'popup' || remotePort.name === 'notification' - var portStream = new PortStream(remotePort) + const isMetaMaskInternalProcess = remotePort.name === 'popup' || remotePort.name === 'notification' + const portStream = new PortStream(remotePort) if (isMetaMaskInternalProcess) { // communication with popup popupIsOpen = popupIsOpen || (remotePort.name === 'popup') - controller.setupTrustedCommunication(portStream, 'MetaMask', remotePort.name) + controller.setupTrustedCommunication(portStream, 'MetaMask') // record popup as closed if (remotePort.name === 'popup') { endOfStream(portStream, () => { @@ -104,7 +104,7 @@ function setupController (initState) { } } else { // communication with page - var originDomain = urlUtil.parse(remotePort.sender.url).hostname + const originDomain = urlUtil.parse(remotePort.sender.url).hostname controller.setupUntrustedCommunication(portStream, originDomain) } } @@ -121,7 +121,7 @@ function setupController (initState) { // plugin badge text function updateBadge () { var label = '' - var unapprovedTxCount = controller.txController.unapprovedTxCount + var unapprovedTxCount = controller.txController.getUnapprovedTxCount() var unapprovedMsgCount = controller.messageManager.unapprovedMsgCount var unapprovedPersonalMsgs = controller.personalMessageManager.unapprovedPersonalMsgCount var count = unapprovedTxCount + unapprovedMsgCount + unapprovedPersonalMsgs diff --git a/app/scripts/blacklister.js b/app/scripts/blacklister.js deleted file mode 100644 index a45265a75..000000000 --- a/app/scripts/blacklister.js +++ /dev/null @@ -1,13 +0,0 @@ -const blacklistedDomains = require('etheraddresslookup/blacklists/domains.json') - -function detectBlacklistedDomain() { - var strCurrentTab = window.location.hostname - if (blacklistedDomains && blacklistedDomains.includes(strCurrentTab)) { - window.location.href = 'https://metamask.io/phishing.html' - } -} - -window.addEventListener('load', function() { - detectBlacklistedDomain() -}) - diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 4b674dcc0..acacf5d4c 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -37,28 +37,33 @@ function setupInjection () { function setupStreams () { // setup communication to page and plugin - var pageStream = new LocalMessageDuplexStream({ + const pageStream = new LocalMessageDuplexStream({ name: 'contentscript', target: 'inpage', }) pageStream.on('error', console.error) - var pluginPort = extension.runtime.connect({name: 'contentscript'}) - var pluginStream = new PortStream(pluginPort) + const pluginPort = extension.runtime.connect({ name: 'contentscript' }) + const pluginStream = new PortStream(pluginPort) pluginStream.on('error', console.error) // forward communication plugin->inpage pageStream.pipe(pluginStream).pipe(pageStream) // setup local multistream channels - var mx = ObjectMultiplex() + const mx = ObjectMultiplex() mx.on('error', console.error) mx.pipe(pageStream).pipe(mx) + mx.pipe(pluginStream).pipe(mx) // connect ping stream - var pongStream = new PongStream({ objectMode: true }) + const pongStream = new PongStream({ objectMode: true }) pongStream.pipe(mx.createStream('pingpong')).pipe(pongStream) - // ignore unused channels (handled by background) + // connect phishing warning stream + const phishingStream = mx.createStream('phishing') + phishingStream.once('data', redirectToPhishingWarning) + + // ignore unused channels (handled by background, inpage) mx.ignoreStream('provider') mx.ignoreStream('publicConfig') } @@ -88,3 +93,8 @@ function suffixCheck () { } return true } + +function redirectToPhishingWarning () { + console.log('MetaMask - redirecting to phishing warning') + window.location.href = 'https://metamask.io/phishing.html' +} diff --git a/app/scripts/controllers/blacklist.js b/app/scripts/controllers/blacklist.js new file mode 100644 index 000000000..7e01fa386 --- /dev/null +++ b/app/scripts/controllers/blacklist.js @@ -0,0 +1,58 @@ +const ObservableStore = require('obs-store') +const extend = require('xtend') +const PhishingDetector = require('eth-phishing-detect/src/detector') + +// compute phishing lists +const PHISHING_DETECTION_CONFIG = require('eth-phishing-detect/src/config.json') +// every ten minutes +const POLLING_INTERVAL = 10 * 60 * 1000 + +class BlacklistController { + + constructor (opts = {}) { + const initState = extend({ + phishing: PHISHING_DETECTION_CONFIG, + }, opts.initState) + this.store = new ObservableStore(initState) + // phishing detector + this._phishingDetector = null + this._setupPhishingDetector(initState.phishing) + // polling references + this._phishingUpdateIntervalRef = null + } + + // + // PUBLIC METHODS + // + + checkForPhishing (hostname) { + if (!hostname) return false + const { result } = this._phishingDetector.check(hostname) + return result + } + + async updatePhishingList () { + const response = await fetch('https://api.infura.io/v2/blacklist') + const phishing = await response.json() + this.store.updateState({ phishing }) + this._setupPhishingDetector(phishing) + return phishing + } + + scheduleUpdates () { + if (this._phishingUpdateIntervalRef) return + this._phishingUpdateIntervalRef = setInterval(() => { + this.updatePhishingList() + }, POLLING_INTERVAL) + } + + // + // PRIVATE METHODS + // + + _setupPhishingDetector (config) { + this._phishingDetector = new PhishingDetector(config) + } +} + +module.exports = BlacklistController diff --git a/app/scripts/controllers/infura.js b/app/scripts/controllers/infura.js index b34b0bc03..10adb1004 100644 --- a/app/scripts/controllers/infura.js +++ b/app/scripts/controllers/infura.js @@ -2,7 +2,7 @@ const ObservableStore = require('obs-store') const extend = require('xtend') // every ten minutes -const POLLING_INTERVAL = 300000 +const POLLING_INTERVAL = 10 * 60 * 1000 class InfuraController { diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js index c07f13b8d..0a3e5e26b 100644 --- a/app/scripts/controllers/network.js +++ b/app/scripts/controllers/network.js @@ -28,9 +28,9 @@ module.exports = class NetworkController extends EventEmitter { this._provider = provider } - initializeProvider (opts) { + initializeProvider (opts, providerContructor = MetaMaskProvider) { this.providerInit = opts - this._provider = MetaMaskProvider(opts) + this._provider = providerContructor(opts) this._proxy = new Proxy(this._provider, { get: (obj, name) => { if (name === 'on') return this._on.bind(this) @@ -38,6 +38,7 @@ module.exports = class NetworkController extends EventEmitter { }, set: (obj, name, value) => { this._provider[name] = value + return value }, }) this.provider.on('block', this._logBlock.bind(this)) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 5f3d84ebe..308d43cb0 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -1,9 +1,9 @@ const EventEmitter = require('events') -const async = require('async') const extend = require('xtend') +const clone = require('clone') const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') -const pify = require('pify') +const EthQuery = require('ethjs-query') const TxProviderUtil = require('../lib/tx-utils') const createId = require('../lib/random-id') const NonceTracker = require('../lib/nonce-tracker') @@ -22,7 +22,6 @@ module.exports = class TransactionController extends EventEmitter { this.blockTracker = opts.blockTracker this.nonceTracker = new NonceTracker({ provider: this.provider, - blockTracker: this.provider._blockTracker, getPendingTransactions: (address) => { return this.getFilteredTxList({ from: address, @@ -31,7 +30,7 @@ module.exports = class TransactionController extends EventEmitter { }) }, }) - this.query = opts.ethQuery + this.query = new EthQuery(this.provider) this.txProviderUtils = new TxProviderUtil(this.query) this.blockTracker.on('rawBlock', this.checkForTxInBlock.bind(this)) // this is a little messy but until ethstore has been either @@ -60,13 +59,6 @@ module.exports = class TransactionController extends EventEmitter { return this.preferencesStore.getState().selectedAddress } - // Returns the tx list - getTxList () { - const network = this.getNetwork() - const fullTxList = this.getFullTxList() - return fullTxList.filter(txMeta => txMeta.metamaskNetworkId === network) - } - // Returns the number of txs for the current network. getTxCount () { return this.getTxList().length @@ -77,6 +69,56 @@ module.exports = class TransactionController extends EventEmitter { return this.store.getState().transactions } + getUnapprovedTxCount () { + return Object.keys(this.getUnapprovedTxList()).length + } + + getPendingTxCount () { + return this.getTxsByMetaData('status', 'signed').length + } + + // Returns the tx list + getTxList () { + const network = this.getNetwork() + const fullTxList = this.getFullTxList() + return this.getTxsByMetaData('metamaskNetworkId', network, fullTxList) + } + + // gets tx by Id and returns it + getTx (txId) { + const txList = this.getTxList() + const txMeta = txList.find(txData => txData.id === txId) + return txMeta + } + getUnapprovedTxList () { + const txList = this.getTxList() + return txList.filter((txMeta) => txMeta.status === 'unapproved') + .reduce((result, tx) => { + result[tx.id] = tx + return result + }, {}) + } + + updateTx (txMeta) { + // create txMeta snapshot for history + const txMetaForHistory = clone(txMeta) + // dont include previous history in this snapshot + delete txMetaForHistory.history + // add snapshot to tx history + if (!txMeta.history) txMeta.history = [] + txMeta.history.push(txMetaForHistory) + + const txId = txMeta.id + const txList = this.getFullTxList() + const index = txList.findIndex(txData => txData.id === txId) + if (!txMeta.history) txMeta.history = [] + txMeta.history.push(txMetaForHistory) + + txList[index] = txMeta + this._saveTxList(txList) + this.emit('update') + } + // Adds a tx to the txlist addTx (txMeta) { const txCount = this.getTxCount() @@ -90,7 +132,7 @@ module.exports = class TransactionController extends EventEmitter { // or rejected tx's. // not tx's that are pending or unapproved if (txCount > txHistoryLimit - 1) { - var index = fullTxList.findIndex((metaTx) => ((metaTx.status === 'confirmed' || metaTx.status === 'rejected') && network === txMeta.metamaskNetworkId)) + const index = fullTxList.findIndex((metaTx) => ((metaTx.status === 'confirmed' || metaTx.status === 'rejected') && network === txMeta.metamaskNetworkId)) fullTxList.splice(index, 1) } fullTxList.push(txMeta) @@ -108,79 +150,59 @@ module.exports = class TransactionController extends EventEmitter { this.emit(`${txMeta.id}:unapproved`, txMeta) } - // gets tx by Id and returns it - getTx (txId, cb) { - var txList = this.getTxList() - var txMeta = txList.find(txData => txData.id === txId) - return cb ? cb(txMeta) : txMeta - } - - // - updateTx (txMeta) { - var txId = txMeta.id - var txList = this.getFullTxList() - var index = txList.findIndex(txData => txData.id === txId) - txList[index] = txMeta - this._saveTxList(txList) - this.emit('update') - } - - get unapprovedTxCount () { - return Object.keys(this.getUnapprovedTxList()).length - } - - get pendingTxCount () { - return this.getTxsByMetaData('status', 'signed').length + async newUnapprovedTransaction (txParams) { + log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`) + const txMeta = await this.addUnapprovedTransaction(txParams) + this.emit('newUnaprovedTx', txMeta) + // listen for tx completion (success, fail) + return new Promise((resolve, reject) => { + this.once(`${txMeta.id}:finished`, (completedTx) => { + switch (completedTx.status) { + case 'submitted': + return resolve(completedTx.hash) + case 'rejected': + return reject(new Error('MetaMask Tx Signature: User denied transaction signature.')) + default: + return reject(new Error(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(completedTx.txParams)}`)) + } + }) + }) } - addUnapprovedTransaction (txParams, done) { - let txMeta - async.waterfall([ - // validate - (cb) => this.txProviderUtils.validateTxParams(txParams, cb), - // construct txMeta - (cb) => { - txMeta = { - id: createId(), - time: (new Date()).getTime(), - status: 'unapproved', - metamaskNetworkId: this.getNetwork(), - txParams: txParams, - } - cb() - }, - // add default tx params - (cb) => this.addTxDefaults(txMeta, cb), - // save txMeta - (cb) => { - this.addTx(txMeta) - cb(null, txMeta) - }, - ], done) + async addUnapprovedTransaction (txParams) { + // validate + await this.txProviderUtils.validateTxParams(txParams) + // construct txMeta + const txMeta = { + id: createId(), + time: (new Date()).getTime(), + status: 'unapproved', + metamaskNetworkId: this.getNetwork(), + txParams: txParams, + history: [], + } + // add default tx params + await this.addTxDefaults(txMeta) + // save txMeta + this.addTx(txMeta) + return txMeta } - addTxDefaults (txMeta, cb) { + async addTxDefaults (txMeta) { const txParams = txMeta.txParams // ensure value txParams.value = txParams.value || '0x0' if (!txParams.gasPrice) { - this.query.gasPrice((err, gasPrice) => { - if (err) return cb(err) - // set gasPrice - txParams.gasPrice = gasPrice - }) + const gasPrice = await this.query.gasPrice() + txParams.gasPrice = gasPrice } // set gasLimit - this.txProviderUtils.analyzeGasUsage(txMeta, cb) + return await this.txProviderUtils.analyzeGasUsage(txMeta) } - getUnapprovedTxList () { - var txList = this.getTxList() - return txList.filter((txMeta) => txMeta.status === 'unapproved') - .reduce((result, tx) => { - result[tx.id] = tx - return result - }, {}) + async updateAndApproveTransaction (txMeta) { + this.updateTx(txMeta) + await this.approveTransaction(txMeta.id) } async approveTransaction (txId) { @@ -191,8 +213,12 @@ module.exports = class TransactionController extends EventEmitter { // get next nonce const txMeta = this.getTx(txId) const fromAddress = txMeta.txParams.from + // wait for a nonce nonceLock = await this.nonceTracker.getNonceLock(fromAddress) + // add nonce to txParams txMeta.txParams.nonce = nonceLock.nextNonce + // add nonce debugging information to txMeta + txMeta.nonceDetails = nonceLock.nonceDetails this.updateTx(txMeta) // sign transaction const rawTx = await this.signTransaction(txId) @@ -201,6 +227,7 @@ module.exports = class TransactionController extends EventEmitter { nonceLock.releaseLock() } catch (err) { this.setTxStatusFailed(txId, { + stack: err.stack || err.message, errCode: err.errCode || err, message: err.message || 'Transaction failed during approval', }) @@ -211,26 +238,6 @@ module.exports = class TransactionController extends EventEmitter { } } - cancelTransaction (txId, cb = warn) { - this.setTxStatusRejected(txId) - cb() - } - - async updateAndApproveTransaction (txMeta) { - this.updateTx(txMeta) - await this.approveTransaction(txMeta.id) - } - - getChainId () { - const networkState = this.networkStore.getState() - const getChainId = parseInt(networkState) - if (Number.isNaN(getChainId)) { - return 0 - } else { - return getChainId - } - } - async signTransaction (txId) { const txMeta = this.getTx(txId) const txParams = txMeta.txParams @@ -238,10 +245,9 @@ module.exports = class TransactionController extends EventEmitter { // add network/chain id txParams.chainId = this.getChainId() const ethTx = this.txProviderUtils.buildEthTxFromParams(txParams) - const rawTx = await this.signEthTx(ethTx, fromAddress).then(() => { - this.setTxStatusSigned(txMeta.id) - return ethUtil.bufferToHex(ethTx.serialize()) - }) + await this.signEthTx(ethTx, fromAddress) + this.setTxStatusSigned(txMeta.id) + const rawTx = ethUtil.bufferToHex(ethTx.serialize()) return rawTx } @@ -249,10 +255,24 @@ module.exports = class TransactionController extends EventEmitter { const txMeta = this.getTx(txId) txMeta.rawTx = rawTx this.updateTx(txMeta) - await this.txProviderUtils.publishTransaction(rawTx).then((txHash) => { - this.setTxHash(txId, txHash) - this.setTxStatusSubmitted(txId) - }) + const txHash = await this.txProviderUtils.publishTransaction(rawTx) + this.setTxHash(txId, txHash) + this.setTxStatusSubmitted(txId) + } + + async cancelTransaction (txId) { + this.setTxStatusRejected(txId) + } + + + getChainId () { + const networkState = this.networkStore.getState() + const getChainId = parseInt(networkState) + if (Number.isNaN(getChainId)) { + return 0 + } else { + return getChainId + } } // receives a txHash records the tx as signed @@ -265,7 +285,7 @@ module.exports = class TransactionController extends EventEmitter { /* Takes an object of fields to search for eg: - var thingsToLookFor = { + let thingsToLookFor = { to: '0x0..', from: '0x0..', status: 'signed', @@ -288,7 +308,7 @@ module.exports = class TransactionController extends EventEmitter { and that have been 'confirmed' */ getFilteredTxList (opts) { - var filteredTxList + let filteredTxList Object.keys(opts).forEach((key) => { filteredTxList = this.getTxsByMetaData(key, opts[key], filteredTxList) }) @@ -349,7 +369,7 @@ module.exports = class TransactionController extends EventEmitter { // merges txParams obj onto txData.txParams // use extend to ensure that all fields are filled updateTxParams (txId, txParams) { - var txMeta = this.getTx(txId) + const txMeta = this.getTx(txId) txMeta.txParams = extend(txMeta.txParams, txParams) this.updateTx(txMeta) } @@ -357,20 +377,19 @@ module.exports = class TransactionController extends EventEmitter { // checks if a signed tx is in a block and // if included sets the tx status as 'confirmed' checkForTxInBlock (block) { - var signedTxList = this.getFilteredTxList({status: 'submitted'}) + const signedTxList = this.getFilteredTxList({status: 'submitted'}) if (!signedTxList.length) return signedTxList.forEach((txMeta) => { - var txHash = txMeta.hash - var txId = txMeta.id + const txHash = txMeta.hash + const txId = txMeta.id if (!txHash) { - const errReason = { - errCode: 'No hash was provided', - message: 'We had an error while submitting this transaction, please try again.', - } - return this.setTxStatusFailed(txId, errReason) + const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.') + noTxHashErr.name = 'NoTxHashError' + this.setTxStatusFailed(txId, noTxHashErr) } + block.transactions.forEach((tx) => { if (tx.hash === txHash) this.setTxStatusConfirmed(txId) }) @@ -388,7 +407,45 @@ module.exports = class TransactionController extends EventEmitter { if (diff > 1) this._checkPendingTxs() } - // PRIVATE METHODS + resubmitPendingTxs () { + const pending = this.getTxsByMetaData('status', 'submitted') + // only try resubmitting if their are transactions to resubmit + if (!pending.length) return + pending.forEach((txMeta) => this._resubmitTx(txMeta).catch((err) => { + /* + Dont marked as failed if the error is a "known" transaction warning + "there is already a transaction with the same sender-nonce + but higher/same gas price" + */ + const errorMessage = err.message.toLowerCase() + const isKnownTx = ( + // geth + errorMessage.includes('replacement transaction underpriced') + || errorMessage.includes('known transaction') + // parity + || errorMessage.includes('gas price too low to replace') + || errorMessage.includes('transaction with the same hash was already imported') + // other + || errorMessage.includes('gateway timeout') + || errorMessage.includes('nonce too low') + ) + // ignore resubmit warnings, return early + if (isKnownTx) return + // encountered real error - transition to error state + this.setTxStatusFailed(txMeta.id, { + stack: err.stack || err.message, + errCode: err.errCode || err, + message: err.message, + }) + })) + } + + +/* _____________________________________ +| | +| PRIVATE METHODS | +|______________________________________*/ + // Should find the tx in the tx list and // update it. @@ -401,7 +458,7 @@ module.exports = class TransactionController extends EventEmitter { // - `'confirmed'` the tx has been included in a block. // - `'failed'` the tx failed for some reason, included on tx data. _setTxStatus (txId, status) { - var txMeta = this.getTx(txId) + const txMeta = this.getTx(txId) txMeta.status = status this.emit(`${txMeta.id}:${status}`, txId) if (status === 'submitted' || status === 'rejected') { @@ -426,39 +483,7 @@ module.exports = class TransactionController extends EventEmitter { this.memStore.updateState({ unapprovedTxs, selectedAddressTxList }) } - resubmitPendingTxs () { - const pending = this.getTxsByMetaData('status', 'submitted') - // only try resubmitting if their are transactions to resubmit - if (!pending.length) return - pending.forEach((txMeta) => this._resubmitTx(txMeta).catch((err) => { - /* - Dont marked as failed if the error is a "known" transaction warning - "there is already a transaction with the same sender-nonce - but higher/same gas price" - */ - const errorMessage = err.message.toLowerCase() - const isKnownTx = ( - // geth - errorMessage.includes('replacement transaction underpriced') - || errorMessage.includes('known transaction') - // parity - || errorMessage.includes('gas price too low to replace') - || errorMessage.includes('transaction with the same hash was already imported') - // other - || errorMessage.includes('gateway timeout') - || errorMessage.includes('nonce too low') - ) - // ignore resubmit warnings, return early - if (isKnownTx) return - // encountered real error - transition to error state - this.setTxStatusFailed(txMeta.id, { - errCode: err.errCode || err, - message: err.message, - }) - })) - } - - async _resubmitTx (txMeta, cb) { + async _resubmitTx (txMeta) { const address = txMeta.txParams.from const balance = this.ethStore.getState().accounts[address].balance if (!('retryCount' in txMeta)) txMeta.retryCount = 0 @@ -466,18 +491,21 @@ module.exports = class TransactionController extends EventEmitter { // if the value of the transaction is greater then the balance, fail. if (!this.txProviderUtils.sufficientBalance(txMeta.txParams, balance)) { const message = 'Insufficient balance.' - this.setTxStatusFailed(txMeta.id, { message }) - cb() - return log.error(message) + this.setTxStatusFailed(txMeta.id, { + stack: '_resubmitTx: custom tx-controller error', + message, + }) + log.error(message) + return } // Only auto-submit already-signed txs: - if (!('rawTx' in txMeta)) return cb() + if (!('rawTx' in txMeta)) return // Increment a try counter. txMeta.retryCount++ const rawTx = txMeta.rawTx - return await this.txProviderUtils.publishTransaction(rawTx, cb) + return await this.txProviderUtils.publishTransaction(rawTx) } // checks the network for signed txs and @@ -501,17 +529,14 @@ module.exports = class TransactionController extends EventEmitter { // extra check in case there was an uncaught error during the // signature and submission process if (!txHash) { - const errReason = { - errCode: 'No hash was provided', - message: 'We had an error while submitting this transaction, please try again.', - } - this.setTxStatusFailed(txId, errReason) - return + const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.') + noTxHashErr.name = 'NoTxHashError' + this.setTxStatusFailed(txId, noTxHashErr) } // get latest transaction status let txParams try { - txParams = await pify((cb) => this.query.getTransactionByHash(txHash, cb))() + txParams = await this.query.getTransactionByHash(txHash) if (!txParams) return if (txParams.blockNumber) { this.setTxStatusConfirmed(txId) @@ -524,12 +549,8 @@ module.exports = class TransactionController extends EventEmitter { message: 'There was a problem loading this transaction.', } this.updateTx(txMeta) - log.error(err) + throw err } } } - -} - - -const warn = () => log.warn('warn was used no cb provided') +} \ No newline at end of file diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index ec764535e..9e98c044b 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -65,3 +65,4 @@ function restoreContextAfterImports () { console.warn('MetaMask - global.define could not be overwritten.') } } + diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js index 8b8623974..fd032a673 100644 --- a/app/scripts/lib/inpage-provider.js +++ b/app/scripts/lib/inpage-provider.js @@ -26,6 +26,9 @@ function MetamaskInpageProvider (connectionStream) { (err) => logStreamDisconnectWarning('MetaMask PublicConfigStore', err) ) + // ignore phishing warning message (handled elsewhere) + multiStream.ignoreStream('phishing') + // connect to async provider const asyncProvider = self.asyncProvider = new StreamProvider() pipe( diff --git a/app/scripts/lib/nonce-tracker.js b/app/scripts/lib/nonce-tracker.js index b76dac4e8..8328e81ec 100644 --- a/app/scripts/lib/nonce-tracker.js +++ b/app/scripts/lib/nonce-tracker.js @@ -4,8 +4,8 @@ const Mutex = require('await-semaphore').Mutex class NonceTracker { - constructor ({ blockTracker, provider, getPendingTransactions }) { - this.blockTracker = blockTracker + constructor ({ provider, getPendingTransactions }) { + this.provider = provider this.ethQuery = new EthQuery(provider) this.getPendingTransactions = getPendingTransactions this.lockMap = {} @@ -31,21 +31,25 @@ class NonceTracker { const currentBlock = await this._getCurrentBlock() const pendingTransactions = this.getPendingTransactions(address) const pendingCount = pendingTransactions.length - assert(Number.isInteger(pendingCount), 'nonce-tracker - pendingCount is an integer') + assert(Number.isInteger(pendingCount), `nonce-tracker - pendingCount is not an integer - got: (${typeof pendingCount}) "${pendingCount}"`) const baseCountHex = await this._getTxCount(address, currentBlock) const baseCount = parseInt(baseCountHex, 16) - assert(Number.isInteger(baseCount), 'nonce-tracker - baseCount is an integer') + assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`) const nextNonce = baseCount + pendingCount - assert(Number.isInteger(nextNonce), 'nonce-tracker - nextNonce is an integer') - // return next nonce and release cb - return { nextNonce, releaseLock } + assert(Number.isInteger(nextNonce), `nonce-tracker - nextNonce is not an integer - got: (${typeof nextNonce}) "${nextNonce}"`) + // collect the numbers used to calculate the nonce for debugging + const blockNumber = currentBlock.number + const nonceDetails = { blockNumber, baseCount, baseCountHex, pendingCount } + // return nonce and release cb + return { nextNonce, nonceDetails, releaseLock } } async _getCurrentBlock () { - const currentBlock = this.blockTracker.getCurrentBlock() + const blockTracker = this._getBlockTracker() + const currentBlock = blockTracker.getCurrentBlock() if (currentBlock) return currentBlock return await Promise((reject, resolve) => { - this.blockTracker.once('latest', resolve) + blockTracker.once('latest', resolve) }) } @@ -79,6 +83,12 @@ class NonceTracker { return mutex } + // this is a hotfix for the fact that the blockTracker will + // change when the network changes + _getBlockTracker () { + return this.provider._blockTracker + } + } module.exports = NonceTracker diff --git a/app/scripts/lib/obj-multiplex.js b/app/scripts/lib/obj-multiplex.js index bd114c394..0034febe0 100644 --- a/app/scripts/lib/obj-multiplex.js +++ b/app/scripts/lib/obj-multiplex.js @@ -5,12 +5,16 @@ module.exports = ObjectMultiplex function ObjectMultiplex (opts) { opts = opts || {} // create multiplexer - var mx = through.obj(function (chunk, enc, cb) { - var name = chunk.name - var data = chunk.data - var substream = mx.streams[name] + const mx = through.obj(function (chunk, enc, cb) { + const name = chunk.name + const data = chunk.data + if (!name) { + console.warn(`ObjectMultiplex - Malformed chunk without name "${chunk}"`) + return cb() + } + const substream = mx.streams[name] if (!substream) { - console.warn(`orphaned data for stream "${name}"`) + console.warn(`ObjectMultiplex - orphaned data for stream "${name}"`) } else { if (substream.push) substream.push(data) } @@ -19,7 +23,7 @@ function ObjectMultiplex (opts) { mx.streams = {} // create substreams mx.createStream = function (name) { - var substream = mx.streams[name] = through.obj(function (chunk, enc, cb) { + const substream = mx.streams[name] = through.obj(function (chunk, enc, cb) { mx.push({ name: name, data: chunk, diff --git a/app/scripts/lib/tx-utils.js b/app/scripts/lib/tx-utils.js index 8f6943937..3687a9652 100644 --- a/app/scripts/lib/tx-utils.js +++ b/app/scripts/lib/tx-utils.js @@ -1,4 +1,3 @@ -const async = require('async') const ethUtil = require('ethereumjs-util') const Transaction = require('ethereumjs-tx') const normalize = require('eth-sig-util').normalize @@ -10,24 +9,19 @@ its passed ethquery and used to do things like calculate gas of a tx. */ -module.exports = class txProviderUtils { - +module.exports = class txProvideUtils { constructor (ethQuery) { this.query = ethQuery } - analyzeGasUsage (txMeta, cb) { - var self = this - this.query.getBlockByNumber('latest', true, (err, block) => { - if (err) return cb(err) - async.waterfall([ - self.estimateTxGas.bind(self, txMeta, block.gasLimit), - self.setTxGas.bind(self, txMeta, block.gasLimit), - ], cb) - }) + async analyzeGasUsage (txMeta) { + const block = await this.query.getBlockByNumber('latest', true) + const estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit) + this.setTxGas(txMeta, block.gasLimit, estimatedGasHex) + return txMeta } - estimateTxGas (txMeta, blockGasLimitHex, cb) { + async estimateTxGas (txMeta, blockGasLimitHex) { const txParams = txMeta.txParams // check if gasLimit is already specified txMeta.gasLimitSpecified = Boolean(txParams.gas) @@ -38,10 +32,10 @@ module.exports = class txProviderUtils { txParams.gas = bnToHex(saferGasLimitBN) } // run tx, see if it will OOG - this.query.estimateGas(txParams, cb) + return this.query.estimateGas(txParams) } - setTxGas (txMeta, blockGasLimitHex, estimatedGasHex, cb) { + setTxGas (txMeta, blockGasLimitHex, estimatedGasHex) { txMeta.estimatedGas = estimatedGasHex const txParams = txMeta.txParams @@ -49,14 +43,12 @@ module.exports = class txProviderUtils { // use original specified amount if (txMeta.gasLimitSpecified) { txMeta.estimatedGas = txParams.gas - cb() return } // if gasLimit not originally specified, // try adding an additional gas buffer to our estimation for safety const recommendedGasHex = this.addGasBuffer(txMeta.estimatedGas, blockGasLimitHex) txParams.gas = recommendedGasHex - cb() return } @@ -74,22 +66,6 @@ module.exports = class txProviderUtils { return bnToHex(upperGasLimitBn) } - fillInTxParams (txParams, cb) { - const fromAddress = txParams.from - const reqs = {} - - if (isUndef(txParams.gas)) reqs.gas = (cb) => this.query.estimateGas(txParams, cb) - if (isUndef(txParams.gasPrice)) reqs.gasPrice = (cb) => this.query.gasPrice(cb) - if (isUndef(txParams.nonce)) reqs.nonce = (cb) => this.query.getTransactionCount(fromAddress, 'pending', cb) - - async.parallel(reqs, function (err, result) { - if (err) return cb(err) - // write results to txParams obj - Object.assign(txParams, result) - cb() - }) - } - // builds ethTx from txParams object buildEthTxFromParams (txParams) { // normalize values @@ -106,20 +82,13 @@ module.exports = class txProviderUtils { return ethTx } - publishTransaction (rawTx) { - return new Promise((resolve, reject) => { - this.query.sendRawTransaction(rawTx, (err, ress) => { - if (err) reject(err) - else resolve(ress) - }) - }) + async publishTransaction (rawTx) { + return await this.query.sendRawTransaction(rawTx) } - validateTxParams (txParams, cb) { + async validateTxParams (txParams) { if (('value' in txParams) && txParams.value.indexOf('-') === 0) { - cb(new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)) - } else { - cb() + throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`) } } @@ -137,10 +106,6 @@ module.exports = class txProviderUtils { // util -function isUndef (value) { - return value === undefined -} - function bnToHex (inputBn) { return ethUtil.addHexPrefix(inputBn.toString(16)) } diff --git a/app/scripts/lib/util.js b/app/scripts/lib/util.js new file mode 100644 index 000000000..bddd60ee8 --- /dev/null +++ b/app/scripts/lib/util.js @@ -0,0 +1,8 @@ +module.exports = { + getStack, +} + +function getStack () { + const stack = new Error('Stack trace generator - not an error').stack + return stack +} diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 11dcde2c1..a007d6fc5 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -16,6 +16,7 @@ const NoticeController = require('./notice-controller') const ShapeShiftController = require('./controllers/shapeshift') const AddressBookController = require('./controllers/address-book') const InfuraController = require('./controllers/infura') +const BlacklistController = require('./controllers/blacklist') const MessageManager = require('./lib/message-manager') const PersonalMessageManager = require('./lib/personal-message-manager') const TransactionController = require('./controllers/transactions') @@ -69,6 +70,10 @@ module.exports = class MetamaskController extends EventEmitter { }) this.infuraController.scheduleInfuraNetworkCheck() + this.blacklistController = new BlacklistController({ + initState: initState.BlacklistController, + }) + this.blacklistController.scheduleUpdates() // rpc provider this.provider = this.initializeProvider() @@ -108,6 +113,7 @@ module.exports = class MetamaskController extends EventEmitter { ethQuery: this.ethQuery, ethStore: this.ethStore, }) + this.txController.on('newUnaprovedTx', opts.showUnapprovedTx.bind(opts)) // notices this.noticeController = new NoticeController({ @@ -151,6 +157,9 @@ module.exports = class MetamaskController extends EventEmitter { this.networkController.store.subscribe((state) => { this.store.updateState({ NetworkController: state }) }) + this.blacklistController.store.subscribe((state) => { + this.store.updateState({ BlacklistController: state }) + }) this.infuraController.store.subscribe((state) => { this.store.updateState({ InfuraController: state }) }) @@ -195,7 +204,7 @@ module.exports = class MetamaskController extends EventEmitter { cb(null, result) }, // tx signing - processTransaction: (txParams, cb) => this.newUnapprovedTransaction(txParams, cb), + processTransaction: nodeify(async (txParams) => await this.txController.newUnapprovedTransaction(txParams), this), // old style msg signing processMessage: this.newUnsignedMessage.bind(this), @@ -308,7 +317,7 @@ module.exports = class MetamaskController extends EventEmitter { exportAccount: nodeify(keyringController.exportAccount, keyringController), // txController - cancelTransaction: txController.cancelTransaction.bind(txController), + cancelTransaction: nodeify(txController.cancelTransaction, txController), updateAndApproveTransaction: nodeify(txController.updateAndApproveTransaction, txController), // messageManager @@ -326,8 +335,15 @@ module.exports = class MetamaskController extends EventEmitter { } setupUntrustedCommunication (connectionStream, originDomain) { + // Check if new connection is blacklisted + if (this.blacklistController.checkForPhishing(originDomain)) { + console.log('MetaMask - sending phishing warning for', originDomain) + this.sendPhishingWarning(connectionStream, originDomain) + return + } + // setup multiplexing - var mx = setupMultiplex(connectionStream) + const mx = setupMultiplex(connectionStream) // connect features this.setupProviderConnection(mx.createStream('provider'), originDomain) this.setupPublicConfig(mx.createStream('publicConfig')) @@ -335,12 +351,18 @@ module.exports = class MetamaskController extends EventEmitter { setupTrustedCommunication (connectionStream, originDomain) { // setup multiplexing - var mx = setupMultiplex(connectionStream) + const mx = setupMultiplex(connectionStream) // connect features this.setupControllerConnection(mx.createStream('controller')) this.setupProviderConnection(mx.createStream('provider'), originDomain) } + sendPhishingWarning (connectionStream, hostname) { + const mx = setupMultiplex(connectionStream) + const phishingStream = mx.createStream('phishing') + phishingStream.write({ hostname }) + } + setupControllerConnection (outStream) { const api = this.getApi() const dnode = Dnode(api) @@ -440,27 +462,6 @@ module.exports = class MetamaskController extends EventEmitter { // Identity Management // - newUnapprovedTransaction (txParams, cb) { - log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`) - const self = this - self.txController.addUnapprovedTransaction(txParams, (err, txMeta) => { - if (err) return cb(err) - self.sendUpdate() - self.opts.showUnapprovedTx(txMeta) - // listen for tx completion (success, fail) - self.txController.once(`${txMeta.id}:finished`, (completedTx) => { - switch (completedTx.status) { - case 'submitted': - return cb(null, completedTx.hash) - case 'rejected': - return cb(new Error('MetaMask Tx Signature: User denied transaction signature.')) - default: - return cb(new Error(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(completedTx.txParams)}`)) - } - }) - }) - } - newUnsignedMessage (msgParams, cb) { const msgId = this.messageManager.addUnapprovedMessage(msgParams) this.sendUpdate() @@ -646,6 +647,4 @@ module.exports = class MetamaskController extends EventEmitter { return Promise.resolve(rpcTarget) }) } - - -} +} \ No newline at end of file -- cgit