Merge branch 'master' into direct-block-tracker

feature/default_network_editable
kumavis 7 years ago committed by GitHub
commit 4404dfc5d3
  1. 4
      CHANGELOG.md
  2. 2
      app/manifest.json
  3. 2
      app/scripts/background.js
  4. 15
      app/scripts/controllers/balance.js
  5. 365
      app/scripts/controllers/transactions.js
  6. 2
      app/scripts/keyring-controller.js
  7. 5
      app/scripts/lib/account-tracker.js
  8. 26
      app/scripts/lib/pending-tx-tracker.js
  9. 22
      app/scripts/lib/tx-gas-utils.js
  10. 245
      app/scripts/lib/tx-state-manager.js
  11. 3
      test/unit/components/pending-tx-test.js
  12. 18
      test/unit/pending-tx-test.js
  13. 424
      test/unit/tx-controller-test.js
  14. 241
      test/unit/tx-state-manager-test.js
  15. 6
      test/unit/tx-utils-test.js

@ -2,6 +2,10 @@
## Current Master ## Current Master
## 3.10.5 2017-9-27
- Fix block gas limit estimation.
## 3.10.4 2017-9-27 ## 3.10.4 2017-9-27
- Fix bug that could mis-render token balances when very small. (Not actually included in 3.9.9) - Fix bug that could mis-render token balances when very small. (Not actually included in 3.9.9)

