Merge branch 'master' into NewUI-flat

This commit is contained in:
kumavis 2017-08-08 23:30:58 -07:00 committed by GitHub
commit 0188e7b94d
22 changed files with 767 additions and 446 deletions

View File

@ -50,7 +50,7 @@
"eqeqeq": [2, "allow-null"], "eqeqeq": [2, "allow-null"],
"generator-star-spacing": [2, { "before": true, "after": true }], "generator-star-spacing": [2, { "before": true, "after": true }],
"handle-callback-err": [1, "^(err|error)$" ], "handle-callback-err": [1, "^(err|error)$" ],
"indent": [2, 2, { "SwitchCase": 1 }], "indent": "off",
"jsx-quotes": [2, "prefer-single"], "jsx-quotes": [2, "prefer-single"],
"key-spacing": 1, "key-spacing": 1,
"keyword-spacing": [2, { "before": true, "after": true }], "keyword-spacing": [2, { "before": true, "after": true }],
@ -133,7 +133,7 @@
"no-with": 2, "no-with": 2,
"one-var": [2, { "initialized": "never" }], "one-var": [2, { "initialized": "never" }],
"operator-linebreak": [1, "after", { "overrides": { "?": "ignore", ":": "ignore" } }], "operator-linebreak": [1, "after", { "overrides": { "?": "ignore", ":": "ignore" } }],
"padded-blocks": [1, "never"], "padded-blocks": "off",
"quotes": [2, "single", {"avoidEscape": true, "allowTemplateLiterals": true}], "quotes": [2, "single", {"avoidEscape": true, "allowTemplateLiterals": true}],
"semi": [2, "never"], "semi": [2, "never"],
"semi-spacing": [2, { "before": false, "after": true }], "semi-spacing": [2, { "before": false, "after": true }],

View File

@ -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 ## Support

View File

@ -86,7 +86,7 @@ function suffixCheck () {
var currentUrl = window.location.href var currentUrl = window.location.href
var currentRegex var currentRegex
for (let i = 0; i < prohibitedTypes.length; i++) { for (let i = 0; i < prohibitedTypes.length; i++) {
currentRegex = new RegExp(`\.${prohibitedTypes[i]}$`) currentRegex = new RegExp(`\\.${prohibitedTypes[i]}$`)
if (currentRegex.test(currentUrl)) { if (currentRegex.test(currentUrl)) {
return false return false
} }

View File

@ -5,6 +5,7 @@ const ObservableStore = require('obs-store')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const EthQuery = require('ethjs-query') const EthQuery = require('ethjs-query')
const TxProviderUtil = require('../lib/tx-utils') const TxProviderUtil = require('../lib/tx-utils')
const PendingTransactionTracker = require('../lib/pending-tx-tracker')
const createId = require('../lib/random-id') const createId = require('../lib/random-id')
const NonceTracker = require('../lib/nonce-tracker') const NonceTracker = require('../lib/nonce-tracker')
@ -20,6 +21,9 @@ module.exports = class TransactionController extends EventEmitter {
this.txHistoryLimit = opts.txHistoryLimit this.txHistoryLimit = opts.txHistoryLimit
this.provider = opts.provider this.provider = opts.provider
this.blockTracker = opts.blockTracker this.blockTracker = opts.blockTracker
this.signEthTx = opts.signTransaction
this.ethStore = opts.ethStore
this.nonceTracker = new NonceTracker({ this.nonceTracker = new NonceTracker({
provider: this.provider, provider: this.provider,
getPendingTransactions: (address) => { getPendingTransactions: (address) => {
@ -31,15 +35,38 @@ module.exports = class TransactionController extends EventEmitter {
}, },
}) })
this.query = new EthQuery(this.provider) this.query = new EthQuery(this.provider)
this.txProviderUtils = new TxProviderUtil(this.query) this.txProviderUtil = new TxProviderUtil(this.provider)
this.blockTracker.on('rawBlock', this.checkForTxInBlock.bind(this))
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 // this is a little messy but until ethstore has been either
// removed or redone this is to guard against the race condition // removed or redone this is to guard against the race condition
// where ethStore hasent been populated by the results yet // where ethStore hasent been populated by the results yet
this.blockTracker.once('latest', () => this.blockTracker.on('latest', this.resubmitPendingTxs.bind(this))) this.blockTracker.once('latest', () => {
this.blockTracker.on('sync', this.queryPendingTxs.bind(this)) this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.bind(this.pendingTxTracker))
this.signEthTx = opts.signTransaction })
this.ethStore = opts.ethStore this.blockTracker.on('sync', this.pendingTxTracker.queryPendingTxs.bind(this.pendingTxTracker))
// memstore is computed from a few different stores // memstore is computed from a few different stores
this._updateMemstore() this._updateMemstore()
this.store.subscribe(() => this._updateMemstore()) this.store.subscribe(() => this._updateMemstore())
@ -171,7 +198,7 @@ module.exports = class TransactionController extends EventEmitter {
async addUnapprovedTransaction (txParams) { async addUnapprovedTransaction (txParams) {
// validate // validate
await this.txProviderUtils.validateTxParams(txParams) await this.txProviderUtil.validateTxParams(txParams)
// construct txMeta // construct txMeta
const txMeta = { const txMeta = {
id: createId(), id: createId(),
@ -197,7 +224,7 @@ module.exports = class TransactionController extends EventEmitter {
txParams.gasPrice = gasPrice txParams.gasPrice = gasPrice
} }
// set gasLimit // set gasLimit
return await this.txProviderUtils.analyzeGasUsage(txMeta) return await this.txProviderUtil.analyzeGasUsage(txMeta)
} }
async updateAndApproveTransaction (txMeta) { async updateAndApproveTransaction (txMeta) {
@ -226,11 +253,7 @@ module.exports = class TransactionController extends EventEmitter {
// must set transaction to submitted/failed before releasing lock // must set transaction to submitted/failed before releasing lock
nonceLock.releaseLock() nonceLock.releaseLock()
} catch (err) { } catch (err) {
this.setTxStatusFailed(txId, { this.setTxStatusFailed(txId, err)
stack: err.stack || err.message,
errCode: err.errCode || err,
message: err.message || 'Transaction failed during approval',
})
// must set transaction to submitted/failed before releasing lock // must set transaction to submitted/failed before releasing lock
if (nonceLock) nonceLock.releaseLock() if (nonceLock) nonceLock.releaseLock()
// continue with error chain // continue with error chain
@ -244,7 +267,7 @@ module.exports = class TransactionController extends EventEmitter {
const fromAddress = txParams.from const fromAddress = txParams.from
// add network/chain id // add network/chain id
txParams.chainId = this.getChainId() txParams.chainId = this.getChainId()
const ethTx = this.txProviderUtils.buildEthTxFromParams(txParams) const ethTx = this.txProviderUtil.buildEthTxFromParams(txParams)
await this.signEthTx(ethTx, fromAddress) await this.signEthTx(ethTx, fromAddress)
this.setTxStatusSigned(txMeta.id) this.setTxStatusSigned(txMeta.id)
const rawTx = ethUtil.bufferToHex(ethTx.serialize()) const rawTx = ethUtil.bufferToHex(ethTx.serialize())
@ -255,7 +278,7 @@ module.exports = class TransactionController extends EventEmitter {
const txMeta = this.getTx(txId) const txMeta = this.getTx(txId)
txMeta.rawTx = rawTx txMeta.rawTx = rawTx
this.updateTx(txMeta) this.updateTx(txMeta)
const txHash = await this.txProviderUtils.publishTransaction(rawTx) const txHash = await this.txProviderUtil.publishTransaction(rawTx)
this.setTxHash(txId, txHash) this.setTxHash(txId, txHash)
this.setTxStatusSubmitted(txId) this.setTxStatusSubmitted(txId)
} }
@ -359,9 +382,12 @@ module.exports = class TransactionController extends EventEmitter {
this._setTxStatus(txId, 'confirmed') this._setTxStatus(txId, 'confirmed')
} }
setTxStatusFailed (txId, reason) { setTxStatusFailed (txId, err) {
const txMeta = this.getTx(txId) const txMeta = this.getTx(txId)
txMeta.err = reason txMeta.err = {
message: err.toString(),
stack: err.stack,
}
this.updateTx(txMeta) this.updateTx(txMeta)
this._setTxStatus(txId, 'failed') this._setTxStatus(txId, 'failed')
} }
@ -374,73 +400,6 @@ module.exports = class TransactionController extends EventEmitter {
this.updateTx(txMeta) 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 | | PRIVATE METHODS |
@ -482,75 +441,4 @@ module.exports = class TransactionController extends EventEmitter {
}) })
this.memStore.updateState({ unapprovedTxs, selectedAddressTxList }) 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
}
}
}
} }

View File

@ -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()
}
}

View File

@ -1,7 +1,11 @@
const ethUtil = require('ethereumjs-util') const EthQuery = require('ethjs-query')
const Transaction = require('ethereumjs-tx') const Transaction = require('ethereumjs-tx')
const normalize = require('eth-sig-util').normalize 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 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. and used to do things like calculate gas of a tx.
*/ */
module.exports = class txProvideUtils { module.exports = class txProvideUtil {
constructor (ethQuery) { constructor (provider) {
this.query = ethQuery this.query = new EthQuery(provider)
} }
async analyzeGasUsage (txMeta) { async analyzeGasUsage (txMeta) {
@ -91,31 +95,4 @@ module.exports = class txProvideUtils {
throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`) 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)
}

View File

@ -1,8 +1,44 @@
const ethUtil = require('ethereumjs-util')
const assert = require('assert')
const BN = require('bn.js')
module.exports = { module.exports = {
getStack, getStack,
sufficientBalance,
hexToBn,
bnToHex,
BnMultiplyByFraction,
} }
function getStack () { function getStack () {
const stack = new Error('Stack trace generator - not an error').stack const stack = new Error('Stack trace generator - not an error').stack
return 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)
}

View File

@ -258,26 +258,27 @@ function zipTask(target) {
} }
} }
function generateBundler(opts) { function generateBundler(opts, performBundle) {
var browserifyOpts = assign({}, watchify.args, { const browserifyOpts = assign({}, watchify.args, {
entries: ['./app/scripts/'+opts.filename], entries: ['./app/scripts/'+opts.filename],
plugin: 'browserify-derequire', plugin: 'browserify-derequire',
debug: debug, debug: debug,
fullPaths: debug, fullPaths: debug,
}) })
return browserify(browserifyOpts) let bundler = browserify(browserifyOpts)
}
function discTask(opts) {
let bundler = generateBundler(opts)
if (opts.watch) { if (opts.watch) {
bundler = watchify(bundler) bundler = watchify(bundler)
// on any dep update, runs the bundler // on any file update, re-runs the bundler
bundler.on('update', performBundle) bundler.on('update', performBundle)
} }
return bundler
}
function discTask(opts) {
const bundler = generateBundler(opts, performBundle)
// output build logs to terminal // output build logs to terminal
bundler.on('log', gutil.log) bundler.on('log', gutil.log)
@ -299,14 +300,7 @@ function discTask(opts) {
function bundleTask(opts) { function bundleTask(opts) {
let bundler = generateBundler(opts) const bundler = generateBundler(opts, performBundle)
if (opts.watch) {
bundler = watchify(bundler)
// on any file update, re-runs the bundler
bundler.on('update', performBundle)
}
// output build logs to terminal // output build logs to terminal
bundler.on('log', gutil.log) bundler.on('log', gutil.log)
@ -316,6 +310,17 @@ function bundleTask(opts) {
return ( return (
bundler.bundle() 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 // convert bundle stream to gulp vinyl stream
.pipe(source(opts.filename)) .pipe(source(opts.filename))
// inject variables into bundle // inject variables into bundle
@ -324,7 +329,7 @@ function bundleTask(opts) {
.pipe(buffer()) .pipe(buffer())
// sourcemaps // sourcemaps
// loads map from browserify file // loads map from browserify file
.pipe(gulpif(debug, sourcemaps.init({loadMaps: true}))) .pipe(gulpif(debug, sourcemaps.init({ loadMaps: true })))
// writes .map file // writes .map file
.pipe(gulpif(debug, sourcemaps.write('./'))) .pipe(gulpif(debug, sourcemaps.write('./')))
// write completed bundles // write completed bundles
@ -338,3 +343,7 @@ function bundleTask(opts) {
) )
} }
} }
function beep () {
process.stdout.write('\x07')
}

View File

@ -49,19 +49,20 @@
] ]
}, },
"dependencies": { "dependencies": {
"async": "^1.5.2", "async": "^2.5.0",
"await-semaphore": "^0.1.1", "await-semaphore": "^0.1.1",
"babel-runtime": "^6.23.0", "babel-runtime": "^6.23.0",
"bip39": "^2.2.0", "bip39": "^2.2.0",
"bluebird": "^3.5.0", "bluebird": "^3.5.0",
"boron": "^0.2.3", "boron": "^0.2.3",
"bn.js": "^4.11.7",
"browser-passworder": "^2.0.3", "browser-passworder": "^2.0.3",
"browserify-derequire": "^0.9.4", "browserify-derequire": "^0.9.4",
"client-sw-ready-event": "^3.3.0", "client-sw-ready-event": "^3.3.0",
"clone": "^1.0.2", "clone": "^2.1.1",
"copy-to-clipboard": "^2.0.0", "copy-to-clipboard": "^3.0.8",
"debounce": "^1.0.0", "debounce": "^1.0.0",
"deep-extend": "^0.4.1", "deep-extend": "^0.5.0",
"detect-node": "^2.0.3", "detect-node": "^2.0.3",
"disc": "^1.3.2", "disc": "^1.3.2",
"dnode": "^1.2.2", "dnode": "^1.2.2",
@ -76,7 +77,7 @@
"eth-simple-keyring": "^1.1.1", "eth-simple-keyring": "^1.1.1",
"eth-token-tracker": "^1.1.2", "eth-token-tracker": "^1.1.2",
"ethereumjs-tx": "^1.3.0", "ethereumjs-tx": "^1.3.0",
"ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9",
"ethereumjs-wallet": "^0.6.0", "ethereumjs-wallet": "^0.6.0",
"ethjs-ens": "^2.0.0", "ethjs-ens": "^2.0.0",
"ethjs-query": "^0.2.6", "ethjs-query": "^0.2.6",
@ -85,13 +86,14 @@
"extensionizer": "^1.0.0", "extensionizer": "^1.0.0",
"fast-levenshtein": "^2.0.6", "fast-levenshtein": "^2.0.6",
"gulp-autoprefixer": "^4.0.0", "gulp-autoprefixer": "^4.0.0",
"gulp-eslint": "^2.0.0",
"gulp-sass": "^3.1.0", "gulp-sass": "^3.1.0",
"gulp": "github:gulpjs/gulp#4.0",
"gulp-eslint": "^4.0.0",
"hat": "0.0.3", "hat": "0.0.3",
"idb-global": "^1.0.0", "idb-global": "^2.1.0",
"identicon.js": "^1.2.1", "identicon.js": "^2.3.1",
"iframe": "^1.0.0", "iframe": "^1.0.0",
"iframe-stream": "^1.0.2", "iframe-stream": "^3.0.0",
"inject-css": "^0.1.1", "inject-css": "^0.1.1",
"jazzicon": "^1.2.0", "jazzicon": "^1.2.0",
"loglevel": "^1.4.1", "loglevel": "^1.4.1",
@ -105,7 +107,7 @@
"ping-pong-stream": "^1.0.0", "ping-pong-stream": "^1.0.0",
"pojo-migrator": "^2.1.0", "pojo-migrator": "^2.1.0",
"polyfill-crypto.getrandomvalues": "^1.0.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-filter": "^1.1.0",
"promise-to-callback": "^1.0.0", "promise-to-callback": "^1.0.0",
"pump": "^1.0.2", "pump": "^1.0.2",
@ -114,9 +116,9 @@
"react": "^15.0.2", "react": "^15.0.2",
"react-addons-css-transition-group": "^15.6.0", "react-addons-css-transition-group": "^15.6.0",
"react-dom": "^15.5.4", "react-dom": "^15.5.4",
"react-hyperscript": "^2.2.2", "react-hyperscript": "^3.0.0",
"react-markdown": "^2.3.0", "react-markdown": "^2.3.0",
"react-redux": "^4.4.5", "react-redux": "^5.0.5",
"react-select": "^1.0.0-rc.2", "react-select": "^1.0.0-rc.2",
"react-simple-file-input": "^1.0.0", "react-simple-file-input": "^1.0.0",
"react-tooltip-component": "^0.3.0", "react-tooltip-component": "^0.3.0",
@ -124,25 +126,24 @@
"reactify": "^1.1.1", "reactify": "^1.1.1",
"readable-stream": "^2.1.2", "readable-stream": "^2.1.2",
"redux": "^3.0.5", "redux": "^3.0.5",
"redux-logger": "^2.10.2", "redux-logger": "^3.0.6",
"redux-thunk": "^1.0.2", "redux-thunk": "^2.2.0",
"request-promise": "^4.1.1", "request-promise": "^4.1.1",
"sandwich-expando": "^1.0.5", "sandwich-expando": "^1.0.5",
"semaphore": "^1.0.5", "semaphore": "^1.0.5",
"sw-stream": "^2.0.0", "sw-stream": "^2.0.0",
"textarea-caret": "^3.0.1", "textarea-caret": "^3.0.1",
"three.js": "^0.73.2",
"through2": "^2.0.3", "through2": "^2.0.3",
"valid-url": "^1.0.9", "valid-url": "^1.0.9",
"vreme": "^3.0.2", "vreme": "^3.0.2",
"web3": "0.19.1", "web3": "^0.20.1",
"web3-provider-engine": "^13.2.9", "web3-provider-engine": "^13.2.9",
"web3-stream-provider": "^3.0.1", "web3-stream-provider": "^3.0.1",
"xtend": "^4.0.1" "xtend": "^4.0.1"
}, },
"devDependencies": { "devDependencies": {
"babel-core": "^6.24.1", "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-async-to-generator": "^6.24.1",
"babel-plugin-transform-runtime": "^6.23.0", "babel-plugin-transform-runtime": "^6.23.0",
"babel-polyfill": "^6.23.0", "babel-polyfill": "^6.23.0",
@ -151,50 +152,52 @@
"babelify": "^7.2.0", "babelify": "^7.2.0",
"beefy": "^2.1.5", "beefy": "^2.1.5",
"brfs": "^1.4.3", "brfs": "^1.4.3",
"browserify": "^13.0.0", "browserify": "^14.4.0",
"chai": "^3.5.0", "chai": "^4.1.0",
"coveralls": "^2.13.1", "coveralls": "^2.13.1",
"deep-freeze-strict": "^1.1.1", "deep-freeze-strict": "^1.1.1",
"del": "^2.2.0", "del": "^3.0.0",
"envify": "^4.0.0", "envify": "^4.0.0",
"enzyme": "^2.8.2", "enzyme": "^2.8.2",
"eslint-plugin-chai": "0.0.1", "eslint-plugin-chai": "0.0.1",
"eslint-plugin-mocha": "^4.9.0", "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": "github:gulpjs/gulp#4.0",
"gulp-if": "^2.0.1", "gulp-if": "^2.0.1",
"gulp-json-editor": "^2.2.1", "gulp-json-editor": "^2.2.1",
"gulp-livereload": "^3.8.1", "gulp-livereload": "^3.8.1",
"gulp-replace": "^0.5.4", "gulp-replace": "^0.6.1",
"gulp-sourcemaps": "^1.6.0", "gulp-sourcemaps": "^2.6.0",
"gulp-util": "^3.0.7", "gulp-util": "^3.0.7",
"gulp-watch": "^4.3.5", "gulp-watch": "^4.3.5",
"gulp-zip": "^3.2.0", "gulp-zip": "^4.0.0",
"isomorphic-fetch": "^2.2.1", "isomorphic-fetch": "^2.2.1",
"jsdom": "^8.1.0", "jsdom": "^11.1.0",
"jsdom-global": "^1.7.0", "jsdom-global": "^3.0.2",
"jshint-stylish": "~0.1.5", "jshint-stylish": "~2.2.1",
"json-rpc-engine": "^3.0.1",
"lodash.assign": "^4.0.6", "lodash.assign": "^4.0.6",
"mocha": "^2.4.5", "mocha": "^3.4.2",
"mocha-eslint": "^2.1.1", "mocha-eslint": "^4.0.0",
"mocha-jsdom": "^1.1.0", "mocha-jsdom": "^1.1.0",
"mocha-sinon": "^1.1.5", "mocha-sinon": "^2.0.0",
"nock": "^8.0.0", "nock": "^9.0.14",
"nyc": "^11.0.3", "nyc": "^11.0.3",
"open": "0.0.5", "open": "0.0.5",
"prompt": "^1.0.0", "prompt": "^1.0.0",
"qs": "^6.2.0", "qs": "^6.2.0",
"qunit": "^0.9.1", "qunit": "^1.0.0",
"react-addons-test-utils": "^15.5.1", "react-addons-test-utils": "^15.5.1",
"react-test-renderer": "^15.5.4", "react-test-renderer": "^15.5.4",
"react-testutils-additions": "^15.2.0", "react-testutils-additions": "^15.2.0",
"sinon": "^2.3.8", "sinon": "^2.3.8",
"tape": "^4.5.1", "tape": "^4.5.1",
"testem": "^1.10.3", "testem": "^1.10.3",
"uglifyify": "^3.0.1", "uglifyify": "^4.0.2",
"vinyl-buffer": "^1.0.0", "vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0", "vinyl-source-stream": "^1.1.0",
"watchify": "^3.7.0" "watchify": "^3.9.0"
}, },
"engines": { "engines": {
"node": ">=0.8.0" "node": ">=0.8.0"

25
test/stub/provider.js Normal file
View File

@ -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)
}

View File

@ -1,7 +1,7 @@
const assert = require('assert') const assert = require('assert')
const MessageManger = require('../../app/scripts/lib/message-manager') const MessageManger = require('../../app/scripts/lib/message-manager')
describe('Transaction Manager', function () { describe('Message Manager', function () {
let messageManager let messageManager
beforeEach(function () { beforeEach(function () {

View File

@ -31,12 +31,11 @@ describe('Nonce Tracker', function () {
}) })
describe('#getNonceLock', function () { describe('#getNonceLock', function () {
it('should work', async function (done) { it('should work', async function () {
this.timeout(15000) this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926') const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '1', 'nonce should be 1') assert.equal(nonceLock.nextNonce, '1', 'nonce should be 1')
await nonceLock.releaseLock() await nonceLock.releaseLock()
done()
}) })
}) })
}) })

View File

@ -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)
})
})
})
})

View File

@ -10,16 +10,20 @@ const noop = () => true
const currentNetworkId = 42 const currentNetworkId = 42
const otherNetworkId = 36 const otherNetworkId = 36
const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex') const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex')
const { createStubedProvider } = require('../stub/provider')
describe('Transaction Controller', function () { describe('Transaction Controller', function () {
let txController let txController, engine, provider, providerResultStub
beforeEach(function () { beforeEach(function () {
providerResultStub = {}
provider = createStubedProvider(providerResultStub)
txController = new TransactionController({ txController = new TransactionController({
provider,
networkStore: new ObservableStore(currentNetworkId), networkStore: new ObservableStore(currentNetworkId),
txHistoryLimit: 10, txHistoryLimit: 10,
blockTracker: { getCurrentBlock: noop, on: noop, once: noop }, blockTracker: { getCurrentBlock: noop, on: noop, once: noop },
provider: { sendAsync: noop },
ethStore: { getState: noop }, ethStore: { getState: noop },
signTransaction: (ethTx) => new Promise((resolve) => { signTransaction: (ethTx) => new Promise((resolve) => {
ethTx.sign(privKey) ethTx.sign(privKey)
@ -27,19 +31,7 @@ describe('Transaction Controller', function () {
}), }),
}) })
txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop }) txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop })
txController.query = new Proxy({}, { txController.txProviderUtils = new TxProvideUtils(txController.provider)
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 () { 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) { it('should resolve when finished and status is submitted and resolve with the hash', function (done) {
txController.once('newUnaprovedTx', (txMetaFromEmit) => { txController.once('newUnaprovedTx', (txMetaFromEmit) => {
setTimeout(() => { setTimeout(() => {
console.log('HELLLO')
txController.setTxHash(txMetaFromEmit.id, '0x0') txController.setTxHash(txMetaFromEmit.id, '0x0')
txController.setTxStatusSubmitted(txMetaFromEmit.id) txController.setTxStatusSubmitted(txMetaFromEmit.id)
}, 10) }, 10)
@ -93,7 +84,6 @@ describe('Transaction Controller', function () {
it('should reject when finished and status is rejected', function (done) { it('should reject when finished and status is rejected', function (done) {
txController.once('newUnaprovedTx', (txMetaFromEmit) => { txController.once('newUnaprovedTx', (txMetaFromEmit) => {
setTimeout(() => { setTimeout(() => {
console.log('HELLLO')
txController.setTxStatusRejected(txMetaFromEmit.id) txController.setTxStatusRejected(txMetaFromEmit.id)
}, 10) }, 10)
}) })
@ -133,11 +123,9 @@ describe('Transaction Controller', function () {
'to':'0xc684832530fcbddae4b4230a47e991ddcec2831d', 'to':'0xc684832530fcbddae4b4230a47e991ddcec2831d',
}, },
} }
providerResultStub.eth_gasPrice = '4a817c800'
txController.query.stubResult('gasPrice', '0x4a817c800') providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' }
txController.query.stubResult('getBlockByNumber', { gasLimit: '0x47b784' }) providerResultStub.eth_estimateGas = '5209'
txController.query.stubResult('estimateGas', '0x5209')
txController.addTxDefaults(txMeta) txController.addTxDefaults(txMeta)
.then((txMetaWithDefaults) => { .then((txMetaWithDefaults) => {
assert(txMetaWithDefaults.txParams.value, '0x0','should have added 0x0 as the value') assert(txMetaWithDefaults.txParams.value, '0x0','should have added 0x0 as the value')
@ -394,9 +382,8 @@ describe('Transaction Controller', function () {
const wrongValue = '0x05' const wrongValue = '0x05'
txController.addTx(txMeta) txController.addTx(txMeta)
providerResultStub.eth_gasPrice = wrongValue
txController.query.stubResult('estimateGas', wrongValue) providerResultStub.eth_estimateGas = '0x5209'
txController.query.stubResult('gasPrice', wrongValue)
const signStub = sinon.stub(txController, 'signTransaction').callsFake(() => Promise.resolve()) const signStub = sinon.stub(txController, 'signTransaction').callsFake(() => Promise.resolve())
@ -429,46 +416,4 @@ describe('Transaction Controller', function () {
}).catch(done) }).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)
})
})
})
}) })

View File

@ -1,7 +1,7 @@
const assert = require('assert') const assert = require('assert')
const ethUtil = require('ethereumjs-util') const BN = require('bn.js')
const BN = ethUtil.BN
const { hexToBn, bnToHex } = require('../../app/scripts/lib/util')
const TxUtils = require('../../app/scripts/lib/tx-utils') 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 () { describe('chain Id', function () {
it('prepares a transaction with the provided chainId', function () { it('prepares a transaction with the provided chainId', function () {
const txParams = { const txParams = {
@ -96,7 +58,7 @@ describe('txUtils', function () {
assert(outputBn.eq(expectedBn), 'returns the original estimatedGas value') 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) // naive estimatedGas: 0x16e360 (1.5 mil)
const inputHex = '0x16e360' const inputHex = '0x16e360'
// dummy gas limit: 0x1e8480 (2 mil) // dummy gas limit: 0x1e8480 (2 mil)
@ -107,17 +69,7 @@ describe('txUtils', function () {
// const inputBn = hexToBn(inputHex) // const inputBn = hexToBn(inputHex)
// const outputBn = hexToBn(output) // const outputBn = hexToBn(output)
const expectedHex = bnToHex(ceilGasLimitBn) 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))
}

41
test/unit/util-test.js Normal file
View File

@ -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.')
})
})

View File

@ -143,7 +143,7 @@ BuyButtonSubview.prototype.formVersionSubview = function () {
return h('div.flex-column', { return h('div.flex-column', {
style: { style: {
alignItems: 'center', alignItems: 'center',
margin: '50px', margin: '20px 50px',
}, },
}, [ }, [
h('h3.text-transform-uppercase', { h('h3.text-transform-uppercase', {

View File

@ -33,10 +33,10 @@ ShapeshiftForm.prototype.renderMain = function () {
return h('.flex-column', { return h('.flex-column', {
style: { style: {
// marginTop: '10px', position: 'relative',
padding: '25px', padding: '25px',
paddingTop: '5px', paddingTop: '5px',
width: '100%', width: '90%',
minHeight: '215px', minHeight: '215px',
alignItems: 'center', alignItems: 'center',
overflowY: 'auto', 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', { h('input#fromCoin.buy-inputs.ex-coins', {
type: 'text', type: 'text',
list: 'coinList', list: 'coinList',
@ -79,27 +81,31 @@ ShapeshiftForm.prototype.renderMain = function () {
style: { style: {
fontSize: '12px', fontSize: '12px',
color: '#F7861C', color: '#F7861C',
position: 'relative', position: 'absolute',
bottom: '48px',
left: '106px',
}, },
}), }),
]), ]),
h('.icon-control', [ h('.icon-control', {
h('i.fa.fa-refresh.fa-4.orange', { style: {
style: { position: 'relative',
bottom: '5px', },
left: '5px', }, [
color: '#F7861C', // Not visible on the screen, can't see it on master.
},
onClick: this.updateCoin.bind(this), // 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', { h('i.fa.fa-chevron-right.fa-4.orange', {
style: { style: {
position: 'relative', position: 'absolute',
bottom: '26px', bottom: '35%',
left: '10px', left: '0%',
color: '#F7861C', color: '#F7861C',
}, },
onClick: this.updateCoin.bind(this), onClick: this.updateCoin.bind(this),
@ -117,69 +123,73 @@ ShapeshiftForm.prototype.renderMain = function () {
}, },
}), }),
]), ]),
h('.flex-column', { h('.flex-column', {
style: { style: {
marginTop: '1%',
alignItems: 'flex-start', alignItems: 'flex-start',
}, },
}, [ }, [
this.props.warning ? this.props.warning && h('span.error.flex-center', { this.props.warning
style: { ? this.props.warning
textAlign: 'center', && h('span.error.flex-center', {
width: '229px', style: {
height: '82px', textAlign: 'center',
}, width: '229px',
}, height: '82px',
this.props.warning) : this.renderInfo(), },
}, 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: { style: {
padding: '10px', boxSizing: 'border-box',
paddingTop: '0px', width: '227px',
width: '100%', 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('button', {
h('div', `${coin} Address:`), onClick: this.shift.bind(this),
h('input#fromCoinAddress.buy-inputs', {
type: 'text',
placeholder: `Your ${coin} Refund Address`,
dataset: {
persistentFormId: 'refund-address',
},
style: { style: {
boxSizing: 'border-box', marginTop: '1%',
width: '227px',
height: '30px',
padding: ' 5px ',
}, },
}), },
'Submit'),
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'),
]),
]), ]),
]) ])
} }

View File

@ -2,7 +2,7 @@ const inherits = require('util').inherits
const Component = require('react').Component const Component = require('react').Component
const h = require('react-hyperscript') const h = require('react-hyperscript')
const connect = require('react-redux').connect const connect = require('react-redux').connect
const vreme = new (require('vreme')) const vreme = new (require('vreme'))()
const explorerLink = require('../../lib/explorer-link') const explorerLink = require('../../lib/explorer-link')
const actions = require('../actions') const actions = require('../actions')
const addressSummary = require('../util').addressSummary const addressSummary = require('../util').addressSummary

View File

@ -6,7 +6,7 @@ const EthBalance = require('./eth-balance')
const addressSummary = require('../util').addressSummary const addressSummary = require('../util').addressSummary
const explorerLink = require('../../lib/explorer-link') const explorerLink = require('../../lib/explorer-link')
const CopyButton = require('./copyButton') const CopyButton = require('./copyButton')
const vreme = new (require('vreme')) const vreme = new (require('vreme'))()
const Tooltip = require('./tooltip') const Tooltip = require('./tooltip')
const numberToBN = require('number-to-bn') const numberToBN = require('number-to-bn')
@ -154,12 +154,21 @@ function failIfFailed (transaction) {
if (transaction.status === 'rejected') { if (transaction.status === 'rejected') {
return h('span.error', ' (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, { return h(Tooltip, {
title: transaction.err.message, title: message,
position: 'bottom', position: 'bottom',
}, [ }, [
h('span.error', ' (Failed)'), h(`span.${errFirst ? 'error' : 'warning'}`,
` (${errFirst ? 'Failed' : 'Warning'})`
),
]) ])
} }
} }

View File

@ -42,9 +42,12 @@ function reduceApp (state, action) {
accountDetail: { accountDetail: {
subview: 'transactions', subview: 'transactions',
}, },
transForward: true, // Used to render transition direction // Used to render transition direction
isLoading: false, // Used to display loading indicator transForward: true,
warning: null, // Used to display error text // Used to display loading indicator
isLoading: false,
// Used to display error text
warning: null,
}, state.appState) }, state.appState)
switch (action.type) { switch (action.type) {

View File

@ -1,8 +1,8 @@
const createStore = require('redux').createStore const createStore = require('redux').createStore
const applyMiddleware = require('redux').applyMiddleware const applyMiddleware = require('redux').applyMiddleware
const thunkMiddleware = require('redux-thunk') const thunkMiddleware = require('redux-thunk').default
const rootReducer = require('./reducers') const rootReducer = require('./reducers')
const createLogger = require('redux-logger') const createLogger = require('redux-logger').createLogger
global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'