diff --git a/.eslintrc b/.eslintrc index a57a93cdc..2eb0dc8b5 100644 --- a/.eslintrc +++ b/.eslintrc @@ -50,7 +50,7 @@ "eqeqeq": [2, "allow-null"], "generator-star-spacing": [2, { "before": true, "after": true }], "handle-callback-err": [1, "^(err|error)$" ], - "indent": [2, 2, { "SwitchCase": 1 }], + "indent": "off", "jsx-quotes": [2, "prefer-single"], "key-spacing": 1, "keyword-spacing": [2, { "before": true, "after": true }], @@ -133,7 +133,7 @@ "no-with": 2, "one-var": [2, { "initialized": "never" }], "operator-linebreak": [1, "after", { "overrides": { "?": "ignore", ":": "ignore" } }], - "padded-blocks": [1, "never"], + "padded-blocks": "off", "quotes": [2, "single", {"avoidEscape": true, "allowTemplateLiterals": true}], "semi": [2, "never"], "semi-spacing": [2, { "before": false, "after": true }], diff --git a/README.md b/README.md index 9aded09c1..e39058790 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# MetaMask Plugin [![Build Status](https://circleci.com/gh/MetaMask/metamask-extension.svg?style=shield&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-extension) [![Coverage Status](https://coveralls.io/repos/github/MetaMask/metamask-extension/badge.svg?branch=master)](https://coveralls.io/github/MetaMask/metamask-extension?branch=master) +# MetaMask Plugin [![Build Status](https://circleci.com/gh/MetaMask/metamask-extension.svg?style=shield&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-extension) [![Coverage Status](https://coveralls.io/repos/github/MetaMask/metamask-extension/badge.svg?branch=master)](https://coveralls.io/github/MetaMask/metamask-extension?branch=master) [![Greenkeeper badge](https://badges.greenkeeper.io/MetaMask/metamask-extension.svg)](https://greenkeeper.io/) + ## Support diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 6fde0edcd..acacf5d4c 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -86,7 +86,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 } diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 308d43cb0..58c468e22 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -5,6 +5,7 @@ const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') const EthQuery = require('ethjs-query') const TxProviderUtil = require('../lib/tx-utils') +const PendingTransactionTracker = require('../lib/pending-tx-tracker') const createId = require('../lib/random-id') const NonceTracker = require('../lib/nonce-tracker') @@ -20,6 +21,9 @@ module.exports = class TransactionController extends EventEmitter { this.txHistoryLimit = opts.txHistoryLimit this.provider = opts.provider this.blockTracker = opts.blockTracker + this.signEthTx = opts.signTransaction + this.ethStore = opts.ethStore + this.nonceTracker = new NonceTracker({ provider: this.provider, getPendingTransactions: (address) => { @@ -31,15 +35,38 @@ module.exports = class TransactionController extends EventEmitter { }, }) this.query = new EthQuery(this.provider) - this.txProviderUtils = new TxProviderUtil(this.query) - this.blockTracker.on('rawBlock', this.checkForTxInBlock.bind(this)) + this.txProviderUtil = new TxProviderUtil(this.provider) + + this.pendingTxTracker = new PendingTransactionTracker({ + provider: this.provider, + nonceTracker: this.nonceTracker, + getBalance: (address) => { + const account = this.ethStore.getState().accounts[address] + if (!account) return + return account.balance + }, + publishTransaction: this.txProviderUtil.publishTransaction.bind(this.txProviderUtil), + getPendingTransactions: () => { + const network = this.getNetwork() + return this.getFilteredTxList({ + status: 'submitted', + metamaskNetworkId: network, + }) + }, + }) + + this.pendingTxTracker.on('txWarning', this.updateTx.bind(this)) + this.pendingTxTracker.on('txFailed', this.setTxStatusFailed.bind(this)) + this.pendingTxTracker.on('txConfirmed', this.setTxStatusConfirmed.bind(this)) + + this.blockTracker.on('rawBlock', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker)) // this is a little messy but until ethstore has been either // removed or redone this is to guard against the race condition // where ethStore hasent been populated by the results yet - this.blockTracker.once('latest', () => this.blockTracker.on('latest', this.resubmitPendingTxs.bind(this))) - this.blockTracker.on('sync', this.queryPendingTxs.bind(this)) - this.signEthTx = opts.signTransaction - this.ethStore = opts.ethStore + this.blockTracker.once('latest', () => { + this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.bind(this.pendingTxTracker)) + }) + this.blockTracker.on('sync', this.pendingTxTracker.queryPendingTxs.bind(this.pendingTxTracker)) // memstore is computed from a few different stores this._updateMemstore() this.store.subscribe(() => this._updateMemstore()) @@ -171,7 +198,7 @@ module.exports = class TransactionController extends EventEmitter { async addUnapprovedTransaction (txParams) { // validate - await this.txProviderUtils.validateTxParams(txParams) + await this.txProviderUtil.validateTxParams(txParams) // construct txMeta const txMeta = { id: createId(), @@ -197,7 +224,7 @@ module.exports = class TransactionController extends EventEmitter { txParams.gasPrice = gasPrice } // set gasLimit - return await this.txProviderUtils.analyzeGasUsage(txMeta) + return await this.txProviderUtil.analyzeGasUsage(txMeta) } async updateAndApproveTransaction (txMeta) { @@ -226,11 +253,7 @@ module.exports = class TransactionController extends EventEmitter { // must set transaction to submitted/failed before releasing lock nonceLock.releaseLock() } catch (err) { - this.setTxStatusFailed(txId, { - stack: err.stack || err.message, - errCode: err.errCode || err, - message: err.message || 'Transaction failed during approval', - }) + this.setTxStatusFailed(txId, err) // must set transaction to submitted/failed before releasing lock if (nonceLock) nonceLock.releaseLock() // continue with error chain @@ -244,7 +267,7 @@ module.exports = class TransactionController extends EventEmitter { const fromAddress = txParams.from // add network/chain id txParams.chainId = this.getChainId() - const ethTx = this.txProviderUtils.buildEthTxFromParams(txParams) + const ethTx = this.txProviderUtil.buildEthTxFromParams(txParams) await this.signEthTx(ethTx, fromAddress) this.setTxStatusSigned(txMeta.id) const rawTx = ethUtil.bufferToHex(ethTx.serialize()) @@ -255,7 +278,7 @@ module.exports = class TransactionController extends EventEmitter { const txMeta = this.getTx(txId) txMeta.rawTx = rawTx this.updateTx(txMeta) - const txHash = await this.txProviderUtils.publishTransaction(rawTx) + const txHash = await this.txProviderUtil.publishTransaction(rawTx) this.setTxHash(txId, txHash) this.setTxStatusSubmitted(txId) } @@ -359,9 +382,12 @@ module.exports = class TransactionController extends EventEmitter { this._setTxStatus(txId, 'confirmed') } - setTxStatusFailed (txId, reason) { + setTxStatusFailed (txId, err) { const txMeta = this.getTx(txId) - txMeta.err = reason + txMeta.err = { + message: err.toString(), + stack: err.stack, + } this.updateTx(txMeta) this._setTxStatus(txId, 'failed') } @@ -374,73 +400,6 @@ module.exports = class TransactionController extends EventEmitter { this.updateTx(txMeta) } - // checks if a signed tx is in a block and - // if included sets the tx status as 'confirmed' - checkForTxInBlock (block) { - const signedTxList = this.getFilteredTxList({status: 'submitted'}) - if (!signedTxList.length) return - signedTxList.forEach((txMeta) => { - const txHash = txMeta.hash - const txId = txMeta.id - - if (!txHash) { - 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) - }) - }) - } - - queryPendingTxs ({oldBlock, newBlock}) { - // check pending transactions on start - if (!oldBlock) { - this._checkPendingTxs() - return - } - // if we synced by more than one block, check for missed pending transactions - const diff = Number.parseInt(newBlock.number) - Number.parseInt(oldBlock.number) - if (diff > 1) this._checkPendingTxs() - } - - 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 | @@ -482,75 +441,4 @@ module.exports = class TransactionController extends EventEmitter { }) this.memStore.updateState({ unapprovedTxs, selectedAddressTxList }) } - - async _resubmitTx (txMeta) { - const address = txMeta.txParams.from - const balance = this.ethStore.getState().accounts[address].balance - if (!('retryCount' in txMeta)) txMeta.retryCount = 0 - - // 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, { - stack: '_resubmitTx: custom tx-controller error', - message, - }) - log.error(message) - return - } - - // Only auto-submit already-signed txs: - if (!('rawTx' in txMeta)) return - - // Increment a try counter. - txMeta.retryCount++ - const rawTx = txMeta.rawTx - return await this.txProviderUtils.publishTransaction(rawTx) - } - - // checks the network for signed txs and - // if confirmed sets the tx status as 'confirmed' - async _checkPendingTxs () { - const signedTxList = this.getFilteredTxList({status: 'submitted'}) - // in order to keep the nonceTracker accurate we block it while updating pending transactions - const nonceGlobalLock = await this.nonceTracker.getGlobalLock() - try { - await Promise.all(signedTxList.map((txMeta) => this._checkPendingTx(txMeta))) - } catch (err) { - console.error('TransactionController - Error updating pending transactions') - console.error(err) - } - nonceGlobalLock.releaseLock() - } - - async _checkPendingTx (txMeta) { - const txHash = txMeta.hash - const txId = txMeta.id - // extra check in case there was an uncaught error during the - // signature and submission process - if (!txHash) { - 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 this.query.getTransactionByHash(txHash) - if (!txParams) return - if (txParams.blockNumber) { - this.setTxStatusConfirmed(txId) - } - } catch (err) { - if (err || !txParams) { - txMeta.err = { - isWarning: true, - errorCode: err, - message: 'There was a problem loading this transaction.', - } - this.updateTx(txMeta) - throw err - } - } - } } \ No newline at end of file diff --git a/app/scripts/lib/pending-tx-tracker.js b/app/scripts/lib/pending-tx-tracker.js new file mode 100644 index 000000000..19720db3f --- /dev/null +++ b/app/scripts/lib/pending-tx-tracker.js @@ -0,0 +1,163 @@ +const EventEmitter = require('events') +const EthQuery = require('ethjs-query') +const sufficientBalance = require('./util').sufficientBalance +/* + + Utility class for tracking the transactions as they + go from a pending state to a confirmed (mined in a block) state + + As well as continues broadcast while in the pending state + + ~config is not optional~ + requires a: { + provider: //, + nonceTracker: //see nonce tracker, + getBalnce: //(address) a function for getting balances, + getPendingTransactions: //() a function for getting an array of transactions, + publishTransaction: //(rawTx) a async function for publishing raw transactions, + } + +*/ + +module.exports = class PendingTransactionTracker extends EventEmitter { + constructor (config) { + super() + this.query = new EthQuery(config.provider) + this.nonceTracker = config.nonceTracker + + this.getBalance = config.getBalance + this.getPendingTransactions = config.getPendingTransactions + this.publishTransaction = config.publishTransaction + } + + // checks if a signed tx is in a block and + // if included sets the tx status as 'confirmed' + checkForTxInBlock (block) { + const signedTxList = this.getPendingTransactions() + if (!signedTxList.length) return + signedTxList.forEach((txMeta) => { + const txHash = txMeta.hash + const txId = txMeta.id + + if (!txHash) { + const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.') + noTxHashErr.name = 'NoTxHashError' + this.emit('txFailed', txId, noTxHashErr) + return + } + + + block.transactions.forEach((tx) => { + if (tx.hash === txHash) this.emit('txConfirmed', txId) + }) + }) + } + + queryPendingTxs ({oldBlock, newBlock}) { + // check pending transactions on start + if (!oldBlock) { + this._checkPendingTxs() + return + } + // if we synced by more than one block, check for missed pending transactions + const diff = Number.parseInt(newBlock.number, 16) - Number.parseInt(oldBlock.number, 16) + if (diff > 1) this._checkPendingTxs() + } + + + resubmitPendingTxs () { + const pending = this.getPendingTransactions() + // 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.emit('txFailed', txMeta.id, err) + })) + } + + async _resubmitTx (txMeta) { + const address = txMeta.txParams.from + const balance = this.getBalance(address) + if (balance === undefined) return + if (!('retryCount' in txMeta)) txMeta.retryCount = 0 + + // if the value of the transaction is greater then the balance, fail. + if (!sufficientBalance(txMeta.txParams, balance)) { + const insufficientFundsError = new Error('Insufficient balance during rebroadcast.') + this.emit('txFailed', txMeta.id, insufficientFundsError) + log.error(insufficientFundsError) + return + } + + // Only auto-submit already-signed txs: + if (!('rawTx' in txMeta)) return + + // Increment a try counter. + txMeta.retryCount++ + const rawTx = txMeta.rawTx + return await this.publishTransaction(rawTx) + } + + async _checkPendingTx (txMeta) { + const txHash = txMeta.hash + const txId = txMeta.id + // extra check in case there was an uncaught error during the + // signature and submission process + if (!txHash) { + const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.') + noTxHashErr.name = 'NoTxHashError' + this.emit('txFailed', txId, noTxHashErr) + return + } + // get latest transaction status + let txParams + try { + txParams = await this.query.getTransactionByHash(txHash) + if (!txParams) return + if (txParams.blockNumber) { + this.emit('txConfirmed', txId) + } + } catch (err) { + txMeta.warning = { + error: err, + message: 'There was a problem loading this transaction.', + } + this.emit('txWarning', txMeta) + throw err + } + } + + // checks the network for signed txs and + // if confirmed sets the tx status as 'confirmed' + async _checkPendingTxs () { + const signedTxList = this.getPendingTransactions() + // in order to keep the nonceTracker accurate we block it while updating pending transactions + const nonceGlobalLock = await this.nonceTracker.getGlobalLock() + try { + await Promise.all(signedTxList.map((txMeta) => this._checkPendingTx(txMeta))) + } catch (err) { + console.error('PendingTransactionWatcher - Error updating pending transactions') + console.error(err) + } + nonceGlobalLock.releaseLock() + } +} \ No newline at end of file diff --git a/app/scripts/lib/tx-utils.js b/app/scripts/lib/tx-utils.js index 3687a9652..b64ea6712 100644 --- a/app/scripts/lib/tx-utils.js +++ b/app/scripts/lib/tx-utils.js @@ -1,7 +1,11 @@ -const ethUtil = require('ethereumjs-util') +const EthQuery = require('ethjs-query') const Transaction = require('ethereumjs-tx') const normalize = require('eth-sig-util').normalize -const BN = ethUtil.BN +const { + hexToBn, + BnMultiplyByFraction, + bnToHex, +} = require('./util') /* tx-utils are utility methods for Transaction manager @@ -9,9 +13,9 @@ its passed ethquery and used to do things like calculate gas of a tx. */ -module.exports = class txProvideUtils { - constructor (ethQuery) { - this.query = ethQuery +module.exports = class txProvideUtil { + constructor (provider) { + this.query = new EthQuery(provider) } async analyzeGasUsage (txMeta) { @@ -91,31 +95,4 @@ module.exports = class txProvideUtils { throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`) } } - - sufficientBalance (txParams, hexBalance) { - const balance = hexToBn(hexBalance) - const value = hexToBn(txParams.value) - const gasLimit = hexToBn(txParams.gas) - const gasPrice = hexToBn(txParams.gasPrice) - - const maxCost = value.add(gasLimit.mul(gasPrice)) - return balance.gte(maxCost) - } - -} - -// util - -function bnToHex (inputBn) { - return ethUtil.addHexPrefix(inputBn.toString(16)) -} - -function hexToBn (inputHex) { - return new BN(ethUtil.stripHexPrefix(inputHex), 16) -} - -function BnMultiplyByFraction (targetBN, numerator, denominator) { - const numBN = new BN(numerator) - const denomBN = new BN(denominator) - return targetBN.mul(numBN).div(denomBN) -} +} \ No newline at end of file diff --git a/app/scripts/lib/util.js b/app/scripts/lib/util.js index bddd60ee8..6dee9edf0 100644 --- a/app/scripts/lib/util.js +++ b/app/scripts/lib/util.js @@ -1,8 +1,44 @@ +const ethUtil = require('ethereumjs-util') +const assert = require('assert') +const BN = require('bn.js') + module.exports = { getStack, + sufficientBalance, + hexToBn, + bnToHex, + BnMultiplyByFraction, } function getStack () { const stack = new Error('Stack trace generator - not an error').stack return stack } + +function sufficientBalance (txParams, hexBalance) { + // validate hexBalance is a hex string + assert.equal(typeof hexBalance, 'string', 'sufficientBalance - hexBalance is not a hex string') + assert.equal(hexBalance.slice(0, 2), '0x', 'sufficientBalance - hexBalance is not a hex string') + + const balance = hexToBn(hexBalance) + const value = hexToBn(txParams.value) + const gasLimit = hexToBn(txParams.gas) + const gasPrice = hexToBn(txParams.gasPrice) + + const maxCost = value.add(gasLimit.mul(gasPrice)) + return balance.gte(maxCost) +} + +function bnToHex (inputBn) { + return ethUtil.addHexPrefix(inputBn.toString(16)) +} + +function hexToBn (inputHex) { + return new BN(ethUtil.stripHexPrefix(inputHex), 16) +} + +function BnMultiplyByFraction (targetBN, numerator, denominator) { + const numBN = new BN(numerator) + const denomBN = new BN(denominator) + return targetBN.mul(numBN).div(denomBN) +} diff --git a/gulpfile.js b/gulpfile.js index f351c2e2f..a8333e950 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -258,26 +258,27 @@ function zipTask(target) { } } -function generateBundler(opts) { - var browserifyOpts = assign({}, watchify.args, { +function generateBundler(opts, performBundle) { + const browserifyOpts = assign({}, watchify.args, { entries: ['./app/scripts/'+opts.filename], plugin: 'browserify-derequire', debug: debug, fullPaths: debug, }) - return browserify(browserifyOpts) -} - -function discTask(opts) { - let bundler = generateBundler(opts) + let bundler = browserify(browserifyOpts) if (opts.watch) { bundler = watchify(bundler) - // on any dep update, runs the bundler + // on any file update, re-runs the bundler bundler.on('update', performBundle) } + return bundler +} + +function discTask(opts) { + const bundler = generateBundler(opts, performBundle) // output build logs to terminal bundler.on('log', gutil.log) @@ -299,14 +300,7 @@ function discTask(opts) { function bundleTask(opts) { - let bundler = generateBundler(opts) - - if (opts.watch) { - bundler = watchify(bundler) - // on any file update, re-runs the bundler - bundler.on('update', performBundle) - } - + const bundler = generateBundler(opts, performBundle) // output build logs to terminal bundler.on('log', gutil.log) @@ -316,6 +310,17 @@ function bundleTask(opts) { return ( bundler.bundle() + + // handle errors + .on('error', (err) => { + beep() + if (opts.watch) { + console.warn(err.stack) + } else { + throw err + } + }) + // convert bundle stream to gulp vinyl stream .pipe(source(opts.filename)) // inject variables into bundle @@ -324,7 +329,7 @@ function bundleTask(opts) { .pipe(buffer()) // sourcemaps // loads map from browserify file - .pipe(gulpif(debug, sourcemaps.init({loadMaps: true}))) + .pipe(gulpif(debug, sourcemaps.init({ loadMaps: true }))) // writes .map file .pipe(gulpif(debug, sourcemaps.write('./'))) // write completed bundles @@ -338,3 +343,7 @@ function bundleTask(opts) { ) } } + +function beep () { + process.stdout.write('\x07') +} diff --git a/package.json b/package.json index 7dc1222d7..25dbc1ee9 100644 --- a/package.json +++ b/package.json @@ -49,19 +49,20 @@ ] }, "dependencies": { - "async": "^1.5.2", + "async": "^2.5.0", "await-semaphore": "^0.1.1", "babel-runtime": "^6.23.0", "bip39": "^2.2.0", "bluebird": "^3.5.0", "boron": "^0.2.3", + "bn.js": "^4.11.7", "browser-passworder": "^2.0.3", "browserify-derequire": "^0.9.4", "client-sw-ready-event": "^3.3.0", - "clone": "^1.0.2", - "copy-to-clipboard": "^2.0.0", + "clone": "^2.1.1", + "copy-to-clipboard": "^3.0.8", "debounce": "^1.0.0", - "deep-extend": "^0.4.1", + "deep-extend": "^0.5.0", "detect-node": "^2.0.3", "disc": "^1.3.2", "dnode": "^1.2.2", @@ -76,7 +77,7 @@ "eth-simple-keyring": "^1.1.1", "eth-token-tracker": "^1.1.2", "ethereumjs-tx": "^1.3.0", - "ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", + "ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-wallet": "^0.6.0", "ethjs-ens": "^2.0.0", "ethjs-query": "^0.2.6", @@ -85,13 +86,14 @@ "extensionizer": "^1.0.0", "fast-levenshtein": "^2.0.6", "gulp-autoprefixer": "^4.0.0", - "gulp-eslint": "^2.0.0", "gulp-sass": "^3.1.0", + "gulp": "github:gulpjs/gulp#4.0", + "gulp-eslint": "^4.0.0", "hat": "0.0.3", - "idb-global": "^1.0.0", - "identicon.js": "^1.2.1", + "idb-global": "^2.1.0", + "identicon.js": "^2.3.1", "iframe": "^1.0.0", - "iframe-stream": "^1.0.2", + "iframe-stream": "^3.0.0", "inject-css": "^0.1.1", "jazzicon": "^1.2.0", "loglevel": "^1.4.1", @@ -105,7 +107,7 @@ "ping-pong-stream": "^1.0.0", "pojo-migrator": "^2.1.0", "polyfill-crypto.getrandomvalues": "^1.0.0", - "post-message-stream": "^1.0.0", + "post-message-stream": "^3.0.0", "promise-filter": "^1.1.0", "promise-to-callback": "^1.0.0", "pump": "^1.0.2", @@ -114,9 +116,9 @@ "react": "^15.0.2", "react-addons-css-transition-group": "^15.6.0", "react-dom": "^15.5.4", - "react-hyperscript": "^2.2.2", + "react-hyperscript": "^3.0.0", "react-markdown": "^2.3.0", - "react-redux": "^4.4.5", + "react-redux": "^5.0.5", "react-select": "^1.0.0-rc.2", "react-simple-file-input": "^1.0.0", "react-tooltip-component": "^0.3.0", @@ -124,25 +126,24 @@ "reactify": "^1.1.1", "readable-stream": "^2.1.2", "redux": "^3.0.5", - "redux-logger": "^2.10.2", - "redux-thunk": "^1.0.2", + "redux-logger": "^3.0.6", + "redux-thunk": "^2.2.0", "request-promise": "^4.1.1", "sandwich-expando": "^1.0.5", "semaphore": "^1.0.5", "sw-stream": "^2.0.0", "textarea-caret": "^3.0.1", - "three.js": "^0.73.2", "through2": "^2.0.3", "valid-url": "^1.0.9", "vreme": "^3.0.2", - "web3": "0.19.1", + "web3": "^0.20.1", "web3-provider-engine": "^13.2.9", "web3-stream-provider": "^3.0.1", "xtend": "^4.0.1" }, "devDependencies": { "babel-core": "^6.24.1", - "babel-eslint": "^6.0.5", + "babel-eslint": "^7.2.3", "babel-plugin-transform-async-to-generator": "^6.24.1", "babel-plugin-transform-runtime": "^6.23.0", "babel-polyfill": "^6.23.0", @@ -151,50 +152,52 @@ "babelify": "^7.2.0", "beefy": "^2.1.5", "brfs": "^1.4.3", - "browserify": "^13.0.0", - "chai": "^3.5.0", + "browserify": "^14.4.0", + "chai": "^4.1.0", "coveralls": "^2.13.1", "deep-freeze-strict": "^1.1.1", - "del": "^2.2.0", + "del": "^3.0.0", "envify": "^4.0.0", "enzyme": "^2.8.2", "eslint-plugin-chai": "0.0.1", "eslint-plugin-mocha": "^4.9.0", - "fs-promise": "^1.0.0", + "eth-json-rpc-middleware": "^1.2.7", + "fs-promise": "^2.0.3", "gulp": "github:gulpjs/gulp#4.0", "gulp-if": "^2.0.1", "gulp-json-editor": "^2.2.1", "gulp-livereload": "^3.8.1", - "gulp-replace": "^0.5.4", - "gulp-sourcemaps": "^1.6.0", + "gulp-replace": "^0.6.1", + "gulp-sourcemaps": "^2.6.0", "gulp-util": "^3.0.7", "gulp-watch": "^4.3.5", - "gulp-zip": "^3.2.0", + "gulp-zip": "^4.0.0", "isomorphic-fetch": "^2.2.1", - "jsdom": "^8.1.0", - "jsdom-global": "^1.7.0", - "jshint-stylish": "~0.1.5", + "jsdom": "^11.1.0", + "jsdom-global": "^3.0.2", + "jshint-stylish": "~2.2.1", + "json-rpc-engine": "^3.0.1", "lodash.assign": "^4.0.6", - "mocha": "^2.4.5", - "mocha-eslint": "^2.1.1", + "mocha": "^3.4.2", + "mocha-eslint": "^4.0.0", "mocha-jsdom": "^1.1.0", - "mocha-sinon": "^1.1.5", - "nock": "^8.0.0", + "mocha-sinon": "^2.0.0", + "nock": "^9.0.14", "nyc": "^11.0.3", "open": "0.0.5", "prompt": "^1.0.0", "qs": "^6.2.0", - "qunit": "^0.9.1", + "qunit": "^1.0.0", "react-addons-test-utils": "^15.5.1", "react-test-renderer": "^15.5.4", "react-testutils-additions": "^15.2.0", "sinon": "^2.3.8", "tape": "^4.5.1", "testem": "^1.10.3", - "uglifyify": "^3.0.1", + "uglifyify": "^4.0.2", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^1.1.0", - "watchify": "^3.7.0" + "watchify": "^3.9.0" }, "engines": { "node": ">=0.8.0" diff --git a/test/stub/provider.js b/test/stub/provider.js new file mode 100644 index 000000000..8a306f6d9 --- /dev/null +++ b/test/stub/provider.js @@ -0,0 +1,25 @@ +const JsonRpcEngine = require('json-rpc-engine') +const scaffoldMiddleware = require('eth-json-rpc-middleware/scaffold') + +module.exports = { + createEngineForTestData, + providerFromEngine, + scaffoldMiddleware, + createStubedProvider +} + + +function createEngineForTestData () { + return new JsonRpcEngine() +} + +function providerFromEngine (engine) { + const provider = { sendAsync: engine.handle.bind(engine) } + return provider +} + +function createStubedProvider (resultStub) { + const engine = createEngineForTestData() + engine.push(scaffoldMiddleware(resultStub)) + return providerFromEngine(engine) +} \ No newline at end of file diff --git a/test/unit/message-manager-test.js b/test/unit/message-manager-test.js index 30cb4f067..9b76241ed 100644 --- a/test/unit/message-manager-test.js +++ b/test/unit/message-manager-test.js @@ -1,7 +1,7 @@ const assert = require('assert') const MessageManger = require('../../app/scripts/lib/message-manager') -describe('Transaction Manager', function () { +describe('Message Manager', function () { let messageManager beforeEach(function () { diff --git a/test/unit/nonce-tracker-test.js b/test/unit/nonce-tracker-test.js index b0283e159..36025a360 100644 --- a/test/unit/nonce-tracker-test.js +++ b/test/unit/nonce-tracker-test.js @@ -31,12 +31,11 @@ describe('Nonce Tracker', function () { }) describe('#getNonceLock', function () { - it('should work', async function (done) { + it('should work', async function () { this.timeout(15000) const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926') assert.equal(nonceLock.nextNonce, '1', 'nonce should be 1') await nonceLock.releaseLock() - done() }) }) }) diff --git a/test/unit/pending-tx-test.js b/test/unit/pending-tx-test.js new file mode 100644 index 000000000..8c6d287f8 --- /dev/null +++ b/test/unit/pending-tx-test.js @@ -0,0 +1,260 @@ +const assert = require('assert') +const ethUtil = require('ethereumjs-util') +const EthTx = require('ethereumjs-tx') +const ObservableStore = require('obs-store') +const clone = require('clone') +const { createStubedProvider } = require('../stub/provider') +const PendingTransactionTracker = require('../../app/scripts/lib/pending-tx-tracker') +const noop = () => true +const currentNetworkId = 42 +const otherNetworkId = 36 +const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex') + +describe('PendingTransactionTracker', function () { + let pendingTxTracker, txMeta, txMetaNoHash, txMetaNoRawTx, providerResultStub, provider + this.timeout(10000) + beforeEach(function () { + txMeta = { + id: 1, + hash: '0x0593ee121b92e10d63150ad08b4b8f9c7857d1bd160195ee648fb9a0f8d00eeb', + status: 'signed', + txParams: { + from: '0x1678a085c290ebd122dc42cba69373b5953b831d', + nonce: '0x1', + value: '0xfffff', + }, + rawTx: '0xf86c808504a817c800827b0d940c62bb85faa3311a998d3aba8098c1235c564966880de0b6b3a7640000802aa08ff665feb887a25d4099e40e11f0fef93ee9608f404bd3f853dd9e84ed3317a6a02ec9d3d1d6e176d4d2593dd760e74ccac753e6a0ea0d00cc9789d0d7ff1f471d', + } + txMetaNoHash = { + id: 2, + status: 'signed', + txParams: { from: '0x1678a085c290ebd122dc42cba69373b5953b831d'}, + } + txMetaNoRawTx = { + hash: '0x0593ee121b92e10d63150ad08b4b8f9c7857d1bd160195ee648fb9a0f8d00eeb', + status: 'signed', + txParams: { from: '0x1678a085c290ebd122dc42cba69373b5953b831d'}, + } + providerResultStub = {} + provider = createStubedProvider(providerResultStub) + + pendingTxTracker = new PendingTransactionTracker({ + provider, + getBalance: () => {}, + nonceTracker: { + getGlobalLock: async () => { + return { releaseLock: () => {} } + } + }, + getPendingTransactions: () => {return []}, + sufficientBalance: () => {}, + publishTransaction: () => {}, + }) + }) + + describe('#checkForTxInBlock', function () { + it('should return if no pending transactions', function () { + // throw a type error if it trys to do anything on the block + // thus failing the test + const block = Proxy.revocable({}, {}).revoke() + pendingTxTracker.checkForTxInBlock(block) + }) + it('should emit \'txFailed\' if the txMeta does not have a hash', function (done) { + const block = Proxy.revocable({}, {}).revoke() + pendingTxTracker.getPendingTransactions = () => [txMetaNoHash] + pendingTxTracker.once('txFailed', (txId, err) => { + assert(txId, txMetaNoHash.id, 'should pass txId') + done() + }) + pendingTxTracker.checkForTxInBlock(block) + }) + it('should emit \'txConfirmed\' if the tx is in the block', function (done) { + const block = { transactions: [txMeta]} + pendingTxTracker.getPendingTransactions = () => [txMeta] + pendingTxTracker.once('txConfirmed', (txId) => { + assert(txId, txMeta.id, 'should pass txId') + done() + }) + pendingTxTracker.once('txFailed', (_, err) => { done(err) }) + pendingTxTracker.checkForTxInBlock(block) + }) + }) + describe('#queryPendingTxs', function () { + it('should call #_checkPendingTxs if their is no oldBlock', function (done) { + let newBlock, oldBlock + newBlock = { number: '0x01' } + pendingTxTracker._checkPendingTxs = done + pendingTxTracker.queryPendingTxs({oldBlock, newBlock}) + }) + it('should call #_checkPendingTxs if oldBlock and the newBlock have a diff of greater then 1', function (done) { + let newBlock, oldBlock + oldBlock = { number: '0x01' } + newBlock = { number: '0x03' } + pendingTxTracker._checkPendingTxs = done + pendingTxTracker.queryPendingTxs({oldBlock, newBlock}) + }) + it('should not call #_checkPendingTxs if oldBlock and the newBlock have a diff of 1 or less', function (done) { + let newBlock, oldBlock + oldBlock = { number: '0x1' } + newBlock = { number: '0x2' } + pendingTxTracker._checkPendingTxs = () => { + const err = new Error('should not call #_checkPendingTxs if oldBlock and the newBlock have a diff of 1 or less') + done(err) + } + pendingTxTracker.queryPendingTxs({oldBlock, newBlock}) + done() + }) + }) + + describe('#_checkPendingTx', function () { + it('should emit \'txFailed\' if the txMeta does not have a hash', function (done) { + pendingTxTracker.once('txFailed', (txId, err) => { + assert(txId, txMetaNoHash.id, 'should pass txId') + done() + }) + pendingTxTracker._checkPendingTx(txMetaNoHash) + }) + + it('should should return if query does not return txParams', function () { + providerResultStub.eth_getTransactionByHash = null + pendingTxTracker._checkPendingTx(txMeta) + }) + + it('should emit \'txConfirmed\'', function (done) { + providerResultStub.eth_getTransactionByHash = {blockNumber: '0x01'} + pendingTxTracker.once('txConfirmed', (txId) => { + assert(txId, txMeta.id, 'should pass txId') + done() + }) + pendingTxTracker.once('txFailed', (_, err) => { done(err) }) + pendingTxTracker._checkPendingTx(txMeta) + }) + }) + + describe('#_checkPendingTxs', function () { + beforeEach(function () { + const txMeta2 = txMeta3 = txMeta + txMeta2.id = 2 + txMeta3.id = 3 + txList = [txMeta, txMeta2, txMeta3].map((tx) => { + tx.processed = new Promise ((resolve) => { tx.resolve = resolve }) + return tx + }) + }) + + it('should warp all txMeta\'s in #_checkPendingTx', function (done) { + pendingTxTracker.getPendingTransactions = () => txList + pendingTxTracker._checkPendingTx = (tx) => { tx.resolve(tx) } + const list = txList.map + Promise.all(txList.map((tx) => tx.processed)) + .then((txCompletedList) => done()) + .catch(done) + + pendingTxTracker._checkPendingTxs() + }) + }) + + describe('#resubmitPendingTxs', function () { + beforeEach(function () { + const txMeta2 = txMeta3 = txMeta + txList = [txMeta, txMeta2, txMeta3].map((tx) => { + tx.processed = new Promise ((resolve) => { tx.resolve = resolve }) + return tx + }) + }) + + it('should return if no pending transactions', function () { + pendingTxTracker.resubmitPendingTxs() + }) + it('should call #_resubmitTx for all pending tx\'s', function (done) { + pendingTxTracker.getPendingTransactions = () => txList + pendingTxTracker._resubmitTx = async (tx) => { tx.resolve(tx) } + Promise.all(txList.map((tx) => tx.processed)) + .then((txCompletedList) => done()) + .catch(done) + pendingTxTracker.resubmitPendingTxs() + }) + it('should not emit \'txFailed\' if the txMeta throws a known txError', function (done) { + knownErrors =[ + // geth + ' Replacement transaction Underpriced ', + ' known transaction', + // parity + 'Gas price too low to replace ', + ' transaction with the sAme hash was already imported', + // other + ' gateway timeout', + ' noncE too low ', + ] + const enoughForAllErrors = txList.concat(txList) + + pendingTxTracker.on('txFailed', (_, err) => done(err)) + + pendingTxTracker.getPendingTransactions = () => enoughForAllErrors + pendingTxTracker._resubmitTx = async (tx) => { + tx.resolve() + throw new Error(knownErrors.pop()) + } + Promise.all(txList.map((tx) => tx.processed)) + .then((txCompletedList) => done()) + .catch(done) + + pendingTxTracker.resubmitPendingTxs() + }) + it('should emit \'txFailed\' if it encountered a real error', function (done) { + pendingTxTracker.once('txFailed', (id, err) => err.message === 'im some real error' ? txList[id - 1].resolve() : done(err)) + + pendingTxTracker.getPendingTransactions = () => txList + pendingTxTracker._resubmitTx = async (tx) => { throw new TypeError('im some real error') } + Promise.all(txList.map((tx) => tx.processed)) + .then((txCompletedList) => done()) + .catch(done) + + pendingTxTracker.resubmitPendingTxs() + }) + }) + describe('#_resubmitTx with a too-low balance', function () { + it('should return before publishing the transaction because to low of balance', function (done) { + const lowBalance = '0x0' + pendingTxTracker.getBalance = (address) => { + assert.equal(address, txMeta.txParams.from, 'Should pass the address') + return lowBalance + } + pendingTxTracker.publishTransaction = async (rawTx) => { + done(new Error('tried to publish transaction')) + } + + // Stubbing out current account state: + // Adding the fake tx: + pendingTxTracker.once('txFailed', (txId, err) => { + assert(err, 'Should have a error') + done() + }) + pendingTxTracker._resubmitTx(txMeta) + .catch((err) => { + assert.ifError(err, 'should not throw an error') + done(err) + }) + }) + + it('should publishing the transaction', function (done) { + const enoughBalance = '0x100000' + pendingTxTracker.getBalance = (address) => { + assert.equal(address, txMeta.txParams.from, 'Should pass the address') + return enoughBalance + } + pendingTxTracker.publishTransaction = async (rawTx) => { + assert.equal(rawTx, txMeta.rawTx, 'Should pass the rawTx') + } + + // Stubbing out current account state: + // Adding the fake tx: + pendingTxTracker._resubmitTx(txMeta) + .then(() => done()) + .catch((err) => { + assert.ifError(err, 'should not throw an error') + done(err) + }) + }) + }) +}) \ No newline at end of file diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index f290088a1..57d7deccd 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -10,16 +10,20 @@ const noop = () => true const currentNetworkId = 42 const otherNetworkId = 36 const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex') +const { createStubedProvider } = require('../stub/provider') describe('Transaction Controller', function () { - let txController + let txController, engine, provider, providerResultStub beforeEach(function () { + providerResultStub = {} + provider = createStubedProvider(providerResultStub) + txController = new TransactionController({ + provider, networkStore: new ObservableStore(currentNetworkId), txHistoryLimit: 10, blockTracker: { getCurrentBlock: noop, on: noop, once: noop }, - provider: { sendAsync: noop }, ethStore: { getState: noop }, signTransaction: (ethTx) => new Promise((resolve) => { ethTx.sign(privKey) @@ -27,19 +31,7 @@ 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) + txController.txProviderUtils = new TxProvideUtils(txController.provider) }) describe('#newUnapprovedTransaction', function () { @@ -76,7 +68,6 @@ describe('Transaction Controller', function () { 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) @@ -93,7 +84,6 @@ describe('Transaction Controller', function () { it('should reject when finished and status is rejected', function (done) { txController.once('newUnaprovedTx', (txMetaFromEmit) => { setTimeout(() => { - console.log('HELLLO') txController.setTxStatusRejected(txMetaFromEmit.id) }, 10) }) @@ -133,11 +123,9 @@ describe('Transaction Controller', function () { 'to':'0xc684832530fcbddae4b4230a47e991ddcec2831d', }, } - - txController.query.stubResult('gasPrice', '0x4a817c800') - txController.query.stubResult('getBlockByNumber', { gasLimit: '0x47b784' }) - txController.query.stubResult('estimateGas', '0x5209') - + providerResultStub.eth_gasPrice = '4a817c800' + providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' } + providerResultStub.eth_estimateGas = '5209' txController.addTxDefaults(txMeta) .then((txMetaWithDefaults) => { assert(txMetaWithDefaults.txParams.value, '0x0','should have added 0x0 as the value') @@ -394,9 +382,8 @@ describe('Transaction Controller', function () { const wrongValue = '0x05' txController.addTx(txMeta) - - txController.query.stubResult('estimateGas', wrongValue) - txController.query.stubResult('gasPrice', wrongValue) + providerResultStub.eth_gasPrice = wrongValue + providerResultStub.eth_estimateGas = '0x5209' const signStub = sinon.stub(txController, 'signTransaction').callsFake(() => Promise.resolve()) @@ -429,46 +416,4 @@ describe('Transaction Controller', function () { }).catch(done) }) }) - - describe('#_resubmitTx with a too-low balance', function () { - it('should fail the transaction', function (done) { - const from = '0xda0da0' - const txMeta = { - id: 1, - status: 'submitted', - metamaskNetworkId: currentNetworkId, - txParams: { - from, - nonce: '0x1', - value: '0xfffff', - }, - } - - const lowBalance = '0x0' - const fakeStoreState = { accounts: {} } - fakeStoreState.accounts[from] = { - balance: lowBalance, - nonce: '0x0', - } - - // Stubbing out current account state: - const getStateStub = sinon.stub(txController.ethStore, 'getState') - .returns(fakeStoreState) - - // Adding the fake tx: - txController.addTx(clone(txMeta)) - - txController._resubmitTx(txMeta) - .then(() => { - const updatedMeta = txController.getTx(txMeta.id) - assert.notEqual(updatedMeta.status, txMeta.status, 'status changed.') - assert.equal(updatedMeta.status, 'failed', 'tx set to failed.') - done() - }) - .catch((err) => { - assert.ifError(err, 'should not throw an error') - done(err) - }) - }) - }) }) \ No newline at end of file diff --git a/test/unit/tx-utils-test.js b/test/unit/tx-utils-test.js index a43bcfb35..43128b977 100644 --- a/test/unit/tx-utils-test.js +++ b/test/unit/tx-utils-test.js @@ -1,7 +1,7 @@ const assert = require('assert') -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN +const BN = require('bn.js') +const { hexToBn, bnToHex } = require('../../app/scripts/lib/util') const TxUtils = require('../../app/scripts/lib/tx-utils') @@ -16,44 +16,6 @@ describe('txUtils', function () { })) }) - describe('#sufficientBalance', function () { - it('returns true if max tx cost is equal to balance.', function () { - const tx = { - 'value': '0x1', - 'gas': '0x2', - 'gasPrice': '0x3', - } - const balance = '0x8' - - const result = txUtils.sufficientBalance(tx, balance) - assert.ok(result, 'sufficient balance found.') - }) - - it('returns true if max tx cost is less than balance.', function () { - const tx = { - 'value': '0x1', - 'gas': '0x2', - 'gasPrice': '0x3', - } - const balance = '0x9' - - const result = txUtils.sufficientBalance(tx, balance) - assert.ok(result, 'sufficient balance found.') - }) - - it('returns false if max tx cost is more than balance.', function () { - const tx = { - 'value': '0x1', - 'gas': '0x2', - 'gasPrice': '0x3', - } - const balance = '0x6' - - const result = txUtils.sufficientBalance(tx, balance) - assert.ok(!result, 'insufficient balance found.') - }) - }) - describe('chain Id', function () { it('prepares a transaction with the provided chainId', function () { const txParams = { @@ -96,7 +58,7 @@ describe('txUtils', function () { assert(outputBn.eq(expectedBn), 'returns the original estimatedGas value') }) - it('buffers up to reccomend gas limit reccomended ceiling', function () { + it('buffers up to recommend gas limit recommended ceiling', function () { // naive estimatedGas: 0x16e360 (1.5 mil) const inputHex = '0x16e360' // dummy gas limit: 0x1e8480 (2 mil) @@ -107,17 +69,7 @@ describe('txUtils', function () { // const inputBn = hexToBn(inputHex) // const outputBn = hexToBn(output) const expectedHex = bnToHex(ceilGasLimitBn) - assert.equal(output, expectedHex, 'returns the gas limit reccomended ceiling value') + assert.equal(output, expectedHex, 'returns the gas limit recommended ceiling value') }) }) -}) - -// util - -function hexToBn (inputHex) { - return new BN(ethUtil.stripHexPrefix(inputHex), 16) -} - -function bnToHex (inputBn) { - return ethUtil.addHexPrefix(inputBn.toString(16)) -} +}) \ No newline at end of file diff --git a/test/unit/util-test.js b/test/unit/util-test.js new file mode 100644 index 000000000..6da185b2c --- /dev/null +++ b/test/unit/util-test.js @@ -0,0 +1,41 @@ +const assert = require('assert') +const { sufficientBalance } = require('../../app/scripts/lib/util') + + +describe('SufficientBalance', function () { + it('returns true if max tx cost is equal to balance.', function () { + const tx = { + 'value': '0x1', + 'gas': '0x2', + 'gasPrice': '0x3', + } + const balance = '0x8' + + const result = sufficientBalance(tx, balance) + assert.ok(result, 'sufficient balance found.') + }) + + it('returns true if max tx cost is less than balance.', function () { + const tx = { + 'value': '0x1', + 'gas': '0x2', + 'gasPrice': '0x3', + } + const balance = '0x9' + + const result = sufficientBalance(tx, balance) + assert.ok(result, 'sufficient balance found.') + }) + + it('returns false if max tx cost is more than balance.', function () { + const tx = { + 'value': '0x1', + 'gas': '0x2', + 'gasPrice': '0x3', + } + const balance = '0x6' + + const result = sufficientBalance(tx, balance) + assert.ok(!result, 'insufficient balance found.') + }) +}) \ No newline at end of file diff --git a/ui/app/components/buy-button-subview.js b/ui/app/components/buy-button-subview.js index 87084f92d..759eb15bd 100644 --- a/ui/app/components/buy-button-subview.js +++ b/ui/app/components/buy-button-subview.js @@ -143,7 +143,7 @@ BuyButtonSubview.prototype.formVersionSubview = function () { return h('div.flex-column', { style: { alignItems: 'center', - margin: '50px', + margin: '20px 50px', }, }, [ h('h3.text-transform-uppercase', { diff --git a/ui/app/components/shapeshift-form.js b/ui/app/components/shapeshift-form.js index 76a265d63..901a4a956 100644 --- a/ui/app/components/shapeshift-form.js +++ b/ui/app/components/shapeshift-form.js @@ -33,10 +33,10 @@ ShapeshiftForm.prototype.renderMain = function () { return h('.flex-column', { style: { - // marginTop: '10px', + position: 'relative', padding: '25px', paddingTop: '5px', - width: '100%', + width: '90%', minHeight: '215px', alignItems: 'center', overflowY: 'auto', @@ -58,7 +58,9 @@ ShapeshiftForm.prototype.renderMain = function () { }, }), - h('.input-container', [ + h('.input-container', { + position: 'relative', + }, [ h('input#fromCoin.buy-inputs.ex-coins', { type: 'text', list: 'coinList', @@ -79,27 +81,31 @@ ShapeshiftForm.prototype.renderMain = function () { style: { fontSize: '12px', color: '#F7861C', - position: 'relative', - bottom: '48px', - left: '106px', + position: 'absolute', }, }), ]), - h('.icon-control', [ - h('i.fa.fa-refresh.fa-4.orange', { - style: { - bottom: '5px', - left: '5px', - color: '#F7861C', - }, - onClick: this.updateCoin.bind(this), - }), + h('.icon-control', { + style: { + position: 'relative', + }, + }, [ + // Not visible on the screen, can't see it on master. + + // h('i.fa.fa-refresh.fa-4.orange', { + // style: { + // bottom: '5px', + // left: '5px', + // color: '#F7861C', + // }, + // onClick: this.updateCoin.bind(this), + // }), h('i.fa.fa-chevron-right.fa-4.orange', { style: { - position: 'relative', - bottom: '26px', - left: '10px', + position: 'absolute', + bottom: '35%', + left: '0%', color: '#F7861C', }, onClick: this.updateCoin.bind(this), @@ -117,69 +123,73 @@ ShapeshiftForm.prototype.renderMain = function () { }, }), ]), + h('.flex-column', { style: { + marginTop: '1%', alignItems: 'flex-start', }, }, [ - this.props.warning ? this.props.warning && h('span.error.flex-center', { - style: { - textAlign: 'center', - width: '229px', - height: '82px', - }, - }, - this.props.warning) : this.renderInfo(), + this.props.warning + ? this.props.warning + && h('span.error.flex-center', { + style: { + textAlign: 'center', + width: '229px', + height: '82px', + }, + }, this.props.warning) + : this.renderInfo(), + + this.renderRefundAddressForCoin(coin), ]), - h(this.activeToggle('.input-container'), { + ]) +} + +ShapeshiftForm.prototype.renderRefundAddressForCoin = function (coin) { + return h(this.activeToggle('.input-container'), { + style: { + marginTop: '1%', + }, + }, [ + + h('div', `${coin} Address:`), + + h('input#fromCoinAddress.buy-inputs', { + type: 'text', + placeholder: `Your ${coin} Refund Address`, + dataset: { + persistentFormId: 'refund-address', + + }, style: { - padding: '10px', - paddingTop: '0px', - width: '100%', + boxSizing: 'border-box', + width: '227px', + height: '30px', + padding: ' 5px ', + }, + }), + + h('i.fa.fa-pencil-square-o.edit-text', { + style: { + fontSize: '12px', + color: '#F7861C', + position: 'absolute', + }, + }), + h('div.flex-row', { + style: { + justifyContent: 'flex-start', }, }, [ - - h('div', `${coin} Address:`), - - h('input#fromCoinAddress.buy-inputs', { - type: 'text', - placeholder: `Your ${coin} Refund Address`, - dataset: { - persistentFormId: 'refund-address', - }, + h('button', { + onClick: this.shift.bind(this), style: { - boxSizing: 'border-box', - width: '227px', - height: '30px', - padding: ' 5px ', + marginTop: '1%', }, - }), - - h('i.fa.fa-pencil-square-o.edit-text', { - style: { - fontSize: '12px', - color: '#F7861C', - position: 'relative', - bottom: '10px', - right: '11px', - }, - }), - h('.flex-row', { - style: { - justifyContent: 'flex-end', - }, - }, [ - h('button', { - onClick: this.shift.bind(this), - style: { - marginTop: '10px', - position: 'relative', - bottom: '40px', - }, - }, - 'Submit'), - ]), + }, + 'Submit'), ]), ]) } diff --git a/ui/app/components/shift-list-item.js b/ui/app/components/shift-list-item.js index 32bfbeda4..079f05e31 100644 --- a/ui/app/components/shift-list-item.js +++ b/ui/app/components/shift-list-item.js @@ -2,7 +2,7 @@ const inherits = require('util').inherits const Component = require('react').Component const h = require('react-hyperscript') const connect = require('react-redux').connect -const vreme = new (require('vreme')) +const vreme = new (require('vreme'))() const explorerLink = require('../../lib/explorer-link') const actions = require('../actions') const addressSummary = require('../util').addressSummary diff --git a/ui/app/components/transaction-list-item.js b/ui/app/components/transaction-list-item.js index dbda66a31..9018bab06 100644 --- a/ui/app/components/transaction-list-item.js +++ b/ui/app/components/transaction-list-item.js @@ -6,7 +6,7 @@ const EthBalance = require('./eth-balance') const addressSummary = require('../util').addressSummary const explorerLink = require('../../lib/explorer-link') const CopyButton = require('./copyButton') -const vreme = new (require('vreme')) +const vreme = new (require('vreme'))() const Tooltip = require('./tooltip') const numberToBN = require('number-to-bn') @@ -154,12 +154,21 @@ function failIfFailed (transaction) { if (transaction.status === 'rejected') { return h('span.error', ' (Rejected)') } - if (transaction.err) { + if (transaction.err || transaction.warning) { + const { err, warning = {} } = transaction + const errFirst = !!(( err && warning ) || err) + const message = errFirst ? err.message : warning.message + + errFirst ? err.message : warning.message + + return h(Tooltip, { - title: transaction.err.message, + title: message, position: 'bottom', }, [ - h('span.error', ' (Failed)'), + h(`span.${errFirst ? 'error' : 'warning'}`, + ` (${errFirst ? 'Failed' : 'Warning'})` + ), ]) } } diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index ac8cbf158..878852bf6 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -42,9 +42,12 @@ function reduceApp (state, action) { accountDetail: { subview: 'transactions', }, - transForward: true, // Used to render transition direction - isLoading: false, // Used to display loading indicator - warning: null, // Used to display error text + // Used to render transition direction + transForward: true, + // Used to display loading indicator + isLoading: false, + // Used to display error text + warning: null, }, state.appState) switch (action.type) { diff --git a/ui/app/store.js b/ui/app/store.js index ba9e58b49..3bafdee11 100644 --- a/ui/app/store.js +++ b/ui/app/store.js @@ -1,8 +1,8 @@ const createStore = require('redux').createStore const applyMiddleware = require('redux').applyMiddleware -const thunkMiddleware = require('redux-thunk') +const thunkMiddleware = require('redux-thunk').default const rootReducer = require('./reducers') -const createLogger = require('redux-logger') +const createLogger = require('redux-logger').createLogger global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'