@ -1,7 +1,7 @@
{ {
"name": "MetaMask", "name": "MetaMask",
"short_name": "Metamask", "short_name": "Metamask",
"version": "3.10.4", "version": "3.10.5",
"manifest_version": 2, "manifest_version": 2,
"author": "https://metamask.io", "author": "https://metamask.io",
"description": "Ethereum Browser Extension", "description": "Ethereum Browser Extension",

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

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

@ -1,74 +1,85 @@
const EventEmitter = require('events') const EventEmitter = require('events')
const extend = require('xtend')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const Transaction = require('ethereumjs-tx')
const EthQuery = require('ethjs-query') 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 PendingTransactionTracker = require('../lib/pending-tx-tracker')
const createId = require('../lib/random-id') const createId = require('../lib/random-id')
const NonceTracker = require('../lib/nonce-tracker') const NonceTracker = require('../lib/nonce-tracker')
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 { module.exports = class TransactionController extends EventEmitter {
constructor (opts) { constructor (opts) {
super() super()
this.store = new ObservableStore(extend({
transactions: [],
}, opts.initState))
this.memStore = new ObservableStore({})
this.networkStore = opts.networkStore || new ObservableStore({}) this.networkStore = opts.networkStore || new ObservableStore({})
this.preferencesStore = opts.preferencesStore || new ObservableStore({}) this.preferencesStore = opts.preferencesStore || new ObservableStore({})
this.txHistoryLimit = opts.txHistoryLimit
this.provider = opts.provider this.provider = opts.provider
this.blockTracker = opts.blockTracker this.blockTracker = opts.blockTracker
this.signEthTx = opts.signTransaction this.signEthTx = opts.signTransaction
this.accountTracker = opts.accountTracker 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({ this.nonceTracker = new NonceTracker({
provider: this.provider, provider: this.provider,
getPendingTransactions: (address) => { getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager),
return this.getFilteredTxList({
from: address,
status: 'submitted',
err: undefined,
})
},
getConfirmedTransactions: (address) => { getConfirmedTransactions: (address) => {
return this.getFilteredTxList({ return this.txStateManager.getFilteredTxList({
from: address, from: address,
status: 'confirmed', status: 'confirmed',
err: undefined, 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({ this.pendingTxTracker = new PendingTransactionTracker({
provider: this.provider, provider: this.provider,
nonceTracker: this.nonceTracker, nonceTracker: this.nonceTracker,
retryLimit: 3500, // Retry 3500 blocks, or about 1 day.
getBalance: (address) => { getBalance: (address) => {
const account = this.accountTracker.getState().accounts[address] const account = this.accountTracker.store.getState().accounts[address]
if (!account) return if (!account) return
return account.balance return account.balance
}, },
publishTransaction: this.txProviderUtil.publishTransaction.bind(this.txProviderUtil), publishTransaction: (rawTx) => this.query.sendRawTransaction(rawTx),
getPendingTransactions: () => { getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager),
const network = this.getNetwork()
return this.getFilteredTxList({
status: 'submitted',
metamaskNetworkId: network,
})
},
}) })
this.pendingTxTracker.on('txWarning', this.updateTx.bind(this)) this.txStateManager.store.subscribe(() => this.emit('update:badge'))
this.pendingTxTracker.on('txFailed', this.setTxStatusFailed.bind(this))
this.pendingTxTracker.on('txConfirmed', this.setTxStatusConfirmed.bind(this)) 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.blockTracker.on('block', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker))
// this is a little messy but until ethstore has been either // this is a little messy but until ethstore has been either
@ -80,7 +91,7 @@ module.exports = class TransactionController extends EventEmitter {
this.blockTracker.on('sync', this.pendingTxTracker.queryPendingTxs.bind(this.pendingTxTracker)) this.blockTracker.on('sync', this.pendingTxTracker.queryPendingTxs.bind(this.pendingTxTracker))
// memstore is computed from a few different stores // memstore is computed from a few different stores
this._updateMemstore() this._updateMemstore()
this.store.subscribe(() => this._updateMemstore()) this.txStateManager.store.subscribe(() => this._updateMemstore())
this.networkStore.subscribe(() => this._updateMemstore()) this.networkStore.subscribe(() => this._updateMemstore())
this.preferencesStore.subscribe(() => this._updateMemstore()) this.preferencesStore.subscribe(() => this._updateMemstore())
} }
@ -97,98 +108,31 @@ module.exports = class TransactionController extends EventEmitter {
return this.preferencesStore.getState().selectedAddress 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 () { getUnapprovedTxCount () {
return Object.keys(this.getUnapprovedTxList()).length return Object.keys(this.txStateManager.getUnapprovedTxList()).length
} }
getPendingTxCount () { getPendingTxCount (account) {
return this.getTxsByMetaData('status', 'signed').length return this.txStateManager.getPendingTransactions(account).length
} }
// Returns the tx list getFilteredTxList (opts) {
getTxList () { return this.txStateManager.getFilteredTxList(opts)
const network = this.getNetwork()
const fullTxList = this.getFullTxList()
return this.getTxsByMetaData('metamaskNetworkId', network, fullTxList)
} }
// gets tx by Id and returns it getChainId () {
getTx (txId) { const networkState = this.networkStore.getState()
const txList = this.getTxList() const getChainId = parseInt(networkState)
const txMeta = txList.find(txData => txData.id === txId) if (Number.isNaN(getChainId)) {
return txMeta return 0
} } else {
getUnapprovedTxList () { return getChainId
const txList = this.getTxList()
return txList.filter((txMeta) => txMeta.status === 'unapproved')
.reduce((result, tx) => {
result[tx.id] = tx
return result
}, {})
} }
updateTx (txMeta) {
// create txMeta snapshot for history
const 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 // Adds a tx to the txlist
addTx (txMeta) { addTx (txMeta) {
// initialize history this.txStateManager.addTx(txMeta)
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.emit(`${txMeta.id}:unapproved`, txMeta) this.emit(`${txMeta.id}:unapproved`, txMeta)
} }
@ -198,7 +142,7 @@ module.exports = class TransactionController extends EventEmitter {
this.emit('newUnaprovedTx', txMeta) this.emit('newUnaprovedTx', txMeta)
// listen for tx completion (success, fail) // listen for tx completion (success, fail)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.once(`${txMeta.id}:finished`, (completedTx) => { this.txStateManager.once(`${txMeta.id}:finished`, (completedTx) => {
switch (completedTx.status) { switch (completedTx.status) {
case 'submitted': case 'submitted':
return resolve(completedTx.hash) return resolve(completedTx.hash)
@ -213,7 +157,7 @@ module.exports = class TransactionController extends EventEmitter {
async addUnapprovedTransaction (txParams) { async addUnapprovedTransaction (txParams) {
// validate // validate
await this.txProviderUtil.validateTxParams(txParams) await this.txGasUtil.validateTxParams(txParams)
// construct txMeta // construct txMeta
const txMeta = { const txMeta = {
id: createId(), id: createId(),
@ -232,17 +176,15 @@ module.exports = class TransactionController extends EventEmitter {
async addTxDefaults (txMeta) { async addTxDefaults (txMeta) {
const txParams = txMeta.txParams const txParams = txMeta.txParams
// ensure value // ensure value
const gasPrice = txParams.gasPrice || await this.query.gasPrice()
txParams.value = txParams.value || '0x0' txParams.value = txParams.value || '0x0'
if (!txParams.gasPrice) { txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16))
const gasPrice = await this.query.gasPrice()
txParams.gasPrice = gasPrice
}
// set gasLimit // set gasLimit
return await this.txProviderUtil.analyzeGasUsage(txMeta) return await this.txGasUtil.analyzeGasUsage(txMeta)
} }
async updateAndApproveTransaction (txMeta) { async updateAndApproveTransaction (txMeta) {
this.updateTx(txMeta) this.txStateManager.updateTx(txMeta)
await this.approveTransaction(txMeta.id) await this.approveTransaction(txMeta.id)
} }
@ -250,24 +192,24 @@ module.exports = class TransactionController extends EventEmitter {
let nonceLock let nonceLock
try { try {
// approve // approve
this.setTxStatusApproved(txId) this.txStateManager.setTxStatusApproved(txId)
// get next nonce // get next nonce
const txMeta = this.getTx(txId) const txMeta = this.txStateManager.getTx(txId)
const fromAddress = txMeta.txParams.from const fromAddress = txMeta.txParams.from
// wait for a nonce // wait for a nonce
nonceLock = await this.nonceTracker.getNonceLock(fromAddress) nonceLock = await this.nonceTracker.getNonceLock(fromAddress)
// add nonce to txParams // add nonce to txParams
txMeta.txParams.nonce = nonceLock.nextNonce txMeta.txParams.nonce = ethUtil.addHexPrefix(nonceLock.nextNonce.toString(16))
// add nonce debugging information to txMeta // add nonce debugging information to txMeta
txMeta.nonceDetails = nonceLock.nonceDetails txMeta.nonceDetails = nonceLock.nonceDetails
this.updateTx(txMeta) this.txStateManager.updateTx(txMeta)
// sign transaction // sign transaction
const rawTx = await this.signTransaction(txId) const rawTx = await this.signTransaction(txId)
await this.publishTransaction(txId, rawTx) await this.publishTransaction(txId, rawTx)
// must set transaction to submitted/failed before releasing lock // must set transaction to submitted/failed before releasing lock
nonceLock.releaseLock() nonceLock.releaseLock()
} catch (err) { } catch (err) {
this.setTxStatusFailed(txId, err) this.txStateManager.setTxStatusFailed(txId, err)
// must set transaction to submitted/failed before releasing lock // must set transaction to submitted/failed before releasing lock
if (nonceLock) nonceLock.releaseLock() if (nonceLock) nonceLock.releaseLock()
// continue with error chain // continue with error chain
@ -276,181 +218,46 @@ module.exports = class TransactionController extends EventEmitter {
} }
async signTransaction (txId) { async signTransaction (txId) {
const txMeta = this.getTx(txId) const txMeta = this.txStateManager.getTx(txId)
const txParams = txMeta.txParams const txParams = txMeta.txParams
const fromAddress = txParams.from const fromAddress = txParams.from
// add network/chain id // add network/chain id
txParams.chainId = this.getChainId() txParams.chainId = ethUtil.addHexPrefix(this.getChainId().toString(16))
const ethTx = this.txProviderUtil.buildEthTxFromParams(txParams) const ethTx = new Transaction(txParams)
await this.signEthTx(ethTx, fromAddress) await this.signEthTx(ethTx, fromAddress)
this.setTxStatusSigned(txMeta.id) this.txStateManager.setTxStatusSigned(txMeta.id)
const rawTx = ethUtil.bufferToHex(ethTx.serialize()) const rawTx = ethUtil.bufferToHex(ethTx.serialize())
return rawTx return rawTx
} }
async publishTransaction (txId, rawTx) { async publishTransaction (txId, rawTx) {
const txMeta = this.getTx(txId) const txMeta = this.txStateManager.getTx(txId)
txMeta.rawTx = rawTx txMeta.rawTx = rawTx
this.updateTx(txMeta) this.txStateManager.updateTx(txMeta)
const txHash = await this.txProviderUtil.publishTransaction(rawTx) const txHash = await this.query.sendRawTransaction(rawTx)
this.setTxHash(txId, txHash) this.setTxHash(txId, txHash)
this.setTxStatusSubmitted(txId) this.txStateManager.setTxStatusSubmitted(txId)
} }
async cancelTransaction (txId) { async cancelTransaction (txId) {
this.setTxStatusRejected(txId) this.txStateManager.setTxStatusRejected(txId)
}
getChainId () {
const networkState = this.networkStore.getState()
const getChainId = parseInt(networkState)
if (Number.isNaN(getChainId)) {
return 0
} else {
return getChainId
}
} }
// receives a txHash records the tx as signed // receives a txHash records the tx as signed
setTxHash (txId, txHash) { setTxHash (txId, txHash) {
// Add the tx hash to the persisted meta-tx object // Add the tx hash to the persisted meta-tx object
const txMeta = this.getTx(txId) const txMeta = this.txStateManager.getTx(txId)
txMeta.hash = txHash txMeta.hash = txHash
this.updateTx(txMeta) this.txStateManager.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')
} }
setTxStatusFailed (txId, err) { //
const txMeta = this.getTx(txId) // PRIVATE METHODS
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 })
}
_updateMemstore () { _updateMemstore () {
const unapprovedTxs = this.getUnapprovedTxList() const unapprovedTxs = this.txStateManager.getUnapprovedTxList()
const selectedAddressTxList = this.getFilteredTxList({ const selectedAddressTxList = this.txStateManager.getFilteredTxList({
from: this.getSelectedAddress(), from: this.getSelectedAddress(),
metamaskNetworkId: this.getNetwork(), metamaskNetworkId: this.getNetwork(),
}) })

@ -568,7 +568,7 @@ class KeyringController extends EventEmitter {
clearKeyrings () { clearKeyrings () {
let accounts let accounts
try { try {
accounts = Object.keys(this.accountTracker.getState()) accounts = Object.keys(this.accountTracker.store.getState())
} catch (e) { } catch (e) {
accounts = [] accounts = []
} }

@ -11,6 +11,7 @@ const async = require('async')
const EthQuery = require('eth-query') const EthQuery = require('eth-query')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const EventEmitter = require('events').EventEmitter const EventEmitter = require('events').EventEmitter
const ethUtil = require('ethereumjs-util')
function noop () {} function noop () {}
@ -58,7 +59,9 @@ class AccountTracker extends EventEmitter {
_updateForBlock (block) { _updateForBlock (block) {
this._currentBlockNumber = block.number this._currentBlockNumber = block.number
this.store.updateState({ currentBlockGasLimit: block.gasLimit }) const currentBlockGasLimit = block.gasLimit
this.store.updateState({ currentBlockGasLimit })
async.parallel([ async.parallel([
this._updateAccounts.bind(this), this._updateAccounts.bind(this),

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

@ -1,6 +1,4 @@
const EthQuery = require('ethjs-query') const EthQuery = require('ethjs-query')
const Transaction = require('ethereumjs-tx')
const normalize = require('eth-sig-util').normalize
const { const {
hexToBn, hexToBn,
BnMultiplyByFraction, BnMultiplyByFraction,
@ -78,26 +76,6 @@ module.exports = class txProvideUtil {
return bnToHex(upperGasLimitBn) 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) { async validateTxParams (txParams) {
if (('value' in txParams) && txParams.value.indexOf('-') === 0) { if (('value' in txParams) && txParams.value.indexOf('-') === 0) {
throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`) throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)

@ -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', 'to': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
'value': '0xde0b6b3a7640000', 'value': '0xde0b6b3a7640000',
gasPrice, gasPrice,
'gas': '0x7b0c'}, 'gas': '0x7b0c',
},
'gasLimitSpecified': false, 'gasLimitSpecified': false,
'estimatedGas': '0x5208', 'estimatedGas': '0x5208',
} }

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

@ -2,21 +2,19 @@ const assert = require('assert')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const EthTx = require('ethereumjs-tx') const EthTx = require('ethereumjs-tx')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const clone = require('clone')
const sinon = require('sinon') const sinon = require('sinon')
const TransactionController = require('../../app/scripts/controllers/transactions') const TransactionController = require('../../app/scripts/controllers/transactions')
const TxProvideUtils = require('../../app/scripts/lib/tx-utils') const TxGasUtils = require('../../app/scripts/lib/tx-gas-utils')
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper') const { createStubedProvider } = require('../stub/provider')
const noop = () => true const noop = () => true
const currentNetworkId = 42 const currentNetworkId = 42
const otherNetworkId = 36 const otherNetworkId = 36
const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex') const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex')
const { createStubedProvider } = require('../stub/provider')
describe('Transaction Controller', function () { describe('Transaction Controller', function () {
let txController, engine, provider, providerResultStub let txController, provider, providerResultStub
beforeEach(function () { beforeEach(function () {
providerResultStub = {} providerResultStub = {}
@ -27,34 +25,97 @@ describe('Transaction Controller', function () {
networkStore: new ObservableStore(currentNetworkId), networkStore: new ObservableStore(currentNetworkId),
txHistoryLimit: 10, txHistoryLimit: 10,
blockTracker: { getCurrentBlock: noop, on: noop, once: noop }, blockTracker: { getCurrentBlock: noop, on: noop, once: noop },
accountTracker: { getState: noop }, accountTracker: { store: { getState: noop } },
signTransaction: (ethTx) => new Promise((resolve) => { signTransaction: (ethTx) => new Promise((resolve) => {
ethTx.sign(privKey) ethTx.sign(privKey)
resolve() resolve()
}), }),
}) })
txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop }) 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 () { describe('#newUnapprovedTransaction', function () {
let stub, txMeta, txParams let stub, txMeta, txParams
beforeEach(function () { beforeEach(function () {
txParams = { txParams = {
'from': '0xc684832530fcbddae4b4230a47e991ddcec2831d', 'from': '0xc684832530fcbddae4b4230a47e991ddcec2831d',
'to': '0xc684832530fcbddae4b4230a47e991ddcec2831d', 'to': '0xc684832530fcbddae4b4230a47e991ddcec2831d',
}, }
txMeta = { txMeta = {
status: 'unapproved', status: 'unapproved',
id: 1, id: 1,
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
txParams, txParams,
history: [],
} }
txController.addTx(txMeta) txController.txStateManager._saveTxList([txMeta])
stub = sinon.stub(txController, 'addUnapprovedTransaction').returns(Promise.resolve(txMeta)) stub = sinon.stub(txController, 'addUnapprovedTransaction').returns(Promise.resolve(txController.txStateManager.addTx(txMeta)))
}) })
afterEach(function () { afterEach(function () {
txController.txStateManager._saveTxList([])
stub.restore() stub.restore()
}) })
@ -72,7 +133,7 @@ describe('Transaction Controller', function () {
txController.once('newUnaprovedTx', (txMetaFromEmit) => { txController.once('newUnaprovedTx', (txMetaFromEmit) => {
setTimeout(() => { setTimeout(() => {
txController.setTxHash(txMetaFromEmit.id, '0x0') txController.setTxHash(txMetaFromEmit.id, '0x0')
txController.setTxStatusSubmitted(txMetaFromEmit.id) txController.txStateManager.setTxStatusSubmitted(txMetaFromEmit.id)
}, 10) }, 10)
}) })
@ -87,7 +148,7 @@ describe('Transaction Controller', function () {
it('should reject when finished and status is rejected', function (done) { it('should reject when finished and status is rejected', function (done) {
txController.once('newUnaprovedTx', (txMetaFromEmit) => { txController.once('newUnaprovedTx', (txMetaFromEmit) => {
setTimeout(() => { setTimeout(() => {
txController.setTxStatusRejected(txMetaFromEmit.id) txController.txStateManager.setTxStatusRejected(txMetaFromEmit.id)
}, 10) }, 10)
}) })
@ -110,7 +171,7 @@ describe('Transaction Controller', function () {
assert(('txParams' in txMeta), 'should have a txParams') assert(('txParams' in txMeta), 'should have a txParams')
assert(('history' in txMeta), 'should have a history') 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}`) assert.deepEqual(txMeta, memTxMeta, `txMeta should be stored in txController after adding it\n expected: ${txMeta} \n got: ${memTxMeta}`)
addTxDefaultsStub.restore() addTxDefaultsStub.restore()
done() done()
@ -120,7 +181,7 @@ describe('Transaction Controller', function () {
describe('#addTxDefaults', function () { describe('#addTxDefaults', function () {
it('should add the tx defaults if their are none', function (done) { it('should add the tx defaults if their are none', function (done) {
let txMeta = { const txMeta = {
'txParams': { 'txParams': {
'from': '0xc684832530fcbddae4b4230a47e991ddcec2831d', 'from': '0xc684832530fcbddae4b4230a47e991ddcec2831d',
'to': '0xc684832530fcbddae4b4230a47e991ddcec2831d', 'to': '0xc684832530fcbddae4b4230a47e991ddcec2831d',
@ -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 () { describe('#addTx', function () {
it('adds a tx returned in getTxList', function () { it('should emit updates', function (done) {
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'
const txMeta = { const txMeta = {
id: '1', id: '1',
status: 'unapproved', status: 'unapproved',
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
txParams: { txParams: {},
gasPrice: originalGasPrice,
},
} }
txController.addTx(txMeta) const eventNames = ['update:badge', '1:unapproved']
const updatedTx = txController.getTx('1') const listeners = []
// verify tx was initialized correctly eventNames.forEach((eventName) => {
assert.equal(updatedTx.history.length, 1, 'one history item (initial)') listeners.push(new Promise((resolve) => {
assert.equal(Array.isArray(updatedTx.history[0]), false, 'first history item is initial state') txController.once(eventName, (arg) => {
assert.deepEqual(updatedTx.history[0], txStateHistoryHelper.snapshotFromTxMeta(updatedTx), 'first history item is initial state') resolve(arg)
// 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)
}) })
}))
}) })
Promise.all(listeners)
describe('#getTx', function () { .then((returnValues) => {
it('returns a tx with the requested id', function () { assert.deepEqual(returnValues.pop(), txMeta, 'last event 1:unapproved should return txMeta')
txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) done()
txController.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) })
assert.equal(txController.getTx('1').status, 'unapproved') .catch(done)
assert.equal(txController.getTx('2').status, 'confirmed') txController.addTx(txMeta)
})
})
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)}`)
}) })
}) })
@ -404,11 +282,11 @@ describe('Transaction Controller', function () {
const pubStub = sinon.stub(txController, 'publishTransaction').callsFake(() => { const pubStub = sinon.stub(txController, 'publishTransaction').callsFake(() => {
txController.setTxHash('1', originalValue) txController.setTxHash('1', originalValue)
txController.setTxStatusSubmitted('1') txController.txStateManager.setTxStatusSubmitted('1')
}) })
txController.approveTransaction(txMeta.id).then(() => { txController.approveTransaction(txMeta.id).then(() => {
const result = txController.getTx(txMeta.id) const result = txController.txStateManager.getTx(txMeta.id)
const params = result.txParams const params = result.txParams
assert.equal(params.gas, originalValue, 'gas unmodified') assert.equal(params.gas, originalValue, 'gas unmodified')
@ -431,4 +309,120 @@ describe('Transaction Controller', function () {
}).catch(done) }).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 assert = require('assert')
const Transaction = require('ethereumjs-tx')
const BN = require('bn.js') const BN = require('bn.js')
const { hexToBn, bnToHex } = require('../../app/scripts/lib/util') 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 () { describe('txUtils', function () {
@ -28,7 +30,7 @@ describe('txUtils', function () {
nonce: '0x3', nonce: '0x3',
chainId: 42, chainId: 42,
} }
const ethTx = txUtils.buildEthTxFromParams(txParams) const ethTx = new Transaction(txParams)
assert.equal(ethTx.getChainId(), 42, 'chainId is set from tx params') assert.equal(ethTx.getChainId(), 42, 'chainId is set from tx params')
}) })
}) })

Loading…
Cancel
Save