Merge pull request #2035 from MetaMask/transactionControllerRefractorPt3

Transaction controller refractor pt3
feature/default_network_editable
kumavis 7 years ago committed by GitHub
commit 5bbea78306
  1. 2
      app/scripts/background.js
  2. 15
      app/scripts/controllers/balance.js
  3. 363
      app/scripts/controllers/transactions.js
  4. 26
      app/scripts/lib/pending-tx-tracker.js
  5. 22
      app/scripts/lib/tx-gas-utils.js
  6. 245
      app/scripts/lib/tx-state-manager.js
  7. 3
      test/unit/components/pending-tx-test.js
  8. 18
      test/unit/pending-tx-test.js
  9. 434
      test/unit/tx-controller-test.js
  10. 241
      test/unit/tx-state-manager-test.js
  11. 6
      test/unit/tx-utils-test.js

@ -114,7 +114,7 @@ function setupController (initState) {
//
updateBadge()
controller.txController.on('updateBadge', updateBadge)
controller.txController.on('update:badge', updateBadge)
controller.messageManager.on('updateBadge', updateBadge)
controller.personalMessageManager.on('updateBadge', updateBadge)

@ -33,9 +33,18 @@ class BalanceController {
_registerUpdates () {
const update = this.updateBalance.bind(this)
this.txController.on('submitted', update)
this.txController.on('confirmed', update)
this.txController.on('failed', update)
this.txController.on('tx:status-update', (txId, status) => {
switch (status) {
case 'submitted':
case 'confirmed':
case 'failed':
update()
return
default:
return
}
})
this.accountTracker.store.subscribe(update)
this.blockTracker.on('block', update)
}

@ -1,74 +1,85 @@
const EventEmitter = require('events')
const extend = require('xtend')
const ObservableStore = require('obs-store')
const ethUtil = require('ethereumjs-util')
const Transaction = require('ethereumjs-tx')
const EthQuery = require('ethjs-query')
const TxProviderUtil = require('../lib/tx-utils')
const TransactionStateManger = require('../lib/tx-state-manager')
const TxGasUtil = require('../lib/tx-gas-utils')
const PendingTransactionTracker = require('../lib/pending-tx-tracker')
const createId = require('../lib/random-id')
const NonceTracker = require('../lib/nonce-tracker')
const txStateHistoryHelper = require('../lib/tx-state-history-helper')
/*
Transaction Controller is an aggregate of sub-controllers and trackers
composing them in a way to be exposed to the metamask controller
- txStateManager
responsible for the state of a transaction and
storing the transaction
- pendingTxTracker
watching blocks for transactions to be include
and emitting confirmed events
- txGasUtil
gas calculations and safety buffering
- nonceTracker
calculating nonces
*/
module.exports = class TransactionController extends EventEmitter {
constructor (opts) {
super()
this.store = new ObservableStore(extend({
transactions: [],
}, opts.initState))
this.memStore = new ObservableStore({})
this.networkStore = opts.networkStore || new ObservableStore({})
this.preferencesStore = opts.preferencesStore || new ObservableStore({})
this.txHistoryLimit = opts.txHistoryLimit
this.provider = opts.provider
this.blockTracker = opts.blockTracker
this.signEthTx = opts.signTransaction
this.accountTracker = opts.accountTracker
this.memStore = new ObservableStore({})
this.query = new EthQuery(this.provider)
this.txGasUtil = new TxGasUtil(this.provider)
this.txStateManager = new TransactionStateManger({
initState: opts.initState,
txHistoryLimit: opts.txHistoryLimit,
getNetwork: this.getNetwork.bind(this),
})
this.store = this.txStateManager.store
this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update'))
this.nonceTracker = new NonceTracker({
provider: this.provider,
getPendingTransactions: (address) => {
return this.getFilteredTxList({
from: address,
status: 'submitted',
err: undefined,
})
},
getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager),
getConfirmedTransactions: (address) => {
return this.getFilteredTxList({
return this.txStateManager.getFilteredTxList({
from: address,
status: 'confirmed',
err: undefined,
})
},
giveUpOnTransaction: (txId) => {
const msg = `Gave up submitting after 3500 blocks un-mined.`
this.setTxStatusFailed(txId, msg)
},
})
this.query = new EthQuery(this.provider)
this.txProviderUtil = new TxProviderUtil(this.provider)
this.pendingTxTracker = new PendingTransactionTracker({
provider: this.provider,
nonceTracker: this.nonceTracker,
retryLimit: 3500, // Retry 3500 blocks, or about 1 day.
getBalance: (address) => {
const account = this.accountTracker.store.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,
})
},
publishTransaction: (rawTx) => this.query.sendRawTransaction(rawTx),
getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager),
})
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.txStateManager.store.subscribe(() => this.emit('update:badge'))
this.pendingTxTracker.on('tx:warning', this.txStateManager.updateTx.bind(this.txStateManager))
this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager))
this.pendingTxTracker.on('tx:confirmed', this.txStateManager.setTxStatusConfirmed.bind(this.txStateManager))
this.pendingTxTracker.on('tx:retry', (txMeta) => {
if (!('retryCount' in txMeta)) txMeta.retryCount = 0
txMeta.retryCount++
this.txStateManager.updateTx(txMeta)
})
this.blockTracker.on('block', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker))
// this is a little messy but until ethstore has been either
@ -80,7 +91,7 @@ module.exports = class TransactionController extends EventEmitter {
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())
this.txStateManager.store.subscribe(() => this._updateMemstore())
this.networkStore.subscribe(() => this._updateMemstore())
this.preferencesStore.subscribe(() => this._updateMemstore())
}
@ -97,98 +108,31 @@ module.exports = class TransactionController extends EventEmitter {
return this.preferencesStore.getState().selectedAddress
}
// Returns the number of txs for the current network.
getTxCount () {
return this.getTxList().length
}
// Returns the full tx list across all networks
getFullTxList () {
return this.store.getState().transactions
}
getUnapprovedTxCount () {
return Object.keys(this.getUnapprovedTxList()).length
return Object.keys(this.txStateManager.getUnapprovedTxList()).length
}
getPendingTxCount () {
return this.getTxsByMetaData('status', 'signed').length
getPendingTxCount (account) {
return this.txStateManager.getPendingTransactions(account).length
}
// Returns the tx list
getTxList () {
const network = this.getNetwork()
const fullTxList = this.getFullTxList()
return this.getTxsByMetaData('metamaskNetworkId', network, fullTxList)
getFilteredTxList (opts) {
return this.txStateManager.getFilteredTxList(opts)
}
// gets tx by Id and returns it
getTx (txId) {
const txList = this.getTxList()
const txMeta = txList.find(txData => txData.id === txId)
return txMeta
}
getUnapprovedTxList () {
const txList = this.getTxList()
return txList.filter((txMeta) => txMeta.status === 'unapproved')
.reduce((result, tx) => {
result[tx.id] = tx
return result
}, {})
getChainId () {
const networkState = this.networkStore.getState()
const getChainId = parseInt(networkState)
if (Number.isNaN(getChainId)) {
return 0
} else {
return getChainId
}
updateTx (txMeta) {
// 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)
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)
this.emit('update')
}
// Adds a tx to the txlist
addTx (txMeta) {
// initialize history
txMeta.history = []
// capture initial snapshot of txMeta for history
const snapshot = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
txMeta.history.push(snapshot)
// 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
const txCount = this.getTxCount()
const network = this.getNetwork()
const fullTxList = this.getFullTxList()
const txHistoryLimit = this.txHistoryLimit
if (txCount > txHistoryLimit - 1) {
const index = fullTxList.findIndex((metaTx) => ((metaTx.status === 'confirmed' || metaTx.status === 'rejected') && network === txMeta.metamaskNetworkId))
fullTxList.splice(index, 1)
}
fullTxList.push(txMeta)
this._saveTxList(fullTxList)
this.emit('update')
this.once(`${txMeta.id}:signed`, function (txId) {
this.removeAllListeners(`${txMeta.id}:rejected`)
})
this.once(`${txMeta.id}:rejected`, function (txId) {
this.removeAllListeners(`${txMeta.id}:signed`)
})
this.emit('updateBadge')
this.txStateManager.addTx(txMeta)
this.emit(`${txMeta.id}:unapproved`, txMeta)
}
@ -198,7 +142,7 @@ module.exports = class TransactionController extends EventEmitter {
this.emit('newUnaprovedTx', txMeta)
// listen for tx completion (success, fail)
return new Promise((resolve, reject) => {
this.once(`${txMeta.id}:finished`, (completedTx) => {
this.txStateManager.once(`${txMeta.id}:finished`, (completedTx) => {
switch (completedTx.status) {
case 'submitted':
return resolve(completedTx.hash)
@ -213,7 +157,7 @@ module.exports = class TransactionController extends EventEmitter {
async addUnapprovedTransaction (txParams) {
// validate
await this.txProviderUtil.validateTxParams(txParams)
await this.txGasUtil.validateTxParams(txParams)
// construct txMeta
const txMeta = {
id: createId(),
@ -232,17 +176,15 @@ module.exports = class TransactionController extends EventEmitter {
async addTxDefaults (txMeta) {
const txParams = txMeta.txParams
// ensure value
const gasPrice = txParams.gasPrice || await this.query.gasPrice()
txParams.value = txParams.value || '0x0'
if (!txParams.gasPrice) {
const gasPrice = await this.query.gasPrice()
txParams.gasPrice = gasPrice
}
txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16))
// set gasLimit
return await this.txProviderUtil.analyzeGasUsage(txMeta)
return await this.txGasUtil.analyzeGasUsage(txMeta)
}
async updateAndApproveTransaction (txMeta) {
this.updateTx(txMeta)
this.txStateManager.updateTx(txMeta)
await this.approveTransaction(txMeta.id)
}
@ -250,24 +192,24 @@ module.exports = class TransactionController extends EventEmitter {
let nonceLock
try {
// approve
this.setTxStatusApproved(txId)
this.txStateManager.setTxStatusApproved(txId)
// get next nonce
const txMeta = this.getTx(txId)
const txMeta = this.txStateManager.getTx(txId)
const fromAddress = txMeta.txParams.from
// wait for a nonce
nonceLock = await this.nonceTracker.getNonceLock(fromAddress)
// add nonce to txParams
txMeta.txParams.nonce = nonceLock.nextNonce
txMeta.txParams.nonce = ethUtil.addHexPrefix(nonceLock.nextNonce.toString(16))
// add nonce debugging information to txMeta
txMeta.nonceDetails = nonceLock.nonceDetails
this.updateTx(txMeta)
this.txStateManager.updateTx(txMeta)
// sign transaction
const rawTx = await this.signTransaction(txId)
await this.publishTransaction(txId, rawTx)
// must set transaction to submitted/failed before releasing lock
nonceLock.releaseLock()
} catch (err) {
this.setTxStatusFailed(txId, err)
this.txStateManager.setTxStatusFailed(txId, err)
// must set transaction to submitted/failed before releasing lock
if (nonceLock) nonceLock.releaseLock()
// continue with error chain
@ -276,181 +218,46 @@ module.exports = class TransactionController extends EventEmitter {
}
async signTransaction (txId) {
const txMeta = this.getTx(txId)
const txMeta = this.txStateManager.getTx(txId)
const txParams = txMeta.txParams
const fromAddress = txParams.from
// add network/chain id
txParams.chainId = this.getChainId()
const ethTx = this.txProviderUtil.buildEthTxFromParams(txParams)
txParams.chainId = ethUtil.addHexPrefix(this.getChainId().toString(16))
const ethTx = new Transaction(txParams)
await this.signEthTx(ethTx, fromAddress)
this.setTxStatusSigned(txMeta.id)
this.txStateManager.setTxStatusSigned(txMeta.id)
const rawTx = ethUtil.bufferToHex(ethTx.serialize())
return rawTx
}
async publishTransaction (txId, rawTx) {
const txMeta = this.getTx(txId)
const txMeta = this.txStateManager.getTx(txId)
txMeta.rawTx = rawTx
this.updateTx(txMeta)
const txHash = await this.txProviderUtil.publishTransaction(rawTx)
this.txStateManager.updateTx(txMeta)
const txHash = await this.query.sendRawTransaction(rawTx)
this.setTxHash(txId, txHash)
this.setTxStatusSubmitted(txId)
this.txStateManager.setTxStatusSubmitted(txId)
}
async cancelTransaction (txId) {
this.setTxStatusRejected(txId)
}
getChainId () {
const networkState = this.networkStore.getState()
const getChainId = parseInt(networkState)
if (Number.isNaN(getChainId)) {
return 0
} else {
return getChainId
}
this.txStateManager.setTxStatusRejected(txId)
}
// receives a txHash records the tx as signed
setTxHash (txId, txHash) {
// Add the tx hash to the persisted meta-tx object
const txMeta = this.getTx(txId)
const txMeta = this.txStateManager.getTx(txId)
txMeta.hash = txHash
this.updateTx(txMeta)
}
/*
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) {
let filteredTxList
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
}
})
}
// STATUS METHODS
// 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 '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'.
setTxStatusSubmitted (txId) {
this._setTxStatus(txId, 'submitted')
}
// should update the status of the tx to 'confirmed'.
setTxStatusConfirmed (txId) {
this._setTxStatus(txId, 'confirmed')
this.txStateManager.updateTx(txMeta)
}
setTxStatusFailed (txId, err) {
const txMeta = this.getTx(txId)
txMeta.err = {
message: err.toString(),
stack: err.stack,
}
this.updateTx(txMeta)
this._setTxStatus(txId, 'failed')
}
// 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)
}
/* _____________________________________
| |
| PRIVATE METHODS |
|______________________________________*/
// Should find the tx in the tx list and
// update it.
// should set the status in txData
// - `'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.
_setTxStatus (txId, status) {
const txMeta = this.getTx(txId)
txMeta.status = status
this.emit(`${txMeta.id}:${status}`, txId)
this.emit(`${status}`, txId)
if (status === 'submitted' || status === 'rejected') {
this.emit(`${txMeta.id}:finished`, txMeta)
}
this.updateTx(txMeta)
this.emit('updateBadge')
}
// Saves the new/updated txList.
// Function is intended only for internal use
_saveTxList (transactions) {
this.store.updateState({ transactions })
}
//
// PRIVATE METHODS
//
_updateMemstore () {
const unapprovedTxs = this.getUnapprovedTxList()
const selectedAddressTxList = this.getFilteredTxList({
const unapprovedTxs = this.txStateManager.getUnapprovedTxList()
const selectedAddressTxList = this.txStateManager.getFilteredTxList({
from: this.getSelectedAddress(),
metamaskNetworkId: this.getNetwork(),
})

@ -1,7 +1,6 @@
const EventEmitter = require('events')
const EthQuery = require('ethjs-query')
const sufficientBalance = require('./util').sufficientBalance
const RETRY_LIMIT = 3500 // Retry 3500 blocks, or about 1 day.
/*
Utility class for tracking the transactions as they
@ -25,11 +24,10 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
super()
this.query = new EthQuery(config.provider)
this.nonceTracker = config.nonceTracker
this.retryLimit = config.retryLimit || Infinity
this.getBalance = config.getBalance
this.getPendingTransactions = config.getPendingTransactions
this.publishTransaction = config.publishTransaction
this.giveUpOnTransaction = config.giveUpOnTransaction
}
// checks if a signed tx is in a block and
@ -44,13 +42,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
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)
this.emit('tx:failed', txId, noTxHashErr)
return
}
block.transactions.forEach((tx) => {
if (tx.hash === txHash) this.emit('txConfirmed', txId)
if (tx.hash === txHash) this.emit('tx:confirmed', txId)
})
})
}
@ -96,7 +94,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
// ignore resubmit warnings, return early
if (isKnownTx) return
// encountered real error - transition to error state
this.emit('txFailed', txMeta.id, err)
this.emit('tx:failed', txMeta.id, err)
}))
}
@ -104,16 +102,16 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
const address = txMeta.txParams.from
const balance = this.getBalance(address)
if (balance === undefined) return
if (!('retryCount' in txMeta)) txMeta.retryCount = 0
if (txMeta.retryCount > RETRY_LIMIT) {
return this.giveUpOnTransaction(txMeta.id)
if (txMeta.retryCount > this.retryLimit) {
const err = new Error(`Gave up submitting after ${this.retryLimit} blocks un-mined.`)
return this.emit('tx:failed', txMeta.id, err)
}
// 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)
this.emit('tx:failed', txMeta.id, insufficientFundsError)
log.error(insufficientFundsError)
return
}
@ -125,7 +123,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
const txHash = await this.publishTransaction(rawTx)
// Increment successful tries:
txMeta.retryCount++
this.emit('tx:retry', txMeta)
return txHash
}
@ -137,7 +135,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
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)
this.emit('tx:failed', txId, noTxHashErr)
return
}
// get latest transaction status
@ -146,14 +144,14 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
txParams = await this.query.getTransactionByHash(txHash)
if (!txParams) return
if (txParams.blockNumber) {
this.emit('txConfirmed', txId)
this.emit('tx:confirmed', txId)
}
} catch (err) {
txMeta.warning = {
error: err,
message: 'There was a problem loading this transaction.',
}
this.emit('txWarning', txMeta)
this.emit('tx:warning', txMeta)
throw err
}
}

@ -1,6 +1,4 @@
const EthQuery = require('ethjs-query')
const Transaction = require('ethereumjs-tx')
const normalize = require('eth-sig-util').normalize
const {
hexToBn,
BnMultiplyByFraction,
@ -78,26 +76,6 @@ module.exports = class txProvideUtil {
return bnToHex(upperGasLimitBn)
}
// builds ethTx from txParams object
buildEthTxFromParams (txParams) {
// normalize values
txParams.to = normalize(txParams.to)
txParams.from = normalize(txParams.from)
txParams.value = normalize(txParams.value)
txParams.data = normalize(txParams.data)
txParams.gas = normalize(txParams.gas || txParams.gasLimit)
txParams.gasPrice = normalize(txParams.gasPrice)
txParams.nonce = normalize(txParams.nonce)
// build ethTx
log.info(`Prepared tx for signing: ${JSON.stringify(txParams)}`)
const ethTx = new Transaction(txParams)
return ethTx
}
async publishTransaction (rawTx) {
return await this.query.sendRawTransaction(rawTx)
}
async validateTxParams (txParams) {
if (('value' in txParams) && txParams.value.indexOf('-') === 0) {
throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)

@ -0,0 +1,245 @@
const extend = require('xtend')
const EventEmitter = require('events')
const ObservableStore = require('obs-store')
const ethUtil = require('ethereumjs-util')
const txStateHistoryHelper = require('./tx-state-history-helper')
module.exports = class TransactionStateManger extends EventEmitter {
constructor ({ initState, txHistoryLimit, getNetwork }) {
super()
this.store = new ObservableStore(
extend({
transactions: [],
}, initState))
this.txHistoryLimit = txHistoryLimit
this.getNetwork = getNetwork
}
// Returns the number of txs for the current network.
getTxCount () {
return this.getTxList().length
}
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)
}
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 = this.getTxCount()
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) {
const index = transactions.findIndex((metaTx) => metaTx.status === 'confirmed' || metaTx.status === 'rejected')
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) {
if (txMeta.txParams) {
Object.keys(txMeta.txParams).forEach((key) => {
let value = txMeta.txParams[key]
if (typeof value !== 'string') console.error(`${key}: ${value} in txParams is not a string`)
if (!ethUtil.isHexPrefixed(value)) console.error('is not hex prefixed, anything on txParams must be hex prefixed')
})
}
// 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)
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)
}
/*
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
}
})
}
// 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.
// 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 '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'.
setTxStatusSubmitted (txId) {
this._setTxStatus(txId, 'submitted')
}
// should update the status of the tx to 'confirmed'.
setTxStatusConfirmed (txId) {
this._setTxStatus(txId, 'confirmed')
}
setTxStatusFailed (txId, err) {
const txMeta = this.getTx(txId)
txMeta.err = {
message: err.toString(),
stack: err.stack,
}
this.updateTx(txMeta)
this._setTxStatus(txId, 'failed')
}
//
// PRIVATE METHODS
//
// Should find the tx in the tx list and
// update it.
// should set the status in txData
// - `'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.
_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 (status === 'submitted' || status === 'rejected') {
this.emit(`${txMeta.id}:finished`, txMeta)
}
this.updateTx(txMeta)
this.emit('update:badge')
}
// Saves the new/updated txList.
// Function is intended only for internal use
_saveTxList (transactions) {
this.store.updateState({ transactions })
}
}

@ -24,7 +24,8 @@ describe('PendingTx', function () {
'to': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
'value': '0xde0b6b3a7640000',
gasPrice,
'gas': '0x7b0c'},
'gas': '0x7b0c',
},
'gasLimitSpecified': false,
'estimatedGas': '0x5208',
}

@ -62,7 +62,7 @@ describe('PendingTransactionTracker', function () {
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) => {
pendingTxTracker.once('tx:failed', (txId, err) => {
assert(txId, txMetaNoHash.id, 'should pass txId')
done()
})
@ -71,11 +71,11 @@ describe('PendingTransactionTracker', function () {
it('should emit \'txConfirmed\' if the tx is in the block', function (done) {
const block = { transactions: [txMeta]}
pendingTxTracker.getPendingTransactions = () => [txMeta]
pendingTxTracker.once('txConfirmed', (txId) => {
pendingTxTracker.once('tx:confirmed', (txId) => {
assert(txId, txMeta.id, 'should pass txId')
done()
})
pendingTxTracker.once('txFailed', (_, err) => { done(err) })
pendingTxTracker.once('tx:failed', (_, err) => { done(err) })
pendingTxTracker.checkForTxInBlock(block)
})
})
@ -108,7 +108,7 @@ describe('PendingTransactionTracker', function () {
describe('#_checkPendingTx', function () {
it('should emit \'txFailed\' if the txMeta does not have a hash', function (done) {
pendingTxTracker.once('txFailed', (txId, err) => {
pendingTxTracker.once('tx:failed', (txId, err) => {
assert(txId, txMetaNoHash.id, 'should pass txId')
done()
})
@ -122,11 +122,11 @@ describe('PendingTransactionTracker', function () {
it('should emit \'txConfirmed\'', function (done) {
providerResultStub.eth_getTransactionByHash = {blockNumber: '0x01'}
pendingTxTracker.once('txConfirmed', (txId) => {
pendingTxTracker.once('tx:confirmed', (txId) => {
assert(txId, txMeta.id, 'should pass txId')
done()
})
pendingTxTracker.once('txFailed', (_, err) => { done(err) })
pendingTxTracker.once('tx:failed', (_, err) => { done(err) })
pendingTxTracker._checkPendingTx(txMeta)
})
})
@ -188,7 +188,7 @@ describe('PendingTransactionTracker', function () {
]
const enoughForAllErrors = txList.concat(txList)
pendingTxTracker.on('txFailed', (_, err) => done(err))
pendingTxTracker.on('tx:failed', (_, err) => done(err))
pendingTxTracker.getPendingTransactions = () => enoughForAllErrors
pendingTxTracker._resubmitTx = async (tx) => {
@ -202,7 +202,7 @@ describe('PendingTransactionTracker', function () {
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.once('tx:failed', (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') }
@ -226,7 +226,7 @@ describe('PendingTransactionTracker', function () {
// Stubbing out current account state:
// Adding the fake tx:
pendingTxTracker.once('txFailed', (txId, err) => {
pendingTxTracker.once('tx:failed', (txId, err) => {
assert(err, 'Should have a error')
done()
})

@ -2,21 +2,19 @@ const assert = require('assert')
const ethUtil = require('ethereumjs-util')
const EthTx = require('ethereumjs-tx')
const ObservableStore = require('obs-store')
const clone = require('clone')
const sinon = require('sinon')
const TransactionController = require('../../app/scripts/controllers/transactions')
const TxProvideUtils = require('../../app/scripts/lib/tx-utils')
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper')
const TxGasUtils = require('../../app/scripts/lib/tx-gas-utils')
const { createStubedProvider } = require('../stub/provider')
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, engine, provider, providerResultStub
let txController, provider, providerResultStub
beforeEach(function () {
providerResultStub = {}
@ -27,34 +25,97 @@ describe('Transaction Controller', function () {
networkStore: new ObservableStore(currentNetworkId),
txHistoryLimit: 10,
blockTracker: { getCurrentBlock: noop, on: noop, once: noop },
accountTracker: { getState: noop },
accountTracker: { store: { getState: noop } },
signTransaction: (ethTx) => new Promise((resolve) => {
ethTx.sign(privKey)
resolve()
}),
})
txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop })
txController.txProviderUtils = new TxProvideUtils(txController.provider)
txController.txProviderUtils = new TxGasUtils(txController.provider)
})
describe('#getState', function () {
it('should return a state object with the right keys and datat types', function () {
const exposedState = txController.getState()
assert('unapprovedTxs' in exposedState, 'state should have the key unapprovedTxs')
assert('selectedAddressTxList' in exposedState, 'state should have the key selectedAddressTxList')
assert(typeof exposedState.unapprovedTxs === 'object', 'should be an object')
assert(Array.isArray(exposedState.selectedAddressTxList), 'should be an array')
})
})
describe('#getUnapprovedTxCount', function () {
it('should return the number of unapproved txs', function () {
txController.txStateManager._saveTxList([
{ id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} },
{ id: 2, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} },
{ id: 3, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} },
])
const unapprovedTxCount = txController.getUnapprovedTxCount()
assert.equal(unapprovedTxCount, 3, 'should be 3')
})
})
describe('#getPendingTxCount', function () {
it('should return the number of pending txs', function () {
txController.txStateManager._saveTxList([
{ id: 1, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {} },
{ id: 2, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {} },
{ id: 3, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {} },
])
const pendingTxCount = txController.getPendingTxCount()
assert.equal(pendingTxCount, 3, 'should be 3')
})
})
describe('#getConfirmedTransactions', function () {
let address
beforeEach(function () {
address = '0xc684832530fcbddae4b4230a47e991ddcec2831d'
const txParams = {
'from': address,
'to': '0xc684832530fcbddae4b4230a47e991ddcec2831d',
}
txController.txStateManager._saveTxList([
{id: 0, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams},
{id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams},
{id: 2, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams},
{id: 3, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams},
{id: 4, status: 'rejected', metamaskNetworkId: currentNetworkId, txParams},
{id: 5, status: 'approved', metamaskNetworkId: currentNetworkId, txParams},
{id: 6, status: 'signed', metamaskNetworkId: currentNetworkId, txParams},
{id: 7, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams},
{id: 8, status: 'failed', metamaskNetworkId: currentNetworkId, txParams},
])
})
it('should return the number of confirmed txs', function () {
assert.equal(txController.nonceTracker.getConfirmedTransactions(address).length, 3)
})
})
describe('#newUnapprovedTransaction', function () {
let stub, txMeta, txParams
beforeEach(function () {
txParams = {
'from':'0xc684832530fcbddae4b4230a47e991ddcec2831d',
'to':'0xc684832530fcbddae4b4230a47e991ddcec2831d',
},
'from': '0xc684832530fcbddae4b4230a47e991ddcec2831d',
'to': '0xc684832530fcbddae4b4230a47e991ddcec2831d',
}
txMeta = {
status: 'unapproved',
id: 1,
metamaskNetworkId: currentNetworkId,
txParams,
history: [],
}
txController.addTx(txMeta)
stub = sinon.stub(txController, 'addUnapprovedTransaction').returns(Promise.resolve(txMeta))
txController.txStateManager._saveTxList([txMeta])
stub = sinon.stub(txController, 'addUnapprovedTransaction').returns(Promise.resolve(txController.txStateManager.addTx(txMeta)))
})
afterEach(function () {
txController.txStateManager._saveTxList([])
stub.restore()
})
@ -72,7 +133,7 @@ describe('Transaction Controller', function () {
txController.once('newUnaprovedTx', (txMetaFromEmit) => {
setTimeout(() => {
txController.setTxHash(txMetaFromEmit.id, '0x0')
txController.setTxStatusSubmitted(txMetaFromEmit.id)
txController.txStateManager.setTxStatusSubmitted(txMetaFromEmit.id)
}, 10)
})
@ -87,7 +148,7 @@ describe('Transaction Controller', function () {
it('should reject when finished and status is rejected', function (done) {
txController.once('newUnaprovedTx', (txMetaFromEmit) => {
setTimeout(() => {
txController.setTxStatusRejected(txMetaFromEmit.id)
txController.txStateManager.setTxStatusRejected(txMetaFromEmit.id)
}, 10)
})
@ -110,7 +171,7 @@ describe('Transaction Controller', function () {
assert(('txParams' in txMeta), 'should have a txParams')
assert(('history' in txMeta), 'should have a history')
const memTxMeta = txController.getTx(txMeta.id)
const memTxMeta = txController.txStateManager.getTx(txMeta.id)
assert.deepEqual(txMeta, memTxMeta, `txMeta should be stored in txController after adding it\n expected: ${txMeta} \n got: ${memTxMeta}`)
addTxDefaultsStub.restore()
done()
@ -120,10 +181,10 @@ describe('Transaction Controller', function () {
describe('#addTxDefaults', function () {
it('should add the tx defaults if their are none', function (done) {
let txMeta = {
const txMeta = {
'txParams': {
'from':'0xc684832530fcbddae4b4230a47e991ddcec2831d',
'to':'0xc684832530fcbddae4b4230a47e991ddcec2831d',
'from': '0xc684832530fcbddae4b4230a47e991ddcec2831d',
'to': '0xc684832530fcbddae4b4230a47e991ddcec2831d',
},
}
providerResultStub.eth_gasPrice = '4a817c800'
@ -131,7 +192,7 @@ describe('Transaction Controller', function () {
providerResultStub.eth_estimateGas = '5209'
txController.addTxDefaults(txMeta)
.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')
assert(txMetaWithDefaults.txParams.gasPrice, 'should have added the gas price')
assert(txMetaWithDefaults.txParams.gas, 'should have added the gas field')
done()
@ -163,214 +224,31 @@ describe('Transaction Controller', function () {
})
})
describe('#getTxList', function () {
it('when new should return empty array', function () {
var result = txController.getTxList()
assert.ok(Array.isArray(result))
assert.equal(result.length, 0)
})
})
describe('#addTx', function () {
it('adds a tx returned in getTxList', function () {
var tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }
txController.addTx(tx, noop)
var result = txController.getTxList()
assert.ok(Array.isArray(result))
assert.equal(result.length, 1)
assert.equal(result[0].id, 1)
})
it('does not override txs from other networks', function () {
var tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }
var tx2 = { id: 2, status: 'confirmed', metamaskNetworkId: otherNetworkId, txParams: {} }
txController.addTx(tx, noop)
txController.addTx(tx2, noop)
var result = txController.getFullTxList()
var result2 = txController.getTxList()
assert.equal(result.length, 2, 'txs were deleted')
assert.equal(result2.length, 1, 'incorrect number of txs on network.')
})
it('cuts off early txs beyond a limit', function () {
const limit = txController.txHistoryLimit
for (let i = 0; i < limit + 1; i++) {
const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }
txController.addTx(tx, noop)
}
var result = txController.getTxList()
assert.equal(result.length, limit, `limit of ${limit} txs enforced`)
assert.equal(result[0].id, 1, 'early txs truncted')
})
it('cuts off early txs beyond a limit whether or not it is confirmed or rejected', function () {
const limit = txController.txHistoryLimit
for (let i = 0; i < limit + 1; i++) {
const tx = { id: i, time: new Date(), status: 'rejected', metamaskNetworkId: currentNetworkId, txParams: {} }
txController.addTx(tx, noop)
}
var result = txController.getTxList()
assert.equal(result.length, limit, `limit of ${limit} txs enforced`)
assert.equal(result[0].id, 1, 'early txs truncted')
})
it('cuts off early txs beyond a limit but does not cut unapproved txs', function () {
var unconfirmedTx = { id: 0, time: new Date(), status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
txController.addTx(unconfirmedTx, noop)
const limit = txController.txHistoryLimit
for (let i = 1; i < limit + 1; i++) {
const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }
txController.addTx(tx, noop)
}
var result = txController.getTxList()
assert.equal(result.length, limit, `limit of ${limit} txs enforced`)
assert.equal(result[0].id, 0, 'first tx should still be there')
assert.equal(result[0].status, 'unapproved', 'first tx should be unapproved')
assert.equal(result[1].id, 2, 'early txs truncted')
})
})
describe('#setTxStatusSigned', function () {
it('sets the tx status to signed', function () {
var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
txController.addTx(tx, noop)
txController.setTxStatusSigned(1)
var result = txController.getTxList()
assert.ok(Array.isArray(result))
assert.equal(result.length, 1)
assert.equal(result[0].status, 'signed')
})
it('should emit a signed event to signal the exciton of callback', (done) => {
this.timeout(10000)
var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
const noop = function () {
assert(true, 'event listener has been triggered and noop executed')
done()
}
txController.addTx(tx)
txController.on('1:signed', noop)
txController.setTxStatusSigned(1)
})
})
describe('#setTxStatusRejected', function () {
it('sets the tx status to rejected', function () {
var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
txController.addTx(tx)
txController.setTxStatusRejected(1)
var result = txController.getTxList()
assert.ok(Array.isArray(result))
assert.equal(result.length, 1)
assert.equal(result[0].status, 'rejected')
})
it('should emit a rejected event to signal the exciton of callback', (done) => {
this.timeout(10000)
var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
txController.addTx(tx)
const noop = function (err, txId) {
assert(true, 'event listener has been triggered and noop executed')
done()
}
txController.on('1:rejected', noop)
txController.setTxStatusRejected(1)
})
})
describe('#updateTx', function () {
it('replaces the tx with the same id', function () {
txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
txController.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
const tx1 = txController.getTx('1')
tx1.status = 'blah'
tx1.hash = 'foo'
txController.updateTx(tx1)
const savedResult = txController.getTx('1')
assert.equal(savedResult.hash, 'foo')
})
it('updates gas price and adds history items', function () {
const originalGasPrice = '0x01'
const desiredGasPrice = '0x02'
it('should emit updates', function (done) {
const txMeta = {
id: '1',
status: 'unapproved',
metamaskNetworkId: currentNetworkId,
txParams: {
gasPrice: originalGasPrice,
},
txParams: {},
}
txController.addTx(txMeta)
const updatedTx = txController.getTx('1')
// verify tx was initialized correctly
assert.equal(updatedTx.history.length, 1, 'one history item (initial)')
assert.equal(Array.isArray(updatedTx.history[0]), false, 'first history item is initial state')
assert.deepEqual(updatedTx.history[0], txStateHistoryHelper.snapshotFromTxMeta(updatedTx), 'first history item is initial state')
// modify value and updateTx
updatedTx.txParams.gasPrice = desiredGasPrice
txController.updateTx(updatedTx)
// check updated value
const result = txController.getTx('1')
assert.equal(result.txParams.gasPrice, desiredGasPrice, 'gas price updated')
// validate history was updated
assert.equal(result.history.length, 2, 'two history items (initial + diff)')
const expectedEntry = { op: 'replace', path: '/txParams/gasPrice', value: desiredGasPrice }
assert.deepEqual(result.history[1], [expectedEntry], 'two history items (initial + diff)')
})
})
describe('#getUnapprovedTxList', function () {
it('returns unapproved txs in a hash', function () {
txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
txController.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
const result = txController.getUnapprovedTxList()
assert.equal(typeof result, 'object')
assert.equal(result['1'].status, 'unapproved')
assert.equal(result['2'], undefined)
const eventNames = ['update:badge', '1:unapproved']
const listeners = []
eventNames.forEach((eventName) => {
listeners.push(new Promise((resolve) => {
txController.once(eventName, (arg) => {
resolve(arg)
})
}))
})
describe('#getTx', function () {
it('returns a tx with the requested id', function () {
txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
txController.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
assert.equal(txController.getTx('1').status, 'unapproved')
assert.equal(txController.getTx('2').status, 'confirmed')
})
})
describe('#getFilteredTxList', function () {
it('returns a tx with the requested data', function () {
const txMetas = [
{ id: 0, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
{ id: 1, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
{ id: 2, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
{ id: 3, status: 'unapproved', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
{ id: 4, status: 'unapproved', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
{ id: 5, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
{ id: 6, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
{ id: 7, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
{ id: 8, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
{ id: 9, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
]
txMetas.forEach((txMeta) => txController.addTx(txMeta, noop))
let filterParams
filterParams = { status: 'unapproved', from: '0xaa' }
assert.equal(txController.getFilteredTxList(filterParams).length, 3, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
filterParams = { status: 'unapproved', to: '0xaa' }
assert.equal(txController.getFilteredTxList(filterParams).length, 2, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
filterParams = { status: 'confirmed', from: '0xbb' }
assert.equal(txController.getFilteredTxList(filterParams).length, 3, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
filterParams = { status: 'confirmed' }
assert.equal(txController.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
filterParams = { from: '0xaa' }
assert.equal(txController.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
filterParams = { to: '0xaa' }
assert.equal(txController.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
Promise.all(listeners)
.then((returnValues) => {
assert.deepEqual(returnValues.pop(), txMeta, 'last event 1:unapproved should return txMeta')
done()
})
.catch(done)
txController.addTx(txMeta)
})
})
@ -404,11 +282,11 @@ describe('Transaction Controller', function () {
const pubStub = sinon.stub(txController, 'publishTransaction').callsFake(() => {
txController.setTxHash('1', originalValue)
txController.setTxStatusSubmitted('1')
txController.txStateManager.setTxStatusSubmitted('1')
})
txController.approveTransaction(txMeta.id).then(() => {
const result = txController.getTx(txMeta.id)
const result = txController.txStateManager.getTx(txMeta.id)
const params = result.txParams
assert.equal(params.gas, originalValue, 'gas unmodified')
@ -431,4 +309,120 @@ describe('Transaction Controller', function () {
}).catch(done)
})
})
describe('#updateAndApproveTransaction', function () {
let txMeta
beforeEach(function () {
txMeta = {
id: 1,
status: 'unapproved',
txParams: {
from: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
to: '0x1678a085c290ebd122dc42cba69373b5953b831d',
gasPrice: '0x77359400',
gas: '0x7b0d',
nonce: '0x4b',
},
metamaskNetworkId: currentNetworkId,
}
})
it('should update and approve transactions', function () {
txController.txStateManager.addTx(txMeta)
txController.updateAndApproveTransaction(txMeta)
const tx = txController.txStateManager.getTx(1)
assert.equal(tx.status, 'approved')
})
})
describe('#getChainId', function () {
it('returns 0 when the chainId is NaN', function () {
txController.networkStore = new ObservableStore(NaN)
assert.equal(txController.getChainId(), 0)
})
})
describe('#cancelTransaction', function () {
beforeEach(function () {
txController.txStateManager._saveTxList([
{ id: 0, status: 'unapproved', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] },
{ id: 1, status: 'rejected', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] },
{ id: 2, status: 'approved', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] },
{ id: 3, status: 'signed', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] },
{ id: 4, status: 'submitted', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] },
{ id: 5, status: 'confirmed', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] },
{ id: 6, status: 'failed', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] },
])
})
it('should set the transaction to rejected from unapproved', async function () {
await txController.cancelTransaction(0)
assert.equal(txController.txStateManager.getTx(0).status, 'rejected')
})
})
describe('#publishTransaction', function () {
let hash, txMeta
beforeEach(function () {
hash = '0x2a5523c6fa98b47b7d9b6c8320179785150b42a16bcff36b398c5062b65657e8'
txMeta = {
id: 1,
status: 'unapproved',
txParams: {},
metamaskNetworkId: currentNetworkId,
}
providerResultStub.eth_sendRawTransaction = hash
})
it('should publish a tx, updates the rawTx when provided a one', async function () {
txController.txStateManager.addTx(txMeta)
await txController.publishTransaction(txMeta.id)
const publishedTx = txController.txStateManager.getTx(1)
assert.equal(publishedTx.hash, hash)
assert.equal(publishedTx.status, 'submitted')
})
})
describe('#getBalance', function () {
it('gets balance', function () {
sinon.stub(txController.accountTracker.store, 'getState').callsFake(() => {
return {
accounts: {
'0x1678a085c290ebd122dc42cba69373b5953b831d': {
address: '0x1678a085c290ebd122dc42cba69373b5953b831d',
balance: '0x00000000000000056bc75e2d63100000',
code: '0x',
nonce: '0x0',
},
'0xc684832530fcbddae4b4230a47e991ddcec2831d': {
address: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
balance: '0x0',
code: '0x',
nonce: '0x0',
},
},
}
})
assert.equal(txController.pendingTxTracker.getBalance('0x1678a085c290ebd122dc42cba69373b5953b831d'), '0x00000000000000056bc75e2d63100000')
assert.equal(txController.pendingTxTracker.getBalance('0xc684832530fcbddae4b4230a47e991ddcec2831d'), '0x0')
})
})
describe('#getPendingTransactions', function () {
beforeEach(function () {
txController.txStateManager._saveTxList([
{ id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} },
{ id: 2, status: 'rejected', metamaskNetworkId: currentNetworkId, txParams: {} },
{ id: 3, status: 'approved', metamaskNetworkId: currentNetworkId, txParams: {} },
{ id: 4, status: 'signed', metamaskNetworkId: currentNetworkId, txParams: {} },
{ id: 5, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {} },
{ id: 6, status: 'confimed', metamaskNetworkId: currentNetworkId, txParams: {} },
{ id: 7, status: 'failed', metamaskNetworkId: currentNetworkId, txParams: {} },
])
})
it('should show only submitted transactions as pending transasction', function () {
assert(txController.pendingTxTracker.getPendingTransactions().length, 1)
assert(txController.pendingTxTracker.getPendingTransactions()[0].status, 'submitted')
})
})
})

@ -0,0 +1,241 @@
const assert = require('assert')
const clone = require('clone')
const ObservableStore = require('obs-store')
const TxStateManager = require('../../app/scripts/lib/tx-state-manager')
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper')
const noop = () => true
describe('TransactionStateManger', function () {
let txStateManager
const currentNetworkId = 42
const otherNetworkId = 2
beforeEach(function () {
txStateManager = new TxStateManager({
initState: {
transactions: [],
},
txHistoryLimit: 10,
getNetwork: () => currentNetworkId
})
})
describe('#setTxStatusSigned', function () {
it('sets the tx status to signed', function () {
let tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
txStateManager.addTx(tx, noop)
txStateManager.setTxStatusSigned(1)
let result = txStateManager.getTxList()
assert.ok(Array.isArray(result))
assert.equal(result.length, 1)
assert.equal(result[0].status, 'signed')
})
it('should emit a signed event to signal the exciton of callback', (done) => {
let tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
const noop = function () {
assert(true, 'event listener has been triggered and noop executed')
done()
}
txStateManager.addTx(tx)
txStateManager.on('1:signed', noop)
txStateManager.setTxStatusSigned(1)
})
})
describe('#setTxStatusRejected', function () {
it('sets the tx status to rejected', function () {
let tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
txStateManager.addTx(tx)
txStateManager.setTxStatusRejected(1)
let result = txStateManager.getTxList()
assert.ok(Array.isArray(result))
assert.equal(result.length, 1)
assert.equal(result[0].status, 'rejected')
})
it('should emit a rejected event to signal the exciton of callback', (done) => {
let tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
txStateManager.addTx(tx)
const noop = function (err, txId) {
assert(true, 'event listener has been triggered and noop executed')
done()
}
txStateManager.on('1:rejected', noop)
txStateManager.setTxStatusRejected(1)
})
})
describe('#getFullTxList', function () {
it('when new should return empty array', function () {
let result = txStateManager.getTxList()
assert.ok(Array.isArray(result))
assert.equal(result.length, 0)
})
})
describe('#getTxList', function () {
it('when new should return empty array', function () {
let result = txStateManager.getTxList()
assert.ok(Array.isArray(result))
assert.equal(result.length, 0)
})
})
describe('#addTx', function () {
it('adds a tx returned in getTxList', function () {
let tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }
txStateManager.addTx(tx, noop)
let result = txStateManager.getTxList()
assert.ok(Array.isArray(result))
assert.equal(result.length, 1)
assert.equal(result[0].id, 1)
})
it('does not override txs from other networks', function () {
let tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }
let tx2 = { id: 2, status: 'confirmed', metamaskNetworkId: otherNetworkId, txParams: {} }
txStateManager.addTx(tx, noop)
txStateManager.addTx(tx2, noop)
let result = txStateManager.getFullTxList()
let result2 = txStateManager.getTxList()
assert.equal(result.length, 2, 'txs were deleted')
assert.equal(result2.length, 1, 'incorrect number of txs on network.')
})
it('cuts off early txs beyond a limit', function () {
const limit = txStateManager.txHistoryLimit
for (let i = 0; i < limit + 1; i++) {
const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }
txStateManager.addTx(tx, noop)
}
let result = txStateManager.getTxList()
assert.equal(result.length, limit, `limit of ${limit} txs enforced`)
assert.equal(result[0].id, 1, 'early txs truncted')
})
it('cuts off early txs beyond a limit whether or not it is confirmed or rejected', function () {
const limit = txStateManager.txHistoryLimit
for (let i = 0; i < limit + 1; i++) {
const tx = { id: i, time: new Date(), status: 'rejected', metamaskNetworkId: currentNetworkId, txParams: {} }
txStateManager.addTx(tx, noop)
}
let result = txStateManager.getTxList()
assert.equal(result.length, limit, `limit of ${limit} txs enforced`)
assert.equal(result[0].id, 1, 'early txs truncted')
})
it('cuts off early txs beyond a limit but does not cut unapproved txs', function () {
let unconfirmedTx = { id: 0, time: new Date(), status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
txStateManager.addTx(unconfirmedTx, noop)
const limit = txStateManager.txHistoryLimit
for (let i = 1; i < limit + 1; i++) {
const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }
txStateManager.addTx(tx, noop)
}
let result = txStateManager.getTxList()
assert.equal(result.length, limit, `limit of ${limit} txs enforced`)
assert.equal(result[0].id, 0, 'first tx should still be there')
assert.equal(result[0].status, 'unapproved', 'first tx should be unapproved')
assert.equal(result[1].id, 2, 'early txs truncted')
})
})
describe('#updateTx', function () {
it('replaces the tx with the same id', function () {
txStateManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
txStateManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
const txMeta = txStateManager.getTx('1')
txMeta.hash = 'foo'
txStateManager.updateTx(txMeta)
let result = txStateManager.getTx('1')
assert.equal(result.hash, 'foo')
})
it('updates gas price and adds history items', function () {
const originalGasPrice = '0x01'
const desiredGasPrice = '0x02'
const txMeta = {
id: '1',
status: 'unapproved',
metamaskNetworkId: currentNetworkId,
txParams: {
gasPrice: originalGasPrice,
},
}
const updatedMeta = clone(txMeta)
txStateManager.addTx(txMeta)
const updatedTx = txStateManager.getTx('1')
// verify tx was initialized correctly
assert.equal(updatedTx.history.length, 1, 'one history item (initial)')
assert.equal(Array.isArray(updatedTx.history[0]), false, 'first history item is initial state')
assert.deepEqual(updatedTx.history[0], txStateHistoryHelper.snapshotFromTxMeta(updatedTx), 'first history item is initial state')
// modify value and updateTx
updatedTx.txParams.gasPrice = desiredGasPrice
txStateManager.updateTx(updatedTx)
// check updated value
const result = txStateManager.getTx('1')
assert.equal(result.txParams.gasPrice, desiredGasPrice, 'gas price updated')
// validate history was updated
assert.equal(result.history.length, 2, 'two history items (initial + diff)')
const expectedEntry = { op: 'replace', path: '/txParams/gasPrice', value: desiredGasPrice }
assert.deepEqual(result.history[1], [expectedEntry], 'two history items (initial + diff)')
})
})
describe('#getUnapprovedTxList', function () {
it('returns unapproved txs in a hash', function () {
txStateManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
txStateManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
const result = txStateManager.getUnapprovedTxList()
assert.equal(typeof result, 'object')
assert.equal(result['1'].status, 'unapproved')
assert.equal(result['2'], undefined)
})
})
describe('#getTx', function () {
it('returns a tx with the requested id', function () {
txStateManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
txStateManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
assert.equal(txStateManager.getTx('1').status, 'unapproved')
assert.equal(txStateManager.getTx('2').status, 'confirmed')
})
})
describe('#getFilteredTxList', function () {
it('returns a tx with the requested data', function () {
const txMetas = [
{ id: 0, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
{ id: 1, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
{ id: 2, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
{ id: 3, status: 'unapproved', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
{ id: 4, status: 'unapproved', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
{ id: 5, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
{ id: 6, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
{ id: 7, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
{ id: 8, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
{ id: 9, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
]
txMetas.forEach((txMeta) => txStateManager.addTx(txMeta, noop))
let filterParams
filterParams = { status: 'unapproved', from: '0xaa' }
assert.equal(txStateManager.getFilteredTxList(filterParams).length, 3, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
filterParams = { status: 'unapproved', to: '0xaa' }
assert.equal(txStateManager.getFilteredTxList(filterParams).length, 2, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
filterParams = { status: 'confirmed', from: '0xbb' }
assert.equal(txStateManager.getFilteredTxList(filterParams).length, 3, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
filterParams = { status: 'confirmed' }
assert.equal(txStateManager.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
filterParams = { from: '0xaa' }
assert.equal(txStateManager.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
filterParams = { to: '0xaa' }
assert.equal(txStateManager.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
})
})
})

@ -1,8 +1,10 @@
const assert = require('assert')
const Transaction = require('ethereumjs-tx')
const BN = require('bn.js')
const { hexToBn, bnToHex } = require('../../app/scripts/lib/util')
const TxUtils = require('../../app/scripts/lib/tx-utils')
const TxUtils = require('../../app/scripts/lib/tx-gas-utils')
describe('txUtils', function () {
@ -28,7 +30,7 @@ describe('txUtils', function () {
nonce: '0x3',
chainId: 42,
}
const ethTx = txUtils.buildEthTxFromParams(txParams)
const ethTx = new Transaction(txParams)
assert.equal(ethTx.getChainId(), 42, 'chainId is set from tx params')
})
})

Loading…
Cancel
Save