Merge pull request #339 from poanetwork/refactor

Refactoring 3 before EIP - 1193
This commit is contained in:
Victor Baranov 2020-03-28 00:09:29 +03:00 committed by GitHub
commit 2d6753d182
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 241 additions and 535 deletions

View File

@ -12,9 +12,9 @@ abiDecoder.addABI(abi)
import TransactionStateManager from './tx-state-manager'
const TxGasUtil = require('./tx-gas-utils')
const PendingTransactionTracker = require('./pending-tx-tracker')
const NonceTracker = require('./nonce-tracker')
import NonceTracker from 'nonce-tracker'
import * as txUtils from './lib/util'
const cleanErrorStack = require('../../lib/cleanErrorStack')
import cleanErrorStack from '../../lib/cleanErrorStack'
import log from 'loglevel'
const recipientBlacklistChecker = require('./lib/recipient-blacklist-checker')
const {
@ -228,18 +228,9 @@ class TransactionController extends EventEmitter {
@return {txMeta}
*/
async retryTransaction (originalTxId, gasPrice) {
async retryTransaction (originalTxId) {
const originalTxMeta = this.txStateManager.getTx(originalTxId)
const { txParams } = originalTxMeta
const lastGasPrice = gasPrice || originalTxMeta.txParams.gasPrice
const suggestedGasPriceBN = new ethUtil.BN(ethUtil.stripHexPrefix(this.getGasPrice()), 16)
const lastGasPriceBN = new ethUtil.BN(ethUtil.stripHexPrefix(lastGasPrice), 16)
// essentially lastGasPrice * 1.1 but
// dont trust decimals so a round about way of doing that
const lastGasPriceBNBumped = lastGasPriceBN.mul(new ethUtil.BN(110, 10)).div(new ethUtil.BN(100, 10))
// transactions that are being retried require a >=%10 bump or the clients will throw an error
txParams.gasPrice = suggestedGasPriceBN.gt(lastGasPriceBNBumped) ? `0x${suggestedGasPriceBN.toString(16)}` : `0x${lastGasPriceBNBumped.toString(16)}`
const lastGasPrice = originalTxMeta.txParams.gasPrice
const txMeta = this.txStateManager.generateTxMeta({
txParams: originalTxMeta.txParams,
lastGasPrice,

View File

@ -1,161 +0,0 @@
const EthQuery = require('ethjs-query')
const assert = require('assert')
const Mutex = require('await-semaphore').Mutex
/**
@param opts {Object}
@param {Object} opts.provider a ethereum provider
@param {Function} opts.getPendingTransactions a function that returns an array of txMeta
whosee status is `submitted`
@param {Function} opts.getConfirmedTransactions a function that returns an array of txMeta
whose status is `confirmed`
@class
*/
class NonceTracker {
constructor ({ provider, blockTracker, getPendingTransactions, getConfirmedTransactions }) {
this.provider = provider
this.blockTracker = blockTracker
this.ethQuery = new EthQuery(provider)
this.getPendingTransactions = getPendingTransactions
this.getConfirmedTransactions = getConfirmedTransactions
this.lockMap = {}
}
/**
@returns {Promise<Object>} with the key releaseLock (the gloabl mutex)
*/
async getGlobalLock () {
const globalMutex = this._lookupMutex('global')
// await global mutex free
const releaseLock = await globalMutex.acquire()
return { releaseLock }
}
/**
* @typedef NonceDetails
* @property {number} highestLocallyConfirmed - A hex string of the highest nonce on a confirmed transaction.
* @property {number} nextNetworkNonce - The next nonce suggested by the eth_getTransactionCount method.
* @property {number} highestSuggested - The maximum between the other two, the number returned.
*/
/**
this will return an object with the `nextNonce` `nonceDetails` of type NonceDetails, and the releaseLock
Note: releaseLock must be called after adding a signed tx to pending transactions (or discarding).
@param address {string} the hex string for the address whose nonce we are calculating
@returns {Promise<NonceDetails>}
*/
async getNonceLock (address) {
// await global mutex free
await this._globalMutexFree()
// await lock free, then take lock
const releaseLock = await this._takeMutex(address)
try {
// evaluate multiple nextNonce strategies
const nonceDetails = {}
const networkNonceResult = await this._getNetworkNextNonce(address)
const highestLocallyConfirmed = this._getHighestLocallyConfirmed(address)
const nextNetworkNonce = networkNonceResult.nonce
const highestSuggested = Math.max(nextNetworkNonce, highestLocallyConfirmed)
const pendingTxs = this.getPendingTransactions(address)
const localNonceResult = this._getHighestContinuousFrom(pendingTxs, highestSuggested) || 0
nonceDetails.params = {
highestLocallyConfirmed,
highestSuggested,
nextNetworkNonce,
}
nonceDetails.local = localNonceResult
nonceDetails.network = networkNonceResult
const nextNonce = Math.max(networkNonceResult.nonce, localNonceResult.nonce)
assert(Number.isInteger(nextNonce), `nonce-tracker - nextNonce is not an integer - got: (${typeof nextNonce}) "${nextNonce}"`)
// return nonce and release cb
return { nextNonce, nonceDetails, releaseLock }
} catch (err) {
// release lock if we encounter an error
releaseLock()
throw err
}
}
async _globalMutexFree () {
const globalMutex = this._lookupMutex('global')
const releaseLock = await globalMutex.acquire()
releaseLock()
}
async _takeMutex (lockId) {
const mutex = this._lookupMutex(lockId)
const releaseLock = await mutex.acquire()
return releaseLock
}
_lookupMutex (lockId) {
let mutex = this.lockMap[lockId]
if (!mutex) {
mutex = new Mutex()
this.lockMap[lockId] = mutex
}
return mutex
}
async _getNetworkNextNonce (address) {
// calculate next nonce
// we need to make sure our base count
// and pending count are from the same block
const blockNumber = await this.blockTracker.getLatestBlock()
const baseCountBN = await this.ethQuery.getTransactionCount(address, blockNumber)
const baseCount = baseCountBN.toNumber()
assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`)
const nonceDetails = { blockNumber, baseCount }
return { name: 'network', nonce: baseCount, details: nonceDetails }
}
_getHighestLocallyConfirmed (address) {
const confirmedTransactions = this.getConfirmedTransactions(address)
const highest = this._getHighestNonce(confirmedTransactions)
return Number.isInteger(highest) ? highest + 1 : 0
}
_getHighestNonce (txList) {
const nonces = txList.map((txMeta) => {
const nonce = txMeta.txParams.nonce
assert(typeof nonce, 'string', 'nonces should be hex strings')
return parseInt(nonce, 16)
})
const highestNonce = Math.max.apply(null, nonces)
return highestNonce
}
/**
@typedef {object} highestContinuousFrom
@property {string} - name the name for how the nonce was calculated based on the data used
@property {number} - nonce the next suggested nonce
@property {object} - details the provided starting nonce that was used (for debugging)
*/
/**
@param txList {array} - list of txMeta's
@param startPoint {number} - the highest known locally confirmed nonce
@returns {highestContinuousFrom}
*/
_getHighestContinuousFrom (txList, startPoint) {
const nonces = txList.map((txMeta) => {
const nonce = txMeta.txParams.nonce
assert(typeof nonce, 'string', 'nonces should be hex strings')
return parseInt(nonce, 16)
})
let highest = startPoint
while (nonces.includes(highest)) {
highest++
}
return { name: 'local', nonce: highest, details: { startPoint, highest } }
}
}
module.exports = NonceTracker

View File

@ -1,11 +1,9 @@
import ethUtil from 'ethereumjs-util'
import extend from 'extend'
import EventEmitter from 'safe-event-emitter'
import ObservableStore from 'obs-store'
import log from 'loglevel'
import txStateHistoryHelper from './lib/tx-state-history-helper'
import createId from '../../lib/random-id'
import { getFinalStates } from './lib/util'
import { getFinalStates, normalizeTxParams } from './lib/util'
/**
TransactionStateManager is responsible for the state of a transaction and
storing the transaction
@ -21,8 +19,8 @@ import { getFinalStates } from './lib/util'
<br> - `'confirmed'` the tx has been included in a block.
<br> - `'failed'` the tx failed for some reason, included on tx data.
<br> - `'dropped'` the tx nonce was already used
@param opts {object}
@param {object} [opts.initState={ transactions: [] }] initial transactions list with the key transaction {array}
@param {Object} opts
@param {Object} [opts.initState={ transactions: [] }] initial transactions list with the key transaction {array}
@param {number} [opts.txHistoryLimit] limit for how many finished
transactions can hang around in state
@param {function} opts.getNetwork return network number
@ -33,7 +31,7 @@ class TransactionStateManager extends EventEmitter {
super()
this.store = new ObservableStore(
extend({
Object.assign({
transactions: [],
}, initState))
this.txHistoryLimit = txHistoryLimit
@ -41,21 +39,25 @@ class TransactionStateManager extends EventEmitter {
}
/**
@param opts {object} - the object to use when overwriting defaults
@returns {txMeta} the default txMeta object
@param {Object} opts - the object to use when overwriting defaults
@returns {txMeta} - the default txMeta object
*/
generateTxMeta (opts) {
return extend({
const netId = this.getNetwork()
if (netId === 'loading') {
throw new Error('MetaMask is having trouble connecting to the network')
}
return Object.assign({
id: createId(),
time: (new Date()).getTime(),
status: 'unapproved',
metamaskNetworkId: this.getNetwork(),
metamaskNetworkId: netId,
loadingDefaults: true,
}, opts)
}
/**
@returns {array} of txMetas that have been filtered for only the current network
@returns {array} - of txMetas that have been filtered for only the current network
*/
getTxList () {
const network = this.getNetwork()
@ -64,14 +66,14 @@ class TransactionStateManager extends EventEmitter {
}
/**
@returns {array} of all the txMetas in store
@returns {array} - of all the txMetas in store
*/
getFullTxList () {
return this.store.getState().transactions
}
/**
@returns {array} the tx list whos status is unapproved
@returns {array} - the tx list whos status is unapproved
*/
getUnapprovedTxList () {
const txList = this.getTxsByMetaData('status', 'unapproved')
@ -83,23 +85,40 @@ class TransactionStateManager extends EventEmitter {
/**
@param [address] {string} - hex prefixed address to sort the txMetas for [optional]
@returns {array} the tx list whos status is submitted if no address is provide
returns all txMetas who's status is submitted for the current network
@returns {array} - the tx list whos status is approved if no address is provide
returns all txMetas who's status is approved for the current network
*/
getPendingTransactions (address) {
const opts = { status: 'submitted' }
if (address) opts.from = address
getApprovedTransactions (address) {
const opts = { status: 'approved' }
if (address) {
opts.from = address
}
return this.getFilteredTxList(opts)
}
/**
@param [address] {string} - hex prefixed address to sort the txMetas for [optional]
@returns {array} the tx list whos status is confirmed if no address is provide
@returns {array} - the tx list whos status is submitted if no address is provide
returns all txMetas who's status is submitted for the current network
*/
getPendingTransactions (address) {
const opts = { status: 'submitted' }
if (address) {
opts.from = address
}
return this.getFilteredTxList(opts)
}
/**
@param [address] {string} - hex prefixed address to sort the txMetas for [optional]
@returns {array} - the tx list whos status is confirmed if no address is provide
returns all txMetas who's status is confirmed for the current network
*/
getConfirmedTransactions (address) {
const opts = { status: 'confirmed' }
if (address) opts.from = address
if (address) {
opts.from = address
}
return this.getFilteredTxList(opts)
}
@ -109,14 +128,19 @@ class TransactionStateManager extends EventEmitter {
is in its final state
it will allso add the key `history` to the txMeta with the snap shot of the original
object
@param txMeta {Object}
@returns {object} the txMeta
@param {Object} txMeta
@returns {Object} - the txMeta
*/
addTx (txMeta) {
this.once(`${txMeta.id}:signed`, function (txId) {
// normalize and validate txParams if present
if (txMeta.txParams) {
txMeta.txParams = this.normalizeAndValidateTxParams(txMeta.txParams)
}
this.once(`${txMeta.id}:signed`, function () {
this.removeAllListeners(`${txMeta.id}:rejected`)
})
this.once(`${txMeta.id}:rejected`, function (txId) {
this.once(`${txMeta.id}:rejected`, function () {
this.removeAllListeners(`${txMeta.id}:signed`)
})
// initialize history
@ -142,13 +166,18 @@ class TransactionStateManager extends EventEmitter {
transactions.splice(index, 1)
}
}
transactions.push(txMeta)
const newTxIndex = transactions
.findIndex((currentTxMeta) => currentTxMeta.time > txMeta.time)
newTxIndex === -1
? transactions.push(txMeta)
: transactions.splice(newTxIndex, 0, txMeta)
this._saveTxList(transactions)
return txMeta
}
/**
@param txId {number}
@returns {object} the txMeta who matches the given id if none found
@param {number} txId
@returns {Object} - the txMeta who matches the given id if none found
for the network returns undefined
*/
getTx (txId) {
@ -158,17 +187,13 @@ class TransactionStateManager extends EventEmitter {
/**
updates the txMeta in the list and adds a history entry
@param txMeta {Object} - the txMeta to update
@param [note] {string} - a note about the update for history
@param {Object} txMeta - the txMeta to update
@param {string} [note] - a note about the update for history
*/
updateTx (txMeta, note) {
// validate txParams
// normalize and validate txParams if present
if (txMeta.txParams) {
if (typeof txMeta.txParams.data === 'undefined') {
delete txMeta.txParams.data
}
this.validateTxParams(txMeta.txParams)
txMeta.txParams = this.normalizeAndValidateTxParams(txMeta.txParams)
}
// create txMeta snapshot for history
@ -182,7 +207,7 @@ class TransactionStateManager extends EventEmitter {
// commit txMeta to state
const txId = txMeta.id
const txList = this.getFullTxList()
const index = txList.findIndex(txData => txData.id === txId)
const index = txList.findIndex((txData) => txData.id === txId)
txList[index] = txMeta
this._saveTxList(txList)
}
@ -191,18 +216,31 @@ class TransactionStateManager extends EventEmitter {
/**
merges txParams obj onto txMeta.txParams
use extend to ensure that all fields are filled
@param txId {number} - the id of the txMeta
@param txParams {object} - the updated txParams
@param {number} txId - the id of the txMeta
@param {Object} txParams - the updated txParams
*/
updateTxParams (txId, txParams) {
const txMeta = this.getTx(txId)
txMeta.txParams = extend(txMeta.txParams, txParams)
txMeta.txParams = { ...txMeta.txParams, ...txParams }
this.updateTx(txMeta, `txStateManager#updateTxParams`)
}
/**
* normalize and validate txParams members
* @param {Object} txParams - txParams
*/
normalizeAndValidateTxParams (txParams) {
if (typeof txParams.data === 'undefined') {
delete txParams.data
}
txParams = normalizeTxParams(txParams, false)
this.validateTxParams(txParams)
return txParams
}
/**
validates txParams members by type
@param txParams {object} - txParams to validate
@param {Object} txParams - txParams to validate
*/
validateTxParams (txParams) {
Object.keys(txParams).forEach((key) => {
@ -210,26 +248,31 @@ class TransactionStateManager extends EventEmitter {
// validate types
switch (key) {
case 'chainId':
if (typeof value !== 'number' && typeof value !== 'string') throw new Error(`${key} in txParams is not a Number or hex string. got: (${value})`)
if (typeof value !== 'number' && typeof value !== 'string') {
throw new Error(`${key} in txParams is not a Number or hex string. got: (${value})`)
}
break
default:
if (typeof value !== 'string') throw new Error(`${key} in txParams is not a string. got: (${value})`)
if (!ethUtil.isHexPrefixed(value)) throw new Error(`${key} in txParams is not hex prefixed. got: (${value})`)
if (typeof value !== 'string') {
throw new Error(`${key} in txParams is not a string. got: (${value})`)
}
break
}
})
}
/**
@param opts {object} - an object of fields to search for eg:<br>
/**
@param {Object} opts - an object of fields to search for eg:<br>
let <code>thingsToLookFor = {<br>
to: '0x0..',<br>
from: '0x0..',<br>
status: 'signed',<br>
status: 'signed', \\ (status) => status !== 'rejected' give me all txs who's status is not rejected<br>
err: undefined,<br>
}<br></code>
optionally the values of the keys can be functions for situations like where
you want all but one status.
@param [initialList=this.getTxList()]
@returns a {array} of txMeta with all
@returns {array} - array of txMeta with all
options matching
*/
/*
@ -243,7 +286,7 @@ class TransactionStateManager extends EventEmitter {
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
or for filtering for all txs from one account
and that have been 'confirmed'
*/
getFilteredTxList (opts, initialList) {
@ -255,18 +298,20 @@ class TransactionStateManager extends EventEmitter {
}
/**
@param key {string} - the key to check
@param value - the value your looking for
@param {string} key - the key to check
@param value - the value your looking for can also be a function that returns a bool
@param [txList=this.getTxList()] {array} - the list to search. default is the txList
from txStateManager#getTxList
@returns {array} a list of txMetas who matches the search params
@returns {array} - a list of txMetas who matches the search params
*/
getTxsByMetaData (key, value, txList = this.getTxList()) {
const filter = typeof value === 'function' ? value : (v) => v === value
return txList.filter((txMeta) => {
if (key in txMeta.txParams) {
return txMeta.txParams[key] === value
return filter(txMeta.txParams[key])
} else {
return txMeta[key] === value
return filter(txMeta[key])
}
})
}
@ -274,8 +319,8 @@ class TransactionStateManager extends EventEmitter {
// get::set status
/**
@param txId {number} - the txMeta Id
@return {string} the status of the tx.
@param {number} txId - the txMeta Id
@returns {string} - the status of the tx.
*/
getTxStatus (txId) {
const txMeta = this.getTx(txId)
@ -284,7 +329,7 @@ class TransactionStateManager extends EventEmitter {
/**
should update the status of the tx to 'rejected'.
@param txId {number} - the txMeta Id
@param {number} txId - the txMeta Id
*/
setTxStatusRejected (txId) {
this._setTxStatus(txId, 'rejected')
@ -293,14 +338,14 @@ class TransactionStateManager extends EventEmitter {
/**
should update the status of the tx to 'unapproved'.
@param txId {number} - the txMeta Id
@param {number} txId - the txMeta Id
*/
setTxStatusUnapproved (txId) {
this._setTxStatus(txId, 'unapproved')
}
/**
should update the status of the tx to 'approved'.
@param txId {number} - the txMeta Id
@param {number} txId - the txMeta Id
*/
setTxStatusApproved (txId) {
this._setTxStatus(txId, 'approved')
@ -308,7 +353,7 @@ class TransactionStateManager extends EventEmitter {
/**
should update the status of the tx to 'signed'.
@param txId {number} - the txMeta Id
@param {number} txId - the txMeta Id
*/
setTxStatusSigned (txId) {
this._setTxStatus(txId, 'signed')
@ -317,7 +362,7 @@ class TransactionStateManager extends EventEmitter {
/**
should update the status of the tx to 'submitted'.
and add a time stamp for when it was called
@param txId {number} - the txMeta Id
@param {number} txId - the txMeta Id
*/
setTxStatusSubmitted (txId) {
const txMeta = this.getTx(txId)
@ -328,7 +373,7 @@ class TransactionStateManager extends EventEmitter {
/**
should update the status of the tx to 'confirmed'.
@param txId {number} - the txMeta Id
@param {number} txId - the txMeta Id
*/
setTxStatusConfirmed (txId) {
this._setTxStatus(txId, 'confirmed')
@ -336,7 +381,7 @@ class TransactionStateManager extends EventEmitter {
/**
should update the status of the tx to 'dropped'.
@param txId {number} - the txMeta Id
@param {number} txId - the txMeta Id
*/
setTxStatusDropped (txId) {
this._setTxStatus(txId, 'dropped')
@ -346,24 +391,26 @@ class TransactionStateManager extends EventEmitter {
/**
should update the status of the tx to 'failed'.
and put the error on the txMeta
@param txId {number} - the txMeta Id
@param err {erroObject} - error object
@param {number} txId - the txMeta Id
@param {erroObject} err - error object
*/
setTxStatusFailed (txId, err) {
const error = !err ? new Error('Internal metamask failure') : err
const txMeta = this.getTx(txId)
txMeta.err = {
message: (err ? (err.message || err.error || err) : '').toString(),
rpc: err.value,
stack: err.stack,
message: error.toString(),
rpc: error.value,
stack: error.stack,
}
this.updateTx(txMeta)
this.updateTx(txMeta, 'transactions:tx-state-manager#fail - add error')
this._setTxStatus(txId, 'failed')
}
/**
Removes transaction from the given address for the current network
from the txList
@param address {string} - hex string of the from address on the txParams to remove
@param {string} address - hex string of the from address on the txParams to remove
*/
wipeTransactions (address) {
// network only tx
@ -376,9 +423,9 @@ class TransactionStateManager extends EventEmitter {
// Update state
this._saveTxList(otherAccountTxs)
}
//
// PRIVATE METHODS
//
//
// PRIVATE METHODS
//
// STATUS METHODS
// statuses:
@ -392,8 +439,8 @@ class TransactionStateManager extends EventEmitter {
// - `'dropped'` the tx nonce was already used
/**
@param txId {number} - the txMeta Id
@param status {string} - the status to set on the txMeta
@param {number} txId - the txMeta Id
@param {string} status - the status to set on the txMeta
@emits tx:status-update - passes txId and status
@emits ${txMeta.id}:finished - if it is a finished state. Passes the txMeta
@emits update:badge
@ -423,7 +470,7 @@ class TransactionStateManager extends EventEmitter {
/**
Saves the new/updated txList.
@param transactions {array} - the list of transactions to save
@param {array} transactions - the list of transactions to save
*/
// Function is intended only for internal use
_saveTxList (transactions) {
@ -436,4 +483,4 @@ class TransactionStateManager extends EventEmitter {
}
}
module.exports = TransactionStateManager
export default TransactionStateManager

View File

@ -1,13 +1,13 @@
/**
* Returns error without stack trace for better UI display
* @param {Error} err - error
* @returns {Error} Error with clean stack trace.
* @returns {Error} - Error with clean stack trace.
*/
function cleanErrorStack (err) {
var name = err.name
let name = err.name
name = (name === undefined) ? 'Error' : String(name)
var msg = err.message
let msg = err.message
msg = (msg === undefined) ? '' : String(msg)
if (name === '') {
@ -21,4 +21,4 @@ function cleanErrorStack (err) {
return err
}
module.exports = cleanErrorStack
export default cleanErrorStack

15
package-lock.json generated
View File

@ -4674,7 +4674,6 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz",
"integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==",
"dev": true,
"requires": {
"object-assign": "^4.1.1",
"util": "0.10.3"
@ -4683,14 +4682,12 @@
"inherits": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
"integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=",
"dev": true
"integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE="
},
"util": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
"dev": true,
"requires": {
"inherits": "2.0.1"
}
@ -45273,6 +45270,16 @@
"integrity": "sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8=",
"dev": true
},
"nonce-tracker": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/nonce-tracker/-/nonce-tracker-1.0.0.tgz",
"integrity": "sha512-hxKokxgLvOZx9A5qPQKwL34G1/YwMC5xJWZHFUKfvwxypkn2nP0KVJjbcoXwY6pXsRRa11KdFEPW61N4YCGnWQ==",
"requires": {
"assert": "^1.4.1",
"await-semaphore": "^0.1.3",
"ethjs-query": "^0.3.8"
}
},
"nopt": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",

View File

@ -157,6 +157,7 @@
"multihashes": "^0.4.12",
"nanoid": "^2.1.6",
"nifty-wallet-inpage-provider": "github:poanetwork/nifty-wallet-inpage-provider#1.2.3",
"nonce-tracker": "^1.0.0",
"number-to-bn": "^1.7.0",
"obj-multiplex": "^1.0.0",
"obs-store": "^4.0.3",

View File

@ -1,7 +1,7 @@
const assert = require('assert')
const cleanErrorStack = require('../../../app/scripts/lib/cleanErrorStack')
import assert from 'assert'
import cleanErrorStack from '../../../app/scripts/lib/cleanErrorStack'
describe('Clean Error Stack', () => {
describe('Clean Error Stack', function () {
const testMessage = 'Test Message'
const testError = new Error(testMessage)
@ -9,24 +9,24 @@ describe('Clean Error Stack', () => {
const blankErrorName = new Error(testMessage)
const blankMsgError = new Error()
beforeEach(() => {
beforeEach(function () {
undefinedErrorName.name = undefined
blankErrorName.name = ''
})
it('tests error with message', () => {
it('tests error with message', function () {
assert.equal(cleanErrorStack(testError), 'Error: Test Message')
})
it('tests error with undefined name', () => {
it('tests error with undefined name', function () {
assert.equal(cleanErrorStack(undefinedErrorName).toString(), 'Error: Test Message')
})
it('tests error with blank name', () => {
it('tests error with blank name', function () {
assert.equal(cleanErrorStack(blankErrorName).toString(), 'Test Message')
})
it('tests error with blank message', () => {
it('tests error with blank message', function () {
assert.equal(cleanErrorStack(blankMsgError), 'Error')
})

View File

@ -1,238 +0,0 @@
const assert = require('assert')
const NonceTracker = require('../../../../../app/scripts/controllers/transactions/nonce-tracker')
const MockTxGen = require('../../../../lib/mock-tx-gen')
const providerResultStub = {}
describe('Nonce Tracker', function () {
let nonceTracker, pendingTxs, confirmedTxs
describe('#getNonceLock', function () {
describe('with 3 confirmed and 1 pending', function () {
beforeEach(function () {
const txGen = new MockTxGen()
confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 3 })
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 1 })
nonceTracker = generateNonceTrackerWith(pendingTxs, confirmedTxs, '0x1')
})
it('should return 4', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '4', `nonce should be 4 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
it('should use localNonce if network returns a nonce lower then a confirmed tx in state', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '4', 'nonce should be 4')
await nonceLock.releaseLock()
})
})
describe('sentry issue 476304902', function () {
beforeEach(function () {
const txGen = new MockTxGen()
pendingTxs = txGen.generate({ status: 'submitted' }, {
fromNonce: 3,
count: 29,
})
nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x3')
})
it('should return 9', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '32', `nonce should be 32 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('issue 3670', function () {
beforeEach(function () {
const txGen = new MockTxGen()
pendingTxs = txGen.generate({ status: 'submitted' }, {
fromNonce: 6,
count: 3,
})
nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x6')
})
it('should return 9', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '9', `nonce should be 9 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('with no previous txs', function () {
beforeEach(function () {
nonceTracker = generateNonceTrackerWith([], [])
})
it('should return 0', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '0', `nonce should be 0 returned ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('with multiple previous txs with same nonce', function () {
beforeEach(function () {
const txGen = new MockTxGen()
confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 1 })
pendingTxs = txGen.generate({
status: 'submitted',
txParams: { nonce: '0x01' },
}, { count: 5 })
nonceTracker = generateNonceTrackerWith(pendingTxs, confirmedTxs, '0x0')
})
it('should return nonce after those', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '2', `nonce should be 2 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('when local confirmed count is higher than network nonce', function () {
beforeEach(function () {
const txGen = new MockTxGen()
confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 3 })
nonceTracker = generateNonceTrackerWith([], confirmedTxs, '0x1')
})
it('should return nonce after those', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '3', `nonce should be 3 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('when local pending count is higher than other metrics', function () {
beforeEach(function () {
const txGen = new MockTxGen()
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 2 })
nonceTracker = generateNonceTrackerWith(pendingTxs, [])
})
it('should return nonce after those', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '2', `nonce should be 2 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('when provider nonce is higher than other metrics', function () {
beforeEach(function () {
const txGen = new MockTxGen()
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 2 })
nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x05')
})
it('should return nonce after those', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '5', `nonce should be 5 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('when there are some pending nonces below the remote one and some over.', function () {
beforeEach(function () {
const txGen = new MockTxGen()
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 5 })
nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x03')
})
it('should return nonce after those', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '5', `nonce should be 5 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('when there are pending nonces non sequentially over the network nonce.', function () {
beforeEach(function () {
const txGen = new MockTxGen()
txGen.generate({ status: 'submitted' }, { count: 5 })
// 5 over that number
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 5 })
nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x00')
})
it('should return nonce after network nonce', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '0', `nonce should be 0 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('When all three return different values', function () {
beforeEach(function () {
const txGen = new MockTxGen()
confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 10 })
pendingTxs = txGen.generate({
status: 'submitted',
nonce: 100,
}, { count: 1 })
// 0x32 is 50 in hex:
nonceTracker = generateNonceTrackerWith(pendingTxs, confirmedTxs, '0x32')
})
it('should return nonce after network nonce', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '50', `nonce should be 50 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
describe('Faq issue 67', function () {
beforeEach(function () {
const txGen = new MockTxGen()
confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 64 })
pendingTxs = txGen.generate({
status: 'submitted',
}, { count: 10 })
// 0x40 is 64 in hex:
nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x40')
})
it('should return nonce after network nonce', async function () {
this.timeout(15000)
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
assert.equal(nonceLock.nextNonce, '74', `nonce should be 74 got ${nonceLock.nextNonce}`)
await nonceLock.releaseLock()
})
})
})
})
function generateNonceTrackerWith (pending, confirmed, providerStub = '0x0') {
const getPendingTransactions = () => pending
const getConfirmedTransactions = () => confirmed
providerResultStub.result = providerStub
const provider = {
sendAsync: (_, cb) => { cb(undefined, providerResultStub) },
}
const blockTracker = {
getCurrentBlock: () => '0x11b568',
getLatestBlock: async () => '0x11b568',
}
return new NonceTracker({
provider,
blockTracker,
getPendingTransactions,
getConfirmedTransactions,
})
}

View File

@ -417,20 +417,19 @@ describe('Transaction Controller', function () {
})
describe('#retryTransaction', function () {
it('should create a new txMeta with the same txParams as the original one but with a higher gasPrice', function (done) {
it('should create a new txMeta with the same txParams as the original one', function (done) {
const txParams = {
gasPrice: '0xee6b2800',
nonce: '0x00',
from: '0xB09d8505E1F4EF1CeA089D47094f5DD3464083d4',
to: '0xB09d8505E1F4EF1CeA089D47094f5DD3464083d4',
from: '0xb09d8505e1f4ef1cea089d47094f5dd3464083d4',
to: '0xb09d8505e1f4ef1cea089d47094f5dd3464083d4',
data: '0x0',
}
txController.txStateManager._saveTxList([
{ id: 1, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams, history: [{}] },
{ id: 1, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams, history: [] },
])
txController.retryTransaction(1)
.then((txMeta) => {
assert.equal(txMeta.txParams.gasPrice, '0x10642ac00', 'gasPrice should have a %10 gasPrice bump')
assert.equal(txMeta.txParams.nonce, txParams.nonce, 'nonce should be the same')
assert.equal(txMeta.txParams.from, txParams.from, 'from should be the same')
assert.equal(txMeta.txParams.to, txParams.to, 'to should be the same')

View File

@ -1,5 +1,5 @@
const assert = require('assert')
const TxStateManager = require('../../../../../app/scripts/controllers/transactions/tx-state-manager')
import assert from 'assert'
import TxStateManager from '../../../../../app/scripts/controllers/transactions/tx-state-manager'
import txStateHistoryHelper from '../../../../../app/scripts/controllers/transactions/lib/tx-state-history-helper'
const noop = () => true
@ -29,7 +29,7 @@ describe('TransactionStateManager', function () {
assert.equal(result[0].status, 'signed')
})
it('should emit a signed event to signal the exciton of callback', (done) => {
it('should emit a signed event to signal the exciton of callback', function (done) {
const tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
const noop = function () {
assert(true, 'event listener has been triggered and noop executed')
@ -52,13 +52,10 @@ describe('TransactionStateManager', function () {
assert.equal(result.length, 0)
})
it('should emit a rejected event to signal the exciton of callback', (done) => {
it('should emit a rejected event to signal the exciton of callback', function (done) {
const tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
txStateManager.addTx(tx)
const noop = function (err, txId) {
if (err) {
console.log('Error: ', err)
}
const noop = () => {
assert(true, 'event listener has been triggered and noop executed')
done()
}
@ -93,6 +90,37 @@ describe('TransactionStateManager', function () {
assert.equal(result[0].id, 1)
})
it('throws error and does not add tx if txParams are invalid', function () {
const validTxParams = {
from: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
to: '0x0039f22efb07a647557c7c5d17854cfd6d489ef3',
nonce: '0x3',
gas: '0x77359400',
gasPrice: '0x77359400',
value: '0x0',
data: '0x0',
}
const invalidValues = [1, true, {}, Symbol('1')]
for (const key in validTxParams) {
for (const value of invalidValues) {
const tx = {
id: 1,
status: 'unapproved',
metamaskNetworkId: currentNetworkId,
txParams: {
...validTxParams,
[key]: value,
},
}
assert.throws(txStateManager.addTx.bind(txStateManager, tx), 'addTx should throw error')
const result = txStateManager.getTxList()
assert.ok(Array.isArray(result), 'txList should be an array')
assert.equal(result.length, 0, 'txList should be empty')
}
}
})
it('does not override txs from other networks', function () {
const tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }
const tx2 = { id: 2, status: 'confirmed', metamaskNetworkId: otherNetworkId, txParams: {} }
@ -153,6 +181,37 @@ describe('TransactionStateManager', function () {
assert.equal(result.hash, 'foo')
})
it('throws error and does not update tx if txParams are invalid', function () {
const validTxParams = {
from: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
to: '0x0039f22efb07a647557c7c5d17854cfd6d489ef3',
nonce: '0x3',
gas: '0x77359400',
gasPrice: '0x77359400',
value: '0x0',
data: '0x0',
}
const invalidValues = [1, true, {}, Symbol('1')]
txStateManager.addTx({ id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: validTxParams })
for (const key in validTxParams) {
for (const value of invalidValues) {
const originalTx = txStateManager.getTx(1)
const newTx = {
...originalTx,
txParams: {
...originalTx.txParams,
[key]: value,
},
}
assert.throws(txStateManager.updateTx.bind(txStateManager, newTx), 'updateTx should throw an error')
const result = txStateManager.getTx(1)
assert.deepEqual(result, originalTx, 'tx should not be updated')
}
}
})
it('updates gas price and adds history items', function () {
const originalGasPrice = '0x01'
const desiredGasPrice = '0x02'
@ -241,6 +300,8 @@ describe('TransactionStateManager', function () {
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)}`)
filterParams = { status: (status) => status !== 'confirmed' }
assert.equal(txStateManager.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
})
})
@ -283,19 +344,18 @@ describe('TransactionStateManager', function () {
assert.equal(txsFromCurrentNetworkAndAddress.length, 0)
assert.equal(txFromOtherNetworks.length, 2)
})
})
describe('#_removeTx', function () {
it('should remove the transaction from the storage', () => {
txStateManager._saveTxList([ {id: 1} ])
it('should remove the transaction from the storage', function () {
txStateManager._saveTxList([ { id: 1 } ])
txStateManager._removeTx(1)
assert(!txStateManager.getFullTxList().length, 'txList should be empty')
})
it('should only remove the transaction with ID 1 from the storage', () => {
txStateManager._saveTxList([ {id: 1}, {id: 2} ])
it('should only remove the transaction with ID 1 from the storage', function () {
txStateManager._saveTxList([ { id: 1 }, { id: 2 } ])
txStateManager._removeTx(1)
assert.equal(txStateManager.getFullTxList()[0].id, 2, 'txList should have a id of 2')
})