diff options
-rw-r--r-- | CHANGELOG.md | 11 | ||||
-rw-r--r-- | app/manifest.json | 11 | ||||
-rw-r--r-- | app/scripts/background.js | 36 | ||||
-rw-r--r-- | app/scripts/blacklister.js | 14 | ||||
-rw-r--r-- | app/scripts/contentscript.js | 22 | ||||
-rw-r--r-- | app/scripts/controllers/blacklist.js | 59 | ||||
-rw-r--r-- | app/scripts/controllers/infura.js | 16 | ||||
-rw-r--r-- | app/scripts/controllers/transactions.js | 344 | ||||
-rw-r--r-- | app/scripts/lib/inpage-provider.js | 3 | ||||
-rw-r--r-- | app/scripts/lib/is-phish.js | 38 | ||||
-rw-r--r-- | app/scripts/lib/nodeify.js | 3 | ||||
-rw-r--r-- | app/scripts/lib/obj-multiplex.js | 16 | ||||
-rw-r--r-- | app/scripts/lib/tx-utils.js | 61 | ||||
-rw-r--r-- | app/scripts/metamask-controller.js | 55 | ||||
-rw-r--r-- | gulpfile.js | 1 | ||||
-rw-r--r-- | package.json | 7 | ||||
-rw-r--r-- | test/unit/actions/tx_test.js | 10 | ||||
-rw-r--r-- | test/unit/blacklist-controller-test.js | 41 | ||||
-rw-r--r-- | test/unit/blacklister-test.js | 24 | ||||
-rw-r--r-- | test/unit/nodeify-test.js | 11 | ||||
-rw-r--r-- | test/unit/tx-controller-test.js | 164 | ||||
-rw-r--r-- | ui/app/actions.js | 9 |
22 files changed, 526 insertions, 430 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 600df2955..71c4f46f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ - Replace account scren with an account drop-down menu. - Replace confusing buttons with an new account-specific drop-down menu. +## 3.9.5 2017-8-04 + +- Improved phishing detection configuration update rate + +## 3.9.4 2017-8-03 + +- Fixed bug that prevented transactions from being rejected. + +## 3.9.3 2017-8-03 + +- Add support for EGO ujo token - Continuously update blacklist for known phishing sites in background. - Automatically detect suspicious URLs too similar to common phishing targets, and blacklist them. diff --git a/app/manifest.json b/app/manifest.json index 591a07d0d..3b9194eb9 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.9.2", + "version": "3.9.5", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", @@ -52,15 +52,6 @@ ], "run_at": "document_start", "all_frames": true - }, - { - "run_at": "document_start", - "matches": [ - "http://*/*", - "https://*/*" - ], - "js": ["scripts/blacklister.js"], - "all_frames": true } ], "permissions": [ diff --git a/app/scripts/background.js b/app/scripts/background.js index bc0fbdc37..f077ca7a8 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -11,7 +11,6 @@ const NotificationManager = require('./lib/notification-manager.js') const MetamaskController = require('./metamask-controller') const extension = require('extensionizer') const firstTimeState = require('./first-time-state') -const isPhish = require('./lib/is-phish') const STORAGE_KEY = 'metamask-config' const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' @@ -91,16 +90,12 @@ function setupController (initState) { extension.runtime.onConnect.addListener(connectRemote) function connectRemote (remotePort) { - if (remotePort.name === 'blacklister') { - return checkBlacklist(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, () => { @@ -109,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) } } @@ -126,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 @@ -140,27 +135,6 @@ function setupController (initState) { return Promise.resolve() } -// Listen for new pages and return if blacklisted: -function checkBlacklist (port) { - const handler = handleNewPageLoad.bind(null, port) - port.onMessage.addListener(handler) - setTimeout(() => { - port.onMessage.removeListener(handler) - }, 30000) -} - -function handleNewPageLoad (port, message) { - const { pageLoaded } = message - if (!pageLoaded || !global.metamaskController) return - - const state = global.metamaskController.getState() - const updatedBlacklist = state.blacklist - - if (isPhish({ updatedBlacklist, hostname: pageLoaded })) { - port.postMessage({ 'blacklist': pageLoaded }) - } -} - // // Etc... // diff --git a/app/scripts/blacklister.js b/app/scripts/blacklister.js deleted file mode 100644 index 37751b595..000000000 --- a/app/scripts/blacklister.js +++ /dev/null @@ -1,14 +0,0 @@ -const extension = require('extensionizer')
-
-var port = extension.runtime.connect({name: 'blacklister'})
-port.postMessage({ 'pageLoaded': window.location.hostname })
-port.onMessage.addListener(redirectIfBlacklisted)
-
-function redirectIfBlacklisted (response) {
- const { blacklist } = response
- const host = window.location.hostname
- if (blacklist && blacklist === host) {
- window.location.href = 'https://metamask.io/phishing.html'
- }
-}
-
diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 291b922e8..6fde0edcd 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..dd671943f --- /dev/null +++ b/app/scripts/controllers/blacklist.js @@ -0,0 +1,59 @@ +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 four minutes +const POLLING_INTERVAL = 4 * 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.updatePhishingList() + 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 97b2ab7e3..10adb1004 100644 --- a/app/scripts/controllers/infura.js +++ b/app/scripts/controllers/infura.js @@ -1,16 +1,14 @@ const ObservableStore = require('obs-store') const extend = require('xtend') -const recentBlacklist = require('etheraddresslookup/blacklists/domains.json') // every ten minutes -const POLLING_INTERVAL = 300000 +const POLLING_INTERVAL = 10 * 60 * 1000 class InfuraController { constructor (opts = {}) { const initState = extend({ infuraNetworkStatus: {}, - blacklist: recentBlacklist, }, opts.initState) this.store = new ObservableStore(initState) } @@ -32,24 +30,12 @@ class InfuraController { }) } - updateLocalBlacklist () { - return fetch('https://api.infura.io/v1/blacklist') - .then(response => response.json()) - .then((parsedResponse) => { - this.store.updateState({ - blacklist: parsedResponse, - }) - return parsedResponse - }) - } - scheduleInfuraNetworkCheck () { if (this.conversionInterval) { clearInterval(this.conversionInterval) } this.conversionInterval = setInterval(() => { this.checkInfuraNetworkStatus() - this.updateLocalBlacklist() }, POLLING_INTERVAL) } } diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index f71659042..308d43cb0 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -1,12 +1,10 @@ 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 getStack = require('../lib/util').getStack const createId = require('../lib/random-id') const NonceTracker = require('../lib/nonce-tracker') @@ -32,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 @@ -61,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 @@ -78,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() @@ -91,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) @@ -109,92 +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) { - // create txMeta snapshot for history - const txMetaForHistory = clone(txMeta) - // dont include previous history in this snapshot - delete txMetaForHistory.history - // add stack to help understand why tx was updated - txMetaForHistory.stack = getStack() - // add snapshot to tx history - if (!txMeta.history) txMeta.history = [] - txMeta.history.push(txMetaForHistory) - - // update the tx - 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, - history: [], - } - 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) { @@ -230,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 @@ -257,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 } @@ -268,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 @@ -284,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', @@ -307,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) }) @@ -368,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) } @@ -376,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) { - return this.setTxStatusFailed(txId, { - stack: 'checkForTxInBlock: custom tx-controller error message', - errCode: 'No hash was provided', - message: 'We had an error while submitting this transaction, please try again.', - }) + 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) }) @@ -407,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. @@ -420,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') { @@ -445,39 +483,6 @@ 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, { - stack: err.stack || err.message, - errCode: err.errCode || err, - message: err.message, - }) - })) - } - async _resubmitTx (txMeta) { const address = txMeta.txParams.from const balance = this.ethStore.getState().accounts[address].balance @@ -524,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) { - this.setTxStatusFailed(txId, { - stack: '_checkPendingTxs: custom tx-controller error message', - errCode: 'No hash was provided', - message: 'We had an error while submitting this transaction, please try again.', - }) - 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) @@ -547,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/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/is-phish.js b/app/scripts/lib/is-phish.js deleted file mode 100644 index 68c09e4ac..000000000 --- a/app/scripts/lib/is-phish.js +++ /dev/null @@ -1,38 +0,0 @@ -const levenshtein = require('fast-levenshtein') -const blacklistedMetaMaskDomains = ['metamask.com'] -let blacklistedDomains = require('etheraddresslookup/blacklists/domains.json').concat(blacklistedMetaMaskDomains) -const whitelistedMetaMaskDomains = ['metamask.io', 'www.metamask.io'] -const whitelistedDomains = require('etheraddresslookup/whitelists/domains.json').concat(whitelistedMetaMaskDomains) -const LEVENSHTEIN_TOLERANCE = 4 -const LEVENSHTEIN_CHECKS = ['myetherwallet', 'myetheroll', 'ledgerwallet', 'metamask'] - - -// credit to @sogoiii and @409H for their help! -// Return a boolean on whether or not a phish is detected. -function isPhish({ hostname, updatedBlacklist = null }) { - var strCurrentTab = hostname - - // check if the domain is part of the whitelist. - if (whitelistedDomains && whitelistedDomains.includes(strCurrentTab)) { return false } - - // Allow updating of blacklist: - if (updatedBlacklist) { - blacklistedDomains = blacklistedDomains.concat(updatedBlacklist) - } - - // check if the domain is part of the blacklist. - const isBlacklisted = blacklistedDomains && blacklistedDomains.includes(strCurrentTab) - - // check for similar values. - let levenshteinMatched = false - var levenshteinForm = strCurrentTab.replace(/\./g, '') - LEVENSHTEIN_CHECKS.forEach((element) => { - if (levenshtein.get(element, levenshteinForm) <= LEVENSHTEIN_TOLERANCE) { - levenshteinMatched = true - } - }) - - return isBlacklisted || levenshteinMatched -} - -module.exports = isPhish diff --git a/app/scripts/lib/nodeify.js b/app/scripts/lib/nodeify.js index 299bfe624..832d6c6d3 100644 --- a/app/scripts/lib/nodeify.js +++ b/app/scripts/lib/nodeify.js @@ -1,9 +1,10 @@ const promiseToCallback = require('promise-to-callback') -module.exports = function(fn, context) { +module.exports = function nodeify (fn, context) { return function(){ const args = [].slice.call(arguments) const callback = args.pop() + if (typeof callback !== 'function') throw new Error('callback is not a function') promiseToCallback(fn.apply(context, args))(callback) } } 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/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 diff --git a/gulpfile.js b/gulpfile.js index 8e008377d..f351c2e2f 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -168,7 +168,6 @@ gulp.task('lint', function () { const jsFiles = [ 'inpage', 'contentscript', - 'blacklister', 'background', 'popup', ] diff --git a/package.json b/package.json index 5449c1fd8..bf3f316dd 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "start": "npm run dev", "dev": "gulp dev --debug", "disc": "gulp disc --debug", - "clear": "rm -rf node_modules/eth-contract-metadata && rm -rf node_modules/etheraddresslookup", + "clear": "rm -rf node_modules/eth-contract-metadata && rm -rf node_modules/eth-phishing-detect", "dist": "npm run clear && npm install && gulp dist", "test": "npm run lint && npm run test-unit && npm run test-integration", "test-unit": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/**/*.js\"", @@ -68,15 +68,16 @@ "eth-bin-to-ops": "^1.0.1", "eth-contract-metadata": "^1.1.4", "eth-hd-keyring": "^1.1.1", + "eth-phishing-detect": "^1.1.4", "eth-query": "^2.1.2", "eth-sig-util": "^1.2.2", "eth-simple-keyring": "^1.1.1", "eth-token-tracker": "^1.1.2", - "etheraddresslookup": "github:409H/EtherAddressLookup", "ethereumjs-tx": "^1.3.0", "ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-wallet": "^0.6.0", "ethjs-ens": "^2.0.0", + "ethjs-query": "^0.2.6", "express": "^4.14.0", "extension-link-enabler": "^1.0.0", "extensionizer": "^1.0.0", @@ -130,7 +131,7 @@ "sw-stream": "^2.0.0", "textarea-caret": "^3.0.1", "three.js": "^0.73.2", - "through2": "^2.0.1", + "through2": "^2.0.3", "valid-url": "^1.0.9", "vreme": "^3.0.2", "web3": "0.19.1", diff --git a/test/unit/actions/tx_test.js b/test/unit/actions/tx_test.js index 0ea1bfdc7..67c72e9a5 100644 --- a/test/unit/actions/tx_test.js +++ b/test/unit/actions/tx_test.js @@ -45,13 +45,15 @@ describe('tx confirmation screen', function () { before(function (done) { actions._setBackgroundConnection({ approveTransaction (txId, cb) { cb('An error!') }, - cancelTransaction (txId) { /* noop */ }, + cancelTransaction (txId, cb) { cb() }, clearSeedWordCache (cb) { cb() }, }) - const action = actions.cancelTx({value: firstTxId}) - result = reducers(initialState, action) - done() + actions.cancelTx({value: firstTxId})((action) => { + result = reducers(initialState, action) + done() + }) + }) it('should transition to the account detail view', function () { diff --git a/test/unit/blacklist-controller-test.js b/test/unit/blacklist-controller-test.js new file mode 100644 index 000000000..a9260466f --- /dev/null +++ b/test/unit/blacklist-controller-test.js @@ -0,0 +1,41 @@ +const assert = require('assert') +const BlacklistController = require('../../app/scripts/controllers/blacklist') + +describe('blacklist controller', function () { + let blacklistController + + before(() => { + blacklistController = new BlacklistController() + }) + + describe('checkForPhishing', function () { + it('should not flag whitelisted values', function () { + const result = blacklistController.checkForPhishing('www.metamask.io') + assert.equal(result, false) + }) + it('should flag explicit values', function () { + const result = blacklistController.checkForPhishing('metamask.com') + assert.equal(result, true) + }) + it('should flag levenshtein values', function () { + const result = blacklistController.checkForPhishing('metmask.io') + assert.equal(result, true) + }) + it('should not flag not-even-close values', function () { + const result = blacklistController.checkForPhishing('example.com') + assert.equal(result, false) + }) + it('should not flag the ropsten faucet domains', function () { + const result = blacklistController.checkForPhishing('faucet.metamask.io') + assert.equal(result, false) + }) + it('should not flag the mascara domain', function () { + const result = blacklistController.checkForPhishing('zero.metamask.io') + assert.equal(result, false) + }) + it('should not flag the mascara-faucet domain', function () { + const result = blacklistController.checkForPhishing('zero-faucet.metamask.io') + assert.equal(result, false) + }) + }) +})
\ No newline at end of file diff --git a/test/unit/blacklister-test.js b/test/unit/blacklister-test.js deleted file mode 100644 index 1badc2c8f..000000000 --- a/test/unit/blacklister-test.js +++ /dev/null @@ -1,24 +0,0 @@ -const assert = require('assert') -const isPhish = require('../../app/scripts/lib/is-phish') - -describe('blacklister', function () { - describe('#isPhish', function () { - it('should not flag whitelisted values', function () { - var result = isPhish({ hostname: 'www.metamask.io' }) - assert(!result) - }) - it('should flag explicit values', function () { - var result = isPhish({ hostname: 'metamask.com' }) - assert(result) - }) - it('should flag levenshtein values', function () { - var result = isPhish({ hostname: 'metmask.com' }) - assert(result) - }) - it('should not flag not-even-close values', function () { - var result = isPhish({ hostname: 'example.com' }) - assert(!result) - }) - }) -}) - diff --git a/test/unit/nodeify-test.js b/test/unit/nodeify-test.js index 06241334d..537dae605 100644 --- a/test/unit/nodeify-test.js +++ b/test/unit/nodeify-test.js @@ -17,4 +17,15 @@ describe('nodeify', function () { done() }) }) + + it('should throw if the last argument is not a function', function (done) { + const nodified = nodeify(obj.promiseFunc, obj) + try { + nodified('baz') + done(new Error('should have thrown if the last argument is not a function')) + } catch (err) { + assert.equal(err.message, 'callback is not a function') + done() + } + }) }) diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index 31908569a..f290088a1 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -1,11 +1,11 @@ const assert = require('assert') const ethUtil = require('ethereumjs-util') const EthTx = require('ethereumjs-tx') -const EthQuery = require('eth-query') const ObservableStore = require('obs-store') const clone = require('clone') const sinon = require('sinon') const TransactionController = require('../../app/scripts/controllers/transactions') +const TxProvideUtils = require('../../app/scripts/lib/tx-utils') const noop = () => true const currentNetworkId = 42 const otherNetworkId = 36 @@ -20,7 +20,6 @@ describe('Transaction Controller', function () { txHistoryLimit: 10, blockTracker: { getCurrentBlock: noop, on: noop, once: noop }, provider: { sendAsync: noop }, - ethQuery: new EthQuery({ sendAsync: noop }), ethStore: { getState: noop }, signTransaction: (ethTx) => new Promise((resolve) => { ethTx.sign(privKey) @@ -28,24 +27,147 @@ describe('Transaction Controller', function () { }), }) txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop }) + txController.query = new Proxy({}, { + get: (queryStubResult, key) => { + if (key === 'stubResult') { + return function (method, ...args) { + queryStubResult[method] = args + } + } else { + const returnValues = queryStubResult[key] + return () => Promise.resolve(...returnValues) + } + }, + }) + txController.txProviderUtils = new TxProvideUtils(txController.query) + }) + + describe('#newUnapprovedTransaction', function () { + let stub, txMeta, txParams + beforeEach(function () { + txParams = { + 'from':'0xc684832530fcbddae4b4230a47e991ddcec2831d', + 'to':'0xc684832530fcbddae4b4230a47e991ddcec2831d', + }, + txMeta = { + status: 'unapproved', + id: 1, + metamaskNetworkId: currentNetworkId, + txParams, + } + txController._saveTxList([txMeta]) + stub = sinon.stub(txController, 'addUnapprovedTransaction').returns(Promise.resolve(txMeta)) + }) + + afterEach(function () { + stub.restore() + }) + + it('should emit newUnaprovedTx event and pass txMeta as the first argument', function (done) { + txController.once('newUnaprovedTx', (txMetaFromEmit) => { + assert(txMetaFromEmit, 'txMeta is falsey') + assert.equal(txMetaFromEmit.id, 1, 'the right txMeta was passed') + done() + }) + txController.newUnapprovedTransaction(txParams) + .catch(done) + }) + + it('should resolve when finished and status is submitted and resolve with the hash', function (done) { + txController.once('newUnaprovedTx', (txMetaFromEmit) => { + setTimeout(() => { + console.log('HELLLO') + txController.setTxHash(txMetaFromEmit.id, '0x0') + txController.setTxStatusSubmitted(txMetaFromEmit.id) + }, 10) + }) + + txController.newUnapprovedTransaction(txParams) + .then((hash) => { + assert(hash, 'newUnapprovedTransaction needs to return the hash') + done() + }) + .catch(done) + }) + + it('should reject when finished and status is rejected', function (done) { + txController.once('newUnaprovedTx', (txMetaFromEmit) => { + setTimeout(() => { + console.log('HELLLO') + txController.setTxStatusRejected(txMetaFromEmit.id) + }, 10) + }) + + txController.newUnapprovedTransaction(txParams) + .catch((err) => { + if (err.message === 'MetaMask Tx Signature: User denied transaction signature.') done() + else done(err) + }) + }) + }) + + describe('#addUnapprovedTransaction', function () { + it('should add an unapproved transaction and return a valid txMeta', function (done) { + const addTxDefaultsStub = sinon.stub(txController, 'addTxDefaults').callsFake(() => Promise.resolve()) + txController.addUnapprovedTransaction({}) + .then((txMeta) => { + assert(('id' in txMeta), 'should have a id') + assert(('time' in txMeta), 'should have a time stamp') + assert(('metamaskNetworkId' in txMeta), 'should have a metamaskNetworkId') + assert(('txParams' in txMeta), 'should have a txParams') + assert(('history' in txMeta), 'should have a history') + + const memTxMeta = txController.getTx(txMeta.id) + assert.deepEqual(txMeta, memTxMeta, `txMeta should be stored in txController after adding it\n expected: ${txMeta} \n got: ${memTxMeta}`) + addTxDefaultsStub.restore() + done() + }).catch(done) + }) + }) + + describe('#addTxDefaults', function () { + it('should add the tx defaults if their are none', function (done) { + let txMeta = { + 'txParams': { + 'from':'0xc684832530fcbddae4b4230a47e991ddcec2831d', + 'to':'0xc684832530fcbddae4b4230a47e991ddcec2831d', + }, + } + + txController.query.stubResult('gasPrice', '0x4a817c800') + txController.query.stubResult('getBlockByNumber', { gasLimit: '0x47b784' }) + txController.query.stubResult('estimateGas', '0x5209') + + txController.addTxDefaults(txMeta) + .then((txMetaWithDefaults) => { + assert(txMetaWithDefaults.txParams.value, '0x0','should have added 0x0 as the value') + assert(txMetaWithDefaults.txParams.gasPrice, 'should have added the gas price') + assert(txMetaWithDefaults.txParams.gas, 'should have added the gas field') + done() + }) + .catch(done) + }) }) describe('#validateTxParams', function () { - it('returns null for positive values', function () { + it('does not throw for positive values', function (done) { var sample = { value: '0x01', } - txController.txProviderUtils.validateTxParams(sample, (err) => { - assert.equal(err, null, 'no error') - }) + txController.txProviderUtils.validateTxParams(sample).then(() => { + done() + }).catch(done) }) - it('returns error for negative values', function () { + it('returns error for negative values', function (done) { var sample = { value: '-0x01', } - txController.txProviderUtils.validateTxParams(sample, (err) => { + txController.txProviderUtils.validateTxParams(sample) + .then(() => done('expected to thrown on negativity values but didn\'t')) + .catch((err) => { assert.ok(err, 'error') + done() }) }) }) @@ -56,9 +178,6 @@ describe('Transaction Controller', function () { assert.ok(Array.isArray(result)) assert.equal(result.length, 0) }) - it('should also return transactions from local storage if any', function () { - - }) }) describe('#addTx', function () { @@ -276,16 +395,15 @@ describe('Transaction Controller', function () { txController.addTx(txMeta) - const estimateStub = sinon.stub(txController.txProviderUtils.query, 'estimateGas') - .callsArgWithAsync(1, null, wrongValue) + txController.query.stubResult('estimateGas', wrongValue) + txController.query.stubResult('gasPrice', wrongValue) - const priceStub = sinon.stub(txController.txProviderUtils.query, 'gasPrice') - .callsArgWithAsync(0, null, wrongValue) + const signStub = sinon.stub(txController, 'signTransaction').callsFake(() => Promise.resolve()) - - const signStub = sinon.stub(txController, 'signTransaction', () => Promise.resolve()) - - const pubStub = sinon.stub(txController.txProviderUtils, 'publishTransaction', () => Promise.resolve(originalValue)) + const pubStub = sinon.stub(txController, 'publishTransaction').callsFake(() => { + txController.setTxHash('1', originalValue) + txController.setTxStatusSubmitted('1') + }) txController.approveTransaction(txMeta.id).then(() => { const result = txController.getTx(txMeta.id) @@ -294,9 +412,6 @@ describe('Transaction Controller', function () { assert.equal(params.gas, originalValue, 'gas unmodified') assert.equal(params.gasPrice, originalValue, 'gas price unmodified') assert.equal(result.hash, originalValue, `hash was set \n got: ${result.hash} \n expected: ${originalValue}`) - - estimateStub.restore() - priceStub.restore() signStub.restore() pubStub.restore() done() @@ -352,9 +467,8 @@ describe('Transaction Controller', function () { }) .catch((err) => { assert.ifError(err, 'should not throw an error') - done() + done(err) }) }) }) -}) - +})
\ No newline at end of file diff --git a/ui/app/actions.js b/ui/app/actions.js index d3d6c165e..13a767343 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -467,9 +467,12 @@ function cancelPersonalMsg (msgData) { } function cancelTx (txData) { - log.debug(`background.cancelTransaction`) - background.cancelTransaction(txData.id) - return actions.completedTx(txData.id) + return (dispatch) => { + log.debug(`background.cancelTransaction`) + background.cancelTransaction(txData.id, () => { + dispatch(actions.completedTx(txData.id)) + }) + } } // |