aboutsummaryrefslogtreecommitdiffstats
path: root/app/scripts/controllers
diff options
context:
space:
mode:
authorfrankiebee <frankie.diamond@gmail.com>2018-04-07 02:07:20 +0800
committerfrankiebee <frankie.diamond@gmail.com>2018-04-11 05:28:05 +0800
commit2d7c3c2b00a698b19ac015624154c3c1cd2619b2 (patch)
tree3cfb7821143431735821954f54e90ce71ef173ef /app/scripts/controllers
parent2b787f2833d4f4cfda74ca22d3d340f0f924c94e (diff)
downloadtangerine-wallet-browser-2d7c3c2b00a698b19ac015624154c3c1cd2619b2.tar
tangerine-wallet-browser-2d7c3c2b00a698b19ac015624154c3c1cd2619b2.tar.gz
tangerine-wallet-browser-2d7c3c2b00a698b19ac015624154c3c1cd2619b2.tar.bz2
tangerine-wallet-browser-2d7c3c2b00a698b19ac015624154c3c1cd2619b2.tar.lz
tangerine-wallet-browser-2d7c3c2b00a698b19ac015624154c3c1cd2619b2.tar.xz
tangerine-wallet-browser-2d7c3c2b00a698b19ac015624154c3c1cd2619b2.tar.zst
tangerine-wallet-browser-2d7c3c2b00a698b19ac015624154c3c1cd2619b2.zip
meta - transactions - create a transactions dir in controller and move relevant files into it
Diffstat (limited to 'app/scripts/controllers')
-rw-r--r--app/scripts/controllers/transactions/index.js (renamed from app/scripts/controllers/transactions.js)68
-rw-r--r--app/scripts/controllers/transactions/lib/tx-state-history-helper.js41
-rw-r--r--app/scripts/controllers/transactions/lib/util.js66
-rw-r--r--app/scripts/controllers/transactions/nonce-tracker.js148
-rw-r--r--app/scripts/controllers/transactions/pending-tx-tracker.js189
-rw-r--r--app/scripts/controllers/transactions/tx-gas-utils.js103
-rw-r--r--app/scripts/controllers/transactions/tx-state-manager.js315
7 files changed, 869 insertions, 61 deletions
diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions/index.js
index 336b0d8f7..6f66e3a1e 100644
--- a/app/scripts/controllers/transactions.js
+++ b/app/scripts/controllers/transactions/index.js
@@ -3,11 +3,11 @@ const ObservableStore = require('obs-store')
const ethUtil = require('ethereumjs-util')
const Transaction = require('ethereumjs-tx')
const EthQuery = require('ethjs-query')
-const TransactionStateManager = require('../lib/tx-state-manager')
-const TxGasUtil = require('../lib/tx-gas-utils')
-const PendingTransactionTracker = require('../lib/pending-tx-tracker')
-const NonceTracker = require('../lib/nonce-tracker')
-
+const TransactionStateManager = require('./tx-state-manager')
+const TxGasUtil = require('./tx-gas-utils')
+const PendingTransactionTracker = require('./pending-tx-tracker')
+const NonceTracker = require('./nonce-tracker')
+const txUtils = require('./lib/util')
/*
Transaction Controller is an aggregate of sub-controllers and trackers
composing them in a way to be exposed to the metamask controller
@@ -185,8 +185,8 @@ module.exports = class TransactionController extends EventEmitter {
async addUnapprovedTransaction (txParams) {
// validate
- const normalizedTxParams = this._normalizeTxParams(txParams)
- this._validateTxParams(normalizedTxParams)
+ const normalizedTxParams = txUtils.normalizeTxParams(txParams)
+ txUtils.validateTxParams(normalizedTxParams)
// construct txMeta
let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams })
this.addTx(txMeta)
@@ -314,60 +314,6 @@ module.exports = class TransactionController extends EventEmitter {
// PRIVATE METHODS
//
- _normalizeTxParams (txParams) {
- // functions that handle normalizing of that key in txParams
- const whiteList = {
- from: from => ethUtil.addHexPrefix(from).toLowerCase(),
- to: to => ethUtil.addHexPrefix(txParams.to).toLowerCase(),
- nonce: nonce => ethUtil.addHexPrefix(nonce),
- value: value => ethUtil.addHexPrefix(value),
- data: data => ethUtil.addHexPrefix(data),
- gas: gas => ethUtil.addHexPrefix(gas),
- gasPrice: gasPrice => ethUtil.addHexPrefix(gasPrice),
- }
-
- // apply only keys in the whiteList
- const normalizedTxParams = {}
- Object.keys(whiteList).forEach((key) => {
- if (txParams[key]) normalizedTxParams[key] = whiteList[key](txParams[key])
- })
-
- return normalizedTxParams
- }
-
- _validateTxParams (txParams) {
- this._validateFrom(txParams)
- this._validateRecipient(txParams)
- if ('value' in txParams) {
- const value = txParams.value.toString()
- if (value.includes('-')) {
- throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)
- }
-
- if (value.includes('.')) {
- throw new Error(`Invalid transaction value of ${txParams.value} number must be in wei`)
- }
- }
- }
-
- _validateFrom (txParams) {
- if ( !(typeof txParams.from === 'string') ) throw new Error(`Invalid from address ${txParams.from} not a string`)
- if (!ethUtil.isValidAddress(txParams.from)) throw new Error('Invalid from address')
- }
-
- _validateRecipient (txParams) {
- if (txParams.to === '0x' || txParams.to === null ) {
- if (txParams.data) {
- delete txParams.to
- } else {
- throw new Error('Invalid recipient address')
- }
- } else if ( txParams.to !== undefined && !ethUtil.isValidAddress(txParams.to) ) {
- throw new Error('Invalid recipient address')
- }
- return txParams
- }
-
_markNonceDuplicatesDropped (txId) {
this.txStateManager.setTxStatusConfirmed(txId)
// get the confirmed transactions nonce and from address
diff --git a/app/scripts/controllers/transactions/lib/tx-state-history-helper.js b/app/scripts/controllers/transactions/lib/tx-state-history-helper.js
new file mode 100644
index 000000000..94c7b6792
--- /dev/null
+++ b/app/scripts/controllers/transactions/lib/tx-state-history-helper.js
@@ -0,0 +1,41 @@
+const jsonDiffer = require('fast-json-patch')
+const clone = require('clone')
+
+module.exports = {
+ generateHistoryEntry,
+ replayHistory,
+ snapshotFromTxMeta,
+ migrateFromSnapshotsToDiffs,
+}
+
+
+function migrateFromSnapshotsToDiffs (longHistory) {
+ return (
+ longHistory
+ // convert non-initial history entries into diffs
+ .map((entry, index) => {
+ if (index === 0) return entry
+ return generateHistoryEntry(longHistory[index - 1], entry)
+ })
+ )
+}
+
+function generateHistoryEntry (previousState, newState, note) {
+ const entry = jsonDiffer.compare(previousState, newState)
+ // Add a note to the first op, since it breaks if we append it to the entry
+ if (note && entry[0]) entry[0].note = note
+ return entry
+}
+
+function replayHistory (_shortHistory) {
+ const shortHistory = clone(_shortHistory)
+ return shortHistory.reduce((val, entry) => jsonDiffer.applyPatch(val, entry).newDocument)
+}
+
+function snapshotFromTxMeta (txMeta) {
+ // create txMeta snapshot for history
+ const snapshot = clone(txMeta)
+ // dont include previous history in this snapshot
+ delete snapshot.history
+ return snapshot
+}
diff --git a/app/scripts/controllers/transactions/lib/util.js b/app/scripts/controllers/transactions/lib/util.js
new file mode 100644
index 000000000..f403b0758
--- /dev/null
+++ b/app/scripts/controllers/transactions/lib/util.js
@@ -0,0 +1,66 @@
+const {
+ addHexPrefix,
+ isValidAddress,
+} = require('ethereumjs-util')
+
+module.exports = {
+ normalizeTxParams,
+ validateTxParams,
+ validateFrom,
+ validateRecipient
+}
+
+
+function normalizeTxParams (txParams) {
+ // functions that handle normalizing of that key in txParams
+ const whiteList = {
+ from: from => addHexPrefix(from).toLowerCase(),
+ to: to => addHexPrefix(txParams.to).toLowerCase(),
+ nonce: nonce => addHexPrefix(nonce),
+ value: value => addHexPrefix(value),
+ data: data => addHexPrefix(data),
+ gas: gas => addHexPrefix(gas),
+ gasPrice: gasPrice => addHexPrefix(gasPrice),
+ }
+
+ // apply only keys in the whiteList
+ const normalizedTxParams = {}
+ Object.keys(whiteList).forEach((key) => {
+ if (txParams[key]) normalizedTxParams[key] = whiteList[key](txParams[key])
+ })
+
+ return normalizedTxParams
+}
+
+function validateTxParams (txParams) {
+ validateFrom(txParams)
+ validateRecipient(txParams)
+ if ('value' in txParams) {
+ const value = txParams.value.toString()
+ if (value.includes('-')) {
+ throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)
+ }
+
+ if (value.includes('.')) {
+ throw new Error(`Invalid transaction value of ${txParams.value} number must be in wei`)
+ }
+ }
+}
+
+function validateFrom (txParams) {
+ if ( !(typeof txParams.from === 'string') ) throw new Error(`Invalid from address ${txParams.from} not a string`)
+ if (!isValidAddress(txParams.from)) throw new Error('Invalid from address')
+}
+
+function validateRecipient (txParams) {
+ if (txParams.to === '0x' || txParams.to === null ) {
+ if (txParams.data) {
+ delete txParams.to
+ } else {
+ throw new Error('Invalid recipient address')
+ }
+ } else if ( txParams.to !== undefined && !isValidAddress(txParams.to) ) {
+ throw new Error('Invalid recipient address')
+ }
+ return txParams
+} \ No newline at end of file
diff --git a/app/scripts/controllers/transactions/nonce-tracker.js b/app/scripts/controllers/transactions/nonce-tracker.js
new file mode 100644
index 000000000..5b1cd7f43
--- /dev/null
+++ b/app/scripts/controllers/transactions/nonce-tracker.js
@@ -0,0 +1,148 @@
+const EthQuery = require('ethjs-query')
+const assert = require('assert')
+const Mutex = require('await-semaphore').Mutex
+
+class NonceTracker {
+
+ constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) {
+ this.provider = provider
+ this.ethQuery = new EthQuery(provider)
+ this.getPendingTransactions = getPendingTransactions
+ this.getConfirmedTransactions = getConfirmedTransactions
+ this.lockMap = {}
+ }
+
+ async getGlobalLock () {
+ const globalMutex = this._lookupMutex('global')
+ // await global mutex free
+ const releaseLock = await globalMutex.acquire()
+ return { releaseLock }
+ }
+
+ // releaseLock must be called
+ // releaseLock must be called after adding signed tx to pending transactions (or discarding)
+ async getNonceLock (address) {
+ // await global mutex free
+ await this._globalMutexFree()
+ // await lock free, then take lock
+ const releaseLock = await this._takeMutex(address)
+ // evaluate multiple nextNonce strategies
+ const nonceDetails = {}
+ const networkNonceResult = await this._getNetworkNextNonce(address)
+ const highestLocallyConfirmed = this._getHighestLocallyConfirmed(address)
+ const nextNetworkNonce = networkNonceResult.nonce
+ const highestSuggested = Math.max(nextNetworkNonce, highestLocallyConfirmed)
+
+ const pendingTxs = this.getPendingTransactions(address)
+ const localNonceResult = this._getHighestContinuousFrom(pendingTxs, highestSuggested) || 0
+
+ nonceDetails.params = {
+ highestLocallyConfirmed,
+ highestSuggested,
+ nextNetworkNonce,
+ }
+ nonceDetails.local = localNonceResult
+ nonceDetails.network = networkNonceResult
+
+ const nextNonce = Math.max(networkNonceResult.nonce, localNonceResult.nonce)
+ assert(Number.isInteger(nextNonce), `nonce-tracker - nextNonce is not an integer - got: (${typeof nextNonce}) "${nextNonce}"`)
+
+ // return nonce and release cb
+ return { nextNonce, nonceDetails, releaseLock }
+ }
+
+ async _getCurrentBlock () {
+ const blockTracker = this._getBlockTracker()
+ const currentBlock = blockTracker.getCurrentBlock()
+ if (currentBlock) return currentBlock
+ return await new Promise((reject, resolve) => {
+ blockTracker.once('latest', resolve)
+ })
+ }
+
+ async _globalMutexFree () {
+ const globalMutex = this._lookupMutex('global')
+ const release = await globalMutex.acquire()
+ release()
+ }
+
+ async _takeMutex (lockId) {
+ const mutex = this._lookupMutex(lockId)
+ const releaseLock = await mutex.acquire()
+ return releaseLock
+ }
+
+ _lookupMutex (lockId) {
+ let mutex = this.lockMap[lockId]
+ if (!mutex) {
+ mutex = new Mutex()
+ this.lockMap[lockId] = mutex
+ }
+ return mutex
+ }
+
+ async _getNetworkNextNonce (address) {
+ // calculate next nonce
+ // we need to make sure our base count
+ // and pending count are from the same block
+ const currentBlock = await this._getCurrentBlock()
+ const blockNumber = currentBlock.blockNumber
+ const baseCountBN = await this.ethQuery.getTransactionCount(address, blockNumber || 'latest')
+ const baseCount = baseCountBN.toNumber()
+ assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`)
+ const nonceDetails = { blockNumber, baseCount }
+ return { name: 'network', nonce: baseCount, details: nonceDetails }
+ }
+
+ _getHighestLocallyConfirmed (address) {
+ const confirmedTransactions = this.getConfirmedTransactions(address)
+ const highest = this._getHighestNonce(confirmedTransactions)
+ return Number.isInteger(highest) ? highest + 1 : 0
+ }
+
+ _reduceTxListToUniqueNonces (txList) {
+ const reducedTxList = txList.reduce((reducedList, txMeta, index) => {
+ if (!index) return [txMeta]
+ const nonceMatches = txList.filter((txData) => {
+ return txMeta.txParams.nonce === txData.txParams.nonce
+ })
+ if (nonceMatches.length > 1) return reducedList
+ reducedList.push(txMeta)
+ return reducedList
+ }, [])
+ return reducedTxList
+ }
+
+ _getHighestNonce (txList) {
+ const nonces = txList.map((txMeta) => {
+ const nonce = txMeta.txParams.nonce
+ assert(typeof nonce, 'string', 'nonces should be hex strings')
+ return parseInt(nonce, 16)
+ })
+ const highestNonce = Math.max.apply(null, nonces)
+ return highestNonce
+ }
+
+ _getHighestContinuousFrom (txList, startPoint) {
+ const nonces = txList.map((txMeta) => {
+ const nonce = txMeta.txParams.nonce
+ assert(typeof nonce, 'string', 'nonces should be hex strings')
+ return parseInt(nonce, 16)
+ })
+
+ let highest = startPoint
+ while (nonces.includes(highest)) {
+ highest++
+ }
+
+ return { name: 'local', nonce: highest, details: { startPoint, highest } }
+ }
+
+ // this is a hotfix for the fact that the blockTracker will
+ // change when the network changes
+ _getBlockTracker () {
+ return this.provider._blockTracker
+ }
+}
+
+module.exports = NonceTracker
diff --git a/app/scripts/controllers/transactions/pending-tx-tracker.js b/app/scripts/controllers/transactions/pending-tx-tracker.js
new file mode 100644
index 000000000..e8869e6b8
--- /dev/null
+++ b/app/scripts/controllers/transactions/pending-tx-tracker.js
@@ -0,0 +1,189 @@
+const EventEmitter = require('events')
+const EthQuery = require('ethjs-query')
+/*
+
+ 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,
+ 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
+ // default is one day
+ this.getPendingTransactions = config.getPendingTransactions
+ this.getCompletedTransactions = config.getCompletedTransactions
+ this.publishTransaction = config.publishTransaction
+ this._checkPendingTxs()
+ }
+
+ // 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('tx:failed', txId, noTxHashErr)
+ return
+ }
+
+
+ block.transactions.forEach((tx) => {
+ if (tx.hash === txHash) this.emit('tx:confirmed', 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 (block) {
+ const pending = this.getPendingTransactions()
+ // only try resubmitting if their are transactions to resubmit
+ if (!pending.length) return
+ pending.forEach((txMeta) => this._resubmitTx(txMeta, block.number).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"
+
+ Also don't mark as failed if it has ever been broadcast successfully.
+ A successful broadcast means it may still be mined.
+ */
+ 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
+ txMeta.warning = {
+ error: errorMessage,
+ message: 'There was an error when resubmitting this transaction.',
+ }
+ this.emit('tx:warning', txMeta, err)
+ }))
+ }
+
+ async _resubmitTx (txMeta, latestBlockNumber) {
+ if (!txMeta.firstRetryBlockNumber) {
+ this.emit('tx:block-update', txMeta, latestBlockNumber)
+ }
+
+ const firstRetryBlockNumber = txMeta.firstRetryBlockNumber || latestBlockNumber
+ const txBlockDistance = Number.parseInt(latestBlockNumber, 16) - Number.parseInt(firstRetryBlockNumber, 16)
+
+ const retryCount = txMeta.retryCount || 0
+
+ // Exponential backoff to limit retries at publishing
+ if (txBlockDistance <= Math.pow(2, retryCount) - 1) return
+
+ // Only auto-submit already-signed txs:
+ if (!('rawTx' in txMeta)) return
+
+ const rawTx = txMeta.rawTx
+ const txHash = await this.publishTransaction(rawTx)
+
+ // Increment successful tries:
+ this.emit('tx:retry', txMeta)
+ return txHash
+ }
+
+ 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('tx:failed', txId, noTxHashErr)
+ return
+ }
+
+ // If another tx with the same nonce is mined, set as failed.
+ const taken = await this._checkIfNonceIsTaken(txMeta)
+ if (taken) {
+ const nonceTakenErr = new Error('Another transaction with this nonce has been mined.')
+ nonceTakenErr.name = 'NonceTakenErr'
+ return this.emit('tx:failed', txId, nonceTakenErr)
+ }
+
+ // get latest transaction status
+ let txParams
+ try {
+ txParams = await this.query.getTransactionByHash(txHash)
+ if (!txParams) return
+ if (txParams.blockNumber) {
+ this.emit('tx:confirmed', txId)
+ }
+ } catch (err) {
+ txMeta.warning = {
+ error: err.message,
+ message: 'There was a problem loading this transaction.',
+ }
+ this.emit('tx:warning', txMeta, 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()
+ }
+
+ async _checkIfNonceIsTaken (txMeta) {
+ const address = txMeta.txParams.from
+ const completed = this.getCompletedTransactions(address)
+ const sameNonce = completed.filter((otherMeta) => {
+ return otherMeta.txParams.nonce === txMeta.txParams.nonce
+ })
+ return sameNonce.length > 0
+ }
+
+}
diff --git a/app/scripts/controllers/transactions/tx-gas-utils.js b/app/scripts/controllers/transactions/tx-gas-utils.js
new file mode 100644
index 000000000..f40542603
--- /dev/null
+++ b/app/scripts/controllers/transactions/tx-gas-utils.js
@@ -0,0 +1,103 @@
+const EthQuery = require('ethjs-query')
+const {
+ hexToBn,
+ BnMultiplyByFraction,
+ bnToHex,
+} = require('../../lib/util')
+const { addHexPrefix } = require('ethereumjs-util')
+const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send.
+
+/*
+tx-utils are utility methods for Transaction manager
+its passed ethquery
+and used to do things like calculate gas of a tx.
+*/
+
+module.exports = class TxGasUtil {
+
+ constructor (provider) {
+ this.query = new EthQuery(provider)
+ }
+
+ async analyzeGasUsage (txMeta) {
+ const block = await this.query.getBlockByNumber('latest', true)
+ let estimatedGasHex
+ try {
+ estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit)
+ } catch (err) {
+ const simulationFailed = (
+ err.message.includes('Transaction execution error.') ||
+ err.message.includes('gas required exceeds allowance or always failing transaction')
+ )
+ if (simulationFailed) {
+ txMeta.simulationFails = true
+ return txMeta
+ }
+ }
+ this.setTxGas(txMeta, block.gasLimit, estimatedGasHex)
+ return txMeta
+ }
+
+ async estimateTxGas (txMeta, blockGasLimitHex) {
+ const txParams = txMeta.txParams
+
+ // check if gasLimit is already specified
+ txMeta.gasLimitSpecified = Boolean(txParams.gas)
+
+ // if it is, use that value
+ if (txMeta.gasLimitSpecified) {
+ return txParams.gas
+ }
+
+ // if recipient has no code, gas is 21k max:
+ const recipient = txParams.to
+ const hasRecipient = Boolean(recipient)
+ let code
+ if (recipient) code = await this.query.getCode(recipient)
+
+ if (hasRecipient && (!code || code === '0x')) {
+ txParams.gas = SIMPLE_GAS_COST
+ txMeta.simpleSend = true // Prevents buffer addition
+ return SIMPLE_GAS_COST
+ }
+
+ // if not, fall back to block gasLimit
+ const blockGasLimitBN = hexToBn(blockGasLimitHex)
+ const saferGasLimitBN = BnMultiplyByFraction(blockGasLimitBN, 19, 20)
+ txParams.gas = bnToHex(saferGasLimitBN)
+
+ // run tx
+ return await this.query.estimateGas(txParams)
+ }
+
+ setTxGas (txMeta, blockGasLimitHex, estimatedGasHex) {
+ txMeta.estimatedGas = addHexPrefix(estimatedGasHex)
+ const txParams = txMeta.txParams
+
+ // if gasLimit was specified and doesnt OOG,
+ // use original specified amount
+ if (txMeta.gasLimitSpecified || txMeta.simpleSend) {
+ txMeta.estimatedGas = txParams.gas
+ return
+ }
+ // if gasLimit not originally specified,
+ // try adding an additional gas buffer to our estimation for safety
+ const recommendedGasHex = this.addGasBuffer(txMeta.estimatedGas, blockGasLimitHex)
+ txParams.gas = recommendedGasHex
+ return
+ }
+
+ addGasBuffer (initialGasLimitHex, blockGasLimitHex) {
+ const initialGasLimitBn = hexToBn(initialGasLimitHex)
+ const blockGasLimitBn = hexToBn(blockGasLimitHex)
+ const upperGasLimitBn = blockGasLimitBn.muln(0.9)
+ const bufferedGasLimitBn = initialGasLimitBn.muln(1.5)
+
+ // if initialGasLimit is above blockGasLimit, dont modify it
+ if (initialGasLimitBn.gt(upperGasLimitBn)) return bnToHex(initialGasLimitBn)
+ // if bufferedGasLimit is below blockGasLimit, use bufferedGasLimit
+ if (bufferedGasLimitBn.lt(upperGasLimitBn)) return bnToHex(bufferedGasLimitBn)
+ // otherwise use blockGasLimit
+ return bnToHex(upperGasLimitBn)
+ }
+} \ No newline at end of file
diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js
new file mode 100644
index 000000000..cb24b8c99
--- /dev/null
+++ b/app/scripts/controllers/transactions/tx-state-manager.js
@@ -0,0 +1,315 @@
+const extend = require('xtend')
+const EventEmitter = require('events')
+const ObservableStore = require('obs-store')
+const createId = require('../../lib/random-id')
+const ethUtil = require('ethereumjs-util')
+const txStateHistoryHelper = require('./lib/tx-state-history-helper')
+
+// STATUS METHODS
+ // statuses:
+ // - `'unapproved'` the user has not responded
+ // - `'rejected'` the user has responded no!
+ // - `'approved'` the user has approved the tx
+ // - `'signed'` the tx is signed
+ // - `'submitted'` the tx is sent to a server
+ // - `'confirmed'` the tx has been included in a block.
+ // - `'failed'` the tx failed for some reason, included on tx data.
+ // - `'dropped'` the tx nonce was already used
+
+module.exports = class TransactionStateManager extends EventEmitter {
+ constructor ({ initState, txHistoryLimit, getNetwork }) {
+ super()
+
+ this.store = new ObservableStore(
+ extend({
+ transactions: [],
+ }, initState))
+ this.txHistoryLimit = txHistoryLimit
+ this.getNetwork = getNetwork
+ }
+
+ generateTxMeta (opts) {
+ return extend({
+ id: createId(),
+ time: (new Date()).getTime(),
+ status: 'unapproved',
+ metamaskNetworkId: this.getNetwork(),
+ loadingDefaults: true,
+ }, opts)
+ }
+
+ getTxList () {
+ const network = this.getNetwork()
+ const fullTxList = this.getFullTxList()
+ return fullTxList.filter((txMeta) => txMeta.metamaskNetworkId === network)
+ }
+
+ getFullTxList () {
+ return this.store.getState().transactions
+ }
+
+ // Returns the tx list
+ getUnapprovedTxList () {
+ const txList = this.getTxsByMetaData('status', 'unapproved')
+ return txList.reduce((result, tx) => {
+ result[tx.id] = tx
+ return result
+ }, {})
+ }
+
+ getPendingTransactions (address) {
+ const opts = { status: 'submitted' }
+ if (address) opts.from = address
+ return this.getFilteredTxList(opts)
+ }
+
+ getConfirmedTransactions (address) {
+ const opts = { status: 'confirmed' }
+ if (address) opts.from = address
+ return this.getFilteredTxList(opts)
+ }
+
+ addTx (txMeta) {
+ this.once(`${txMeta.id}:signed`, function (txId) {
+ this.removeAllListeners(`${txMeta.id}:rejected`)
+ })
+ this.once(`${txMeta.id}:rejected`, function (txId) {
+ this.removeAllListeners(`${txMeta.id}:signed`)
+ })
+ // initialize history
+ txMeta.history = []
+ // capture initial snapshot of txMeta for history
+ const snapshot = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
+ txMeta.history.push(snapshot)
+
+ const transactions = this.getFullTxList()
+ const txCount = transactions.length
+ const txHistoryLimit = this.txHistoryLimit
+
+ // checks if the length of the tx history is
+ // longer then desired persistence limit
+ // and then if it is removes only confirmed
+ // or rejected tx's.
+ // not tx's that are pending or unapproved
+ if (txCount > txHistoryLimit - 1) {
+ let index = transactions.findIndex((metaTx) => {
+ return this.getFinalStates().includes(metaTx.status)
+ })
+ if (index !== -1) {
+ transactions.splice(index, 1)
+ }
+ }
+ transactions.push(txMeta)
+ this._saveTxList(transactions)
+ return txMeta
+ }
+ // gets tx by Id and returns it
+ getTx (txId) {
+ const txMeta = this.getTxsByMetaData('id', txId)[0]
+ return txMeta
+ }
+
+ updateTx (txMeta, note) {
+ // validate txParams
+ if (txMeta.txParams) {
+ if (typeof txMeta.txParams.data === 'undefined') {
+ delete txMeta.txParams.data
+ }
+
+ this.validateTxParams(txMeta.txParams)
+ }
+
+ // create txMeta snapshot for history
+ const currentState = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
+ // recover previous tx state obj
+ const previousState = txStateHistoryHelper.replayHistory(txMeta.history)
+ // generate history entry and add to history
+ const entry = txStateHistoryHelper.generateHistoryEntry(previousState, currentState, note)
+ txMeta.history.push(entry)
+
+ // commit txMeta to state
+ const txId = txMeta.id
+ const txList = this.getFullTxList()
+ const index = txList.findIndex(txData => txData.id === txId)
+ txList[index] = txMeta
+ this._saveTxList(txList)
+ }
+
+
+ // merges txParams obj onto txData.txParams
+ // use extend to ensure that all fields are filled
+ updateTxParams (txId, txParams) {
+ const txMeta = this.getTx(txId)
+ txMeta.txParams = extend(txMeta.txParams, txParams)
+ this.updateTx(txMeta, `txStateManager#updateTxParams`)
+ }
+
+ // validates txParams members by type
+ validateTxParams(txParams) {
+ Object.keys(txParams).forEach((key) => {
+ const value = txParams[key]
+ // validate types
+ switch (key) {
+ case 'chainId':
+ if (typeof value !== 'number' && typeof value !== 'string') throw new Error(`${key} in txParams is not a Number or hex string. got: (${value})`)
+ break
+ default:
+ if (typeof value !== 'string') throw new Error(`${key} in txParams is not a string. got: (${value})`)
+ if (!ethUtil.isHexPrefixed(value)) throw new Error(`${key} in txParams is not hex prefixed. got: (${value})`)
+ break
+ }
+ })
+ }
+
+/*
+ Takes an object of fields to search for eg:
+ let thingsToLookFor = {
+ to: '0x0..',
+ from: '0x0..',
+ status: 'signed',
+ err: undefined,
+ }
+ and returns a list of tx with all
+ options matching
+
+ ****************HINT****************
+ | `err: undefined` is like looking |
+ | for a tx with no err |
+ | so you can also search txs that |
+ | dont have something as well by |
+ | setting the value as undefined |
+ ************************************
+
+ this is for things like filtering a the tx list
+ for only tx's from 1 account
+ or for filltering for all txs from one account
+ and that have been 'confirmed'
+ */
+ getFilteredTxList (opts, initialList) {
+ let filteredTxList = initialList
+ Object.keys(opts).forEach((key) => {
+ filteredTxList = this.getTxsByMetaData(key, opts[key], filteredTxList)
+ })
+ return filteredTxList
+ }
+
+ getTxsByMetaData (key, value, txList = this.getTxList()) {
+ return txList.filter((txMeta) => {
+ if (txMeta.txParams[key]) {
+ return txMeta.txParams[key] === value
+ } else {
+ return txMeta[key] === value
+ }
+ })
+ }
+
+ // get::set status
+
+ // should return the status of the tx.
+ getTxStatus (txId) {
+ const txMeta = this.getTx(txId)
+ return txMeta.status
+ }
+
+ // should update the status of the tx to 'rejected'.
+ setTxStatusRejected (txId) {
+ this._setTxStatus(txId, 'rejected')
+ }
+
+ // should update the status of the tx to 'unapproved'.
+ setTxStatusUnapproved (txId) {
+ this._setTxStatus(txId, 'unapproved')
+ }
+ // should update the status of the tx to 'approved'.
+ setTxStatusApproved (txId) {
+ this._setTxStatus(txId, 'approved')
+ }
+
+ // should update the status of the tx to 'signed'.
+ setTxStatusSigned (txId) {
+ this._setTxStatus(txId, 'signed')
+ }
+
+ // should update the status of the tx to 'submitted'.
+ // and add a time stamp for when it was called
+ setTxStatusSubmitted (txId) {
+ const txMeta = this.getTx(txId)
+ txMeta.submittedTime = (new Date()).getTime()
+ this.updateTx(txMeta, 'txStateManager - add submitted time stamp')
+ this._setTxStatus(txId, 'submitted')
+ }
+
+ // should update the status of the tx to 'confirmed'.
+ setTxStatusConfirmed (txId) {
+ this._setTxStatus(txId, 'confirmed')
+ }
+
+ // should update the status dropped
+ setTxStatusDropped (txId) {
+ this._setTxStatus(txId, 'dropped')
+ }
+
+
+ setTxStatusFailed (txId, err) {
+ const txMeta = this.getTx(txId)
+ txMeta.err = {
+ message: err.toString(),
+ stack: err.stack,
+ }
+ this.updateTx(txMeta)
+ this._setTxStatus(txId, 'failed')
+ }
+
+ // returns an array of states that can be considered final
+ getFinalStates () {
+ return [
+ 'rejected', // the user has responded no!
+ 'confirmed', // the tx has been included in a block.
+ 'failed', // the tx failed for some reason, included on tx data.
+ 'dropped', // the tx nonce was already used
+ ]
+ }
+
+ wipeTransactions (address) {
+ // network only tx
+ const txs = this.getFullTxList()
+ const network = this.getNetwork()
+
+ // Filter out the ones from the current account and network
+ const otherAccountTxs = txs.filter((txMeta) => !(txMeta.txParams.from === address && txMeta.metamaskNetworkId === network))
+
+ // Update state
+ this._saveTxList(otherAccountTxs)
+ }
+//
+// PRIVATE METHODS
+//
+
+ // STATUS METHODS
+ // statuses:
+ // - `'unapproved'` the user has not responded
+ // - `'rejected'` the user has responded no!
+ // - `'approved'` the user has approved the tx
+ // - `'signed'` the tx is signed
+ // - `'submitted'` the tx is sent to a server
+ // - `'confirmed'` the tx has been included in a block.
+ // - `'failed'` the tx failed for some reason, included on tx data.
+ // - `'dropped'` the tx nonce was already used
+ _setTxStatus (txId, status) {
+ const txMeta = this.getTx(txId)
+ txMeta.status = status
+ this.emit(`${txMeta.id}:${status}`, txId)
+ this.emit(`tx:status-update`, txId, status)
+ if (['submitted', 'rejected', 'failed'].includes(status)) {
+ this.emit(`${txMeta.id}:finished`, txMeta)
+ }
+ this.updateTx(txMeta, `txStateManager: setting status to ${status}`)
+ this.emit('update:badge')
+ }
+
+ // Saves the new/updated txList.
+ // Function is intended only for internal use
+ _saveTxList (transactions) {
+ this.store.updateState({ transactions })
+ }
+}