1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 18:00:18 +01:00

Merge pull request #4042 from MetaMask/tx-controller-rewrite-v3

docs and file organization for txController
This commit is contained in:
kumavis 2018-04-25 16:52:46 -06:00 committed by GitHub
commit dcd04091cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 861 additions and 396 deletions

View File

@ -0,0 +1,92 @@
# Transaction Controller
Transaction Controller is an aggregate of sub-controllers and trackers
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
## Flow diagram of processing a transaction
![transaction-flow](../../../../docs/transaction-flow.png)
## txMeta's & txParams
A txMeta is the "meta" object it has all the random bits of info we need about a transaction on it. txParams are sacred every thing on txParams gets signed so it must
be a valid key and be hex prefixed except for the network number. Extra stuff must go on the txMeta!
Here is a txMeta too look at:
```js
txMeta = {
"id": 2828415030114568, // unique id for this txMeta used for look ups
"time": 1524094064821, // time of creation
"status": "confirmed",
"metamaskNetworkId": "1524091532133", //the network id for the transaction
"loadingDefaults": false, // used to tell the ui when we are done calculatyig gass defaults
"txParams": { // the txParams object
"from": "0x8acce2391c0d510a6c5e5d8f819a678f79b7e675",
"to": "0x8acce2391c0d510a6c5e5d8f819a678f79b7e675",
"value": "0x0",
"gasPrice": "0x3b9aca00",
"gas": "0x7b0c",
"nonce": "0x0"
},
"history": [{ //debug
"id": 2828415030114568,
"time": 1524094064821,
"status": "unapproved",
"metamaskNetworkId": "1524091532133",
"loadingDefaults": true,
"txParams": {
"from": "0x8acce2391c0d510a6c5e5d8f819a678f79b7e675",
"to": "0x8acce2391c0d510a6c5e5d8f819a678f79b7e675",
"value": "0x0"
}
},
[
{
"op": "add",
"path": "/txParams/gasPrice",
"value": "0x3b9aca00"
},
...], // I've removed most of history for this
"gasPriceSpecified": false, //whether or not the user/dapp has specified gasPrice
"gasLimitSpecified": false, //whether or not the user/dapp has specified gas
"estimatedGas": "5208",
"origin": "MetaMask", //debug
"nonceDetails": {
"params": {
"highestLocallyConfirmed": 0,
"highestSuggested": 0,
"nextNetworkNonce": 0
},
"local": {
"name": "local",
"nonce": 0,
"details": {
"startPoint": 0,
"highest": 0
}
},
"network": {
"name": "network",
"nonce": 0,
"details": {
"baseCount": 0
}
}
},
"rawTx": "0xf86980843b9aca00827b0c948acce2391c0d510a6c5e5d8f819a678f79b7e67580808602c5b5de66eea05c01a320b96ac730cb210ca56d2cb71fa360e1fc2c21fa5cf333687d18eb323fa02ed05987a6e5fd0f2459fcff80710b76b83b296454ad9a37594a0ccb4643ea90", // used for rebroadcast
"hash": "0xa45ba834b97c15e6ff4ed09badd04ecd5ce884b455eb60192cdc73bcc583972a",
"submittedTime": 1524094077902 // time of the attempt to submit the raw tx to the network, used in the ui to show the retry button
}
```

View File

@ -3,28 +3,42 @@ const ObservableStore = require('obs-store')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const Transaction = require('ethereumjs-tx') const Transaction = require('ethereumjs-tx')
const EthQuery = require('ethjs-query') const EthQuery = require('ethjs-query')
const TransactionStateManager = require('../lib/tx-state-manager') const TransactionStateManager = require('./tx-state-manager')
const TxGasUtil = require('../lib/tx-gas-utils') const TxGasUtil = require('./tx-gas-utils')
const PendingTransactionTracker = require('../lib/pending-tx-tracker') const PendingTransactionTracker = require('./pending-tx-tracker')
const NonceTracker = require('../lib/nonce-tracker') const NonceTracker = require('./nonce-tracker')
const txUtils = require('./lib/util')
const log = require('loglevel') const log = require('loglevel')
/* /**
Transaction Controller is an aggregate of sub-controllers and trackers Transaction Controller is an aggregate of sub-controllers and trackers
composing them in a way to be exposed to the metamask controller composing them in a way to be exposed to the metamask controller
- txStateManager <br>- txStateManager
responsible for the state of a transaction and responsible for the state of a transaction and
storing the transaction storing the transaction
- pendingTxTracker <br>- pendingTxTracker
watching blocks for transactions to be include watching blocks for transactions to be include
and emitting confirmed events and emitting confirmed events
- txGasUtil <br>- txGasUtil
gas calculations and safety buffering gas calculations and safety buffering
- nonceTracker <br>- nonceTracker
calculating nonces calculating nonces
@class
@param {object} - opts
@param {object} opts.initState - initial transaction list default is an empty array
@param {Object} opts.networkStore - an observable store for network number
@param {Object} opts.blockTracker - An instance of eth-blocktracker
@param {Object} opts.provider - A network provider.
@param {Function} opts.signTransaction - function the signs an ethereumjs-tx
@param {Function} [opts.getGasPrice] - optional gas price calculator
@param {Function} opts.signTransaction - ethTx signer that returns a rawTx
@param {Number} [opts.txHistoryLimit] - number *optional* for limiting how many transactions are in state
@param {Object} opts.preferencesStore
*/ */
module.exports = class TransactionController extends EventEmitter { class TransactionController extends EventEmitter {
constructor (opts) { constructor (opts) {
super() super()
this.networkStore = opts.networkStore || new ObservableStore({}) this.networkStore = opts.networkStore || new ObservableStore({})
@ -38,45 +52,19 @@ module.exports = class TransactionController extends EventEmitter {
this.query = new EthQuery(this.provider) this.query = new EthQuery(this.provider)
this.txGasUtil = new TxGasUtil(this.provider) this.txGasUtil = new TxGasUtil(this.provider)
this._mapMethods()
this.txStateManager = new TransactionStateManager({ this.txStateManager = new TransactionStateManager({
initState: opts.initState, initState: opts.initState,
txHistoryLimit: opts.txHistoryLimit, txHistoryLimit: opts.txHistoryLimit,
getNetwork: this.getNetwork.bind(this), getNetwork: this.getNetwork.bind(this),
}) })
this._onBootCleanUp()
this.txStateManager.getFilteredTxList({
status: 'unapproved',
loadingDefaults: true,
}).forEach((tx) => {
this.addTxDefaults(tx)
.then((txMeta) => {
txMeta.loadingDefaults = false
this.txStateManager.updateTx(txMeta, 'transactions: gas estimation for tx on boot')
}).catch((error) => {
this.txStateManager.setTxStatusFailed(tx.id, error)
})
})
this.txStateManager.getFilteredTxList({
status: 'approved',
}).forEach((txMeta) => {
const txSignError = new Error('Transaction found as "approved" during boot - possibly stuck during signing')
this.txStateManager.setTxStatusFailed(txMeta.id, txSignError)
})
this.store = this.txStateManager.store 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: this.txStateManager.getPendingTransactions.bind(this.txStateManager), getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager),
getConfirmedTransactions: (address) => { getConfirmedTransactions: this.txStateManager.getConfirmedTransactions.bind(this.txStateManager),
return this.txStateManager.getFilteredTxList({
from: address,
status: 'confirmed',
err: undefined,
})
},
}) })
this.pendingTxTracker = new PendingTransactionTracker({ this.pendingTxTracker = new PendingTransactionTracker({
@ -88,60 +76,14 @@ module.exports = class TransactionController extends EventEmitter {
}) })
this.txStateManager.store.subscribe(() => this.emit('update:badge')) this.txStateManager.store.subscribe(() => this.emit('update:badge'))
this._setupListners()
this.pendingTxTracker.on('tx:warning', (txMeta) => {
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:warning')
})
this.pendingTxTracker.on('tx:confirmed', (txId) => this._markNonceDuplicatesDropped(txId))
this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager))
this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => {
if (!txMeta.firstRetryBlockNumber) {
txMeta.firstRetryBlockNumber = latestBlockNumber
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:block-update')
}
})
this.pendingTxTracker.on('tx:retry', (txMeta) => {
if (!('retryCount' in txMeta)) txMeta.retryCount = 0
txMeta.retryCount++
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:retry')
})
this.blockTracker.on('block', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker))
// this is a little messy but until ethstore has been either
// removed or redone this is to guard against the race condition
this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.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.txStateManager.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())
} }
/** @returns {number} the chainId*/
getState () {
return this.memStore.getState()
}
getNetwork () {
return this.networkStore.getState()
}
getSelectedAddress () {
return this.preferencesStore.getState().selectedAddress
}
getUnapprovedTxCount () {
return Object.keys(this.txStateManager.getUnapprovedTxList()).length
}
getPendingTxCount (account) {
return this.txStateManager.getPendingTransactions(account).length
}
getFilteredTxList (opts) {
return this.txStateManager.getFilteredTxList(opts)
}
getChainId () { getChainId () {
const networkState = this.networkStore.getState() const networkState = this.networkStore.getState()
const getChainId = parseInt(networkState) const getChainId = parseInt(networkState)
@ -152,16 +94,30 @@ module.exports = class TransactionController extends EventEmitter {
} }
} }
wipeTransactions (address) { /**
this.txStateManager.wipeTransactions(address) Adds a tx to the txlist
} @emits ${txMeta.id}:unapproved
*/
// Adds a tx to the txlist
addTx (txMeta) { addTx (txMeta) {
this.txStateManager.addTx(txMeta) this.txStateManager.addTx(txMeta)
this.emit(`${txMeta.id}:unapproved`, txMeta) this.emit(`${txMeta.id}:unapproved`, txMeta)
} }
/**
Wipes the transactions for a given account
@param {string} address - hex string of the from address for txs being removed
*/
wipeTransactions (address) {
this.txStateManager.wipeTransactions(address)
}
/**
add a new unapproved transaction to the pipeline
@returns {Promise<string>} the hash of the transaction after being submitted to the network
@param txParams {object} - txParams for the transaction
@param opts {object} - with the key origin to put the origin on the txMeta
*/
async newUnapprovedTransaction (txParams, opts = {}) { async newUnapprovedTransaction (txParams, opts = {}) {
log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`) log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`)
const initialTxMeta = await this.addUnapprovedTransaction(txParams) const initialTxMeta = await this.addUnapprovedTransaction(txParams)
@ -184,17 +140,24 @@ module.exports = class TransactionController extends EventEmitter {
}) })
} }
/**
Validates and generates a txMeta with defaults and puts it in txStateManager
store
@returns {txMeta}
*/
async addUnapprovedTransaction (txParams) { async addUnapprovedTransaction (txParams) {
// validate // validate
const normalizedTxParams = this._normalizeTxParams(txParams) const normalizedTxParams = txUtils.normalizeTxParams(txParams)
this._validateTxParams(normalizedTxParams) txUtils.validateTxParams(normalizedTxParams)
// construct txMeta // construct txMeta
let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams }) let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams })
this.addTx(txMeta) this.addTx(txMeta)
this.emit('newUnapprovedTx', txMeta) this.emit('newUnapprovedTx', txMeta)
// add default tx params // add default tx params
try { try {
txMeta = await this.addTxDefaults(txMeta) txMeta = await this.addTxGasDefaults(txMeta)
} catch (error) { } catch (error) {
console.log(error) console.log(error)
this.txStateManager.setTxStatusFailed(txMeta.id, error) this.txStateManager.setTxStatusFailed(txMeta.id, error)
@ -206,21 +169,33 @@ module.exports = class TransactionController extends EventEmitter {
return txMeta return txMeta
} }
/**
async addTxDefaults (txMeta) { adds the tx gas defaults: gas && gasPrice
@param txMeta {Object} - the txMeta object
@returns {Promise<object>} resolves with txMeta
*/
async addTxGasDefaults (txMeta) {
const txParams = txMeta.txParams const txParams = txMeta.txParams
// ensure value // ensure value
txParams.value = txParams.value ? ethUtil.addHexPrefix(txParams.value) : '0x0'
txMeta.gasPriceSpecified = Boolean(txParams.gasPrice) txMeta.gasPriceSpecified = Boolean(txParams.gasPrice)
let gasPrice = txParams.gasPrice let gasPrice = txParams.gasPrice
if (!gasPrice) { if (!gasPrice) {
gasPrice = this.getGasPrice ? this.getGasPrice() : await this.query.gasPrice() gasPrice = this.getGasPrice ? this.getGasPrice() : await this.query.gasPrice()
} }
txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16)) txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16))
txParams.value = txParams.value || '0x0'
// set gasLimit // set gasLimit
return await this.txGasUtil.analyzeGasUsage(txMeta) return await this.txGasUtil.analyzeGasUsage(txMeta)
} }
/**
Creates a new txMeta with the same txParams as the original
to allow the user to resign the transaction with a higher gas values
@param originalTxId {number} - the id of the txMeta that
you want to attempt to retry
@return {txMeta}
*/
async retryTransaction (originalTxId) { async retryTransaction (originalTxId) {
const originalTxMeta = this.txStateManager.getTx(originalTxId) const originalTxMeta = this.txStateManager.getTx(originalTxId)
const lastGasPrice = originalTxMeta.txParams.gasPrice const lastGasPrice = originalTxMeta.txParams.gasPrice
@ -234,15 +209,31 @@ module.exports = class TransactionController extends EventEmitter {
return txMeta return txMeta
} }
/**
updates the txMeta in the txStateManager
@param txMeta {Object} - the updated txMeta
*/
async updateTransaction (txMeta) { async updateTransaction (txMeta) {
this.txStateManager.updateTx(txMeta, 'confTx: user updated transaction') this.txStateManager.updateTx(txMeta, 'confTx: user updated transaction')
} }
/**
updates and approves the transaction
@param txMeta {Object}
*/
async updateAndApproveTransaction (txMeta) { async updateAndApproveTransaction (txMeta) {
this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction') this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction')
await this.approveTransaction(txMeta.id) await this.approveTransaction(txMeta.id)
} }
/**
sets the tx status to approved
auto fills the nonce
signs the transaction
publishes the transaction
if any of these steps fails the tx status will be set to failed
@param txId {number} - the tx's Id
*/
async approveTransaction (txId) { async approveTransaction (txId) {
let nonceLock let nonceLock
try { try {
@ -274,7 +265,11 @@ module.exports = class TransactionController extends EventEmitter {
throw err throw err
} }
} }
/**
adds the chain id and signs the transaction and set the status to signed
@param txId {number} - the tx's Id
@returns - rawTx {string}
*/
async signTransaction (txId) { async signTransaction (txId) {
const txMeta = this.txStateManager.getTx(txId) const txMeta = this.txStateManager.getTx(txId)
// add network/chain id // add network/chain id
@ -290,6 +285,12 @@ module.exports = class TransactionController extends EventEmitter {
return rawTx return rawTx
} }
/**
publishes the raw tx and sets the txMeta to submitted
@param txId {number} - the tx's Id
@param rawTx {string} - the hex string of the serialized signed transaction
@returns {Promise<void>}
*/
async publishTransaction (txId, rawTx) { async publishTransaction (txId, rawTx) {
const txMeta = this.txStateManager.getTx(txId) const txMeta = this.txStateManager.getTx(txId)
txMeta.rawTx = rawTx txMeta.rawTx = rawTx
@ -299,11 +300,20 @@ module.exports = class TransactionController extends EventEmitter {
this.txStateManager.setTxStatusSubmitted(txId) this.txStateManager.setTxStatusSubmitted(txId)
} }
/**
Convenience method for the ui thats sets the transaction to rejected
@param txId {number} - the tx's Id
@returns {Promise<void>}
*/
async cancelTransaction (txId) { async cancelTransaction (txId) {
this.txStateManager.setTxStatusRejected(txId) this.txStateManager.setTxStatusRejected(txId)
} }
// receives a txHash records the tx as signed /**
Sets the txHas on the txMeta
@param txId {number} - the tx's Id
@param txHash {string} - the hash for the txMeta
*/
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.txStateManager.getTx(txId) const txMeta = this.txStateManager.getTx(txId)
@ -314,63 +324,92 @@ module.exports = class TransactionController extends EventEmitter {
// //
// PRIVATE METHODS // PRIVATE METHODS
// //
/** maps methods for convenience*/
_mapMethods () {
/** @returns the state in transaction controller */
this.getState = () => this.memStore.getState()
/** @returns the network number stored in networkStore */
this.getNetwork = () => this.networkStore.getState()
/** @returns the user selected address */
this.getSelectedAddress = () => this.preferencesStore.getState().selectedAddress
/** Returns an array of transactions whos status is unapproved */
this.getUnapprovedTxCount = () => Object.keys(this.txStateManager.getUnapprovedTxList()).length
/**
@returns a number that represents how many transactions have the status submitted
@param account {String} - hex prefixed account
*/
this.getPendingTxCount = (account) => this.txStateManager.getPendingTransactions(account).length
/** see txStateManager */
this.getFilteredTxList = (opts) => this.txStateManager.getFilteredTxList(opts)
}
_normalizeTxParams (txParams) { /**
// functions that handle normalizing of that key in txParams If transaction controller was rebooted with transactions that are uncompleted
const whiteList = { in steps of the transaction signing or user confirmation process it will either
from: from => ethUtil.addHexPrefix(from).toLowerCase(), transition txMetas to a failed state or try to redo those tasks.
to: to => ethUtil.addHexPrefix(txParams.to).toLowerCase(), */
nonce: nonce => ethUtil.addHexPrefix(nonce),
value: value => ethUtil.addHexPrefix(value),
data: data => ethUtil.addHexPrefix(data),
gas: gas => ethUtil.addHexPrefix(gas),
gasPrice: gasPrice => ethUtil.addHexPrefix(gasPrice),
}
// apply only keys in the whiteList _onBootCleanUp () {
const normalizedTxParams = {} this.txStateManager.getFilteredTxList({
Object.keys(whiteList).forEach((key) => { status: 'unapproved',
if (txParams[key]) normalizedTxParams[key] = whiteList[key](txParams[key]) loadingDefaults: true,
}).forEach((tx) => {
this.addTxGasDefaults(tx)
.then((txMeta) => {
txMeta.loadingDefaults = false
this.txStateManager.updateTx(txMeta, 'transactions: gas estimation for tx on boot')
}).catch((error) => {
this.txStateManager.setTxStatusFailed(tx.id, error)
})
}) })
return normalizedTxParams this.txStateManager.getFilteredTxList({
status: 'approved',
}).forEach((txMeta) => {
const txSignError = new Error('Transaction found as "approved" during boot - possibly stuck during signing')
this.txStateManager.setTxStatusFailed(txMeta.id, txSignError)
})
} }
_validateTxParams (txParams) { /**
this._validateFrom(txParams) is called in constructor applies the listeners for pendingTxTracker txStateManager
this._validateRecipient(txParams) and blockTracker
if ('value' in txParams) { */
const value = txParams.value.toString() _setupListners () {
if (value.includes('-')) { this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update'))
throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`) this.pendingTxTracker.on('tx:warning', (txMeta) => {
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:warning')
})
this.pendingTxTracker.on('tx:confirmed', (txId) => this.txStateManager.setTxStatusConfirmed(txId))
this.pendingTxTracker.on('tx:confirmed', (txId) => this._markNonceDuplicatesDropped(txId))
this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager))
this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => {
if (!txMeta.firstRetryBlockNumber) {
txMeta.firstRetryBlockNumber = latestBlockNumber
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:block-update')
} }
})
this.pendingTxTracker.on('tx:retry', (txMeta) => {
if (!('retryCount' in txMeta)) txMeta.retryCount = 0
txMeta.retryCount++
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:retry')
})
this.blockTracker.on('block', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker))
// this is a little messy but until ethstore has been either
// removed or redone this is to guard against the race condition
this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.bind(this.pendingTxTracker))
this.blockTracker.on('sync', this.pendingTxTracker.queryPendingTxs.bind(this.pendingTxTracker))
if (value.includes('.')) {
throw new Error(`Invalid transaction value of ${txParams.value} number must be in wei`)
}
}
} }
_validateFrom (txParams) { /**
if ( !(typeof txParams.from === 'string') ) throw new Error(`Invalid from address ${txParams.from} not a string`) Sets other txMeta statuses to dropped if the txMeta that has been confirmed has other transactions
if (!ethUtil.isValidAddress(txParams.from)) throw new Error('Invalid from address') in the list have the same nonce
}
_validateRecipient (txParams) {
if (txParams.to === '0x' || txParams.to === null ) {
if (txParams.data) {
delete txParams.to
} else {
throw new Error('Invalid recipient address')
}
} else if ( txParams.to !== undefined && !ethUtil.isValidAddress(txParams.to) ) {
throw new Error('Invalid recipient address')
}
return txParams
}
@param txId {Number} - the txId of the transaction that has been confirmed in a block
*/
_markNonceDuplicatesDropped (txId) { _markNonceDuplicatesDropped (txId) {
this.txStateManager.setTxStatusConfirmed(txId)
// get the confirmed transactions nonce and from address // get the confirmed transactions nonce and from address
const txMeta = this.txStateManager.getTx(txId) const txMeta = this.txStateManager.getTx(txId)
const { nonce, from } = txMeta.txParams const { nonce, from } = txMeta.txParams
@ -385,6 +424,9 @@ module.exports = class TransactionController extends EventEmitter {
}) })
} }
/**
Updates the memStore in transaction controller
*/
_updateMemstore () { _updateMemstore () {
const unapprovedTxs = this.txStateManager.getUnapprovedTxList() const unapprovedTxs = this.txStateManager.getUnapprovedTxList()
const selectedAddressTxList = this.txStateManager.getFilteredTxList({ const selectedAddressTxList = this.txStateManager.getFilteredTxList({
@ -394,3 +436,5 @@ module.exports = class TransactionController extends EventEmitter {
this.memStore.updateState({ unapprovedTxs, selectedAddressTxList }) this.memStore.updateState({ unapprovedTxs, selectedAddressTxList })
} }
} }
module.exports = TransactionController

View File

@ -1,6 +1,6 @@
const jsonDiffer = require('fast-json-patch') const jsonDiffer = require('fast-json-patch')
const clone = require('clone') const clone = require('clone')
/** @module*/
module.exports = { module.exports = {
generateHistoryEntry, generateHistoryEntry,
replayHistory, replayHistory,
@ -8,7 +8,11 @@ module.exports = {
migrateFromSnapshotsToDiffs, migrateFromSnapshotsToDiffs,
} }
/**
converts non-initial history entries into diffs
@param longHistory {array}
@returns {array}
*/
function migrateFromSnapshotsToDiffs (longHistory) { function migrateFromSnapshotsToDiffs (longHistory) {
return ( return (
longHistory longHistory
@ -20,6 +24,17 @@ function migrateFromSnapshotsToDiffs (longHistory) {
) )
} }
/**
generates an array of history objects sense the previous state.
The object has the keys opp(the operation preformed),
path(the key and if a nested object then each key will be seperated with a `/`)
value
with the first entry having the note
@param previousState {object} - the previous state of the object
@param newState {object} - the update object
@param note {string} - a optional note for the state change
@reurns {array}
*/
function generateHistoryEntry (previousState, newState, note) { function generateHistoryEntry (previousState, newState, note) {
const entry = jsonDiffer.compare(previousState, newState) const entry = jsonDiffer.compare(previousState, newState)
// Add a note to the first op, since it breaks if we append it to the entry // Add a note to the first op, since it breaks if we append it to the entry
@ -27,11 +42,19 @@ function generateHistoryEntry (previousState, newState, note) {
return entry return entry
} }
/**
Recovers previous txMeta state obj
@return {object}
*/
function replayHistory (_shortHistory) { function replayHistory (_shortHistory) {
const shortHistory = clone(_shortHistory) const shortHistory = clone(_shortHistory)
return shortHistory.reduce((val, entry) => jsonDiffer.applyPatch(val, entry).newDocument) return shortHistory.reduce((val, entry) => jsonDiffer.applyPatch(val, entry).newDocument)
} }
/**
@param txMeta {Object}
@returns {object} a clone object of the txMeta with out history
*/
function snapshotFromTxMeta (txMeta) { function snapshotFromTxMeta (txMeta) {
// create txMeta snapshot for history // create txMeta snapshot for history
const snapshot = clone(txMeta) const snapshot = clone(txMeta)

View File

@ -0,0 +1,99 @@
const {
addHexPrefix,
isValidAddress,
} = require('ethereumjs-util')
/**
@module
*/
module.exports = {
normalizeTxParams,
validateTxParams,
validateFrom,
validateRecipient,
getFinalStates,
}
// functions that handle normalizing of that key in txParams
const normalizers = {
from: from => addHexPrefix(from).toLowerCase(),
to: to => addHexPrefix(to).toLowerCase(),
nonce: nonce => addHexPrefix(nonce),
value: value => addHexPrefix(value),
data: data => addHexPrefix(data),
gas: gas => addHexPrefix(gas),
gasPrice: gasPrice => addHexPrefix(gasPrice),
}
/**
normalizes txParams
@param txParams {object}
@returns {object} normalized txParams
*/
function normalizeTxParams (txParams) {
// apply only keys in the normalizers
const normalizedTxParams = {}
for (const key in normalizers) {
if (txParams[key]) normalizedTxParams[key] = normalizers[key](txParams[key])
}
return normalizedTxParams
}
/**
validates txParams
@param txParams {object}
*/
function validateTxParams (txParams) {
validateFrom(txParams)
validateRecipient(txParams)
if ('value' in txParams) {
const value = txParams.value.toString()
if (value.includes('-')) {
throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)
}
if (value.includes('.')) {
throw new Error(`Invalid transaction value of ${txParams.value} number must be in wei`)
}
}
}
/**
validates the from field in txParams
@param txParams {object}
*/
function validateFrom (txParams) {
if (!(typeof txParams.from === 'string')) throw new Error(`Invalid from address ${txParams.from} not a string`)
if (!isValidAddress(txParams.from)) throw new Error('Invalid from address')
}
/**
validates the to field in txParams
@param txParams {object}
*/
function validateRecipient (txParams) {
if (txParams.to === '0x' || txParams.to === null) {
if (txParams.data) {
delete txParams.to
} else {
throw new Error('Invalid recipient address')
}
} else if (txParams.to !== undefined && !isValidAddress(txParams.to)) {
throw new Error('Invalid recipient address')
}
return txParams
}
/**
@returns an {array} of states that can be considered final
*/
function getFinalStates () {
return [
'rejected', // the user has responded no!
'confirmed', // the tx has been included in a block.
'failed', // the tx failed for some reason, included on tx data.
'dropped', // the tx nonce was already used
]
}

View File

@ -1,7 +1,15 @@
const EthQuery = require('ethjs-query') const EthQuery = require('ethjs-query')
const assert = require('assert') const assert = require('assert')
const Mutex = require('await-semaphore').Mutex 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 { class NonceTracker {
constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) { constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) {
@ -12,6 +20,9 @@ class NonceTracker {
this.lockMap = {} this.lockMap = {}
} }
/**
@returns {Promise<Object>} with the key releaseLock (the gloabl mutex)
*/
async getGlobalLock () { async getGlobalLock () {
const globalMutex = this._lookupMutex('global') const globalMutex = this._lookupMutex('global')
// await global mutex free // await global mutex free
@ -19,8 +30,20 @@ class NonceTracker {
return { releaseLock } return { releaseLock }
} }
// releaseLock must be called /**
// releaseLock must be called after adding signed tx to pending transactions (or discarding) * @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} highetSuggested - 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) { async getNonceLock (address) {
// await global mutex free // await global mutex free
await this._globalMutexFree() await this._globalMutexFree()
@ -123,6 +146,17 @@ class NonceTracker {
return highestNonce 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) { _getHighestContinuousFrom (txList, startPoint) {
const nonces = txList.map((txMeta) => { const nonces = txList.map((txMeta) => {
const nonce = txMeta.txParams.nonce const nonce = txMeta.txParams.nonce
@ -140,6 +174,10 @@ class NonceTracker {
// this is a hotfix for the fact that the blockTracker will // this is a hotfix for the fact that the blockTracker will
// change when the network changes // change when the network changes
/**
@returns {Object} the current blockTracker
*/
_getBlockTracker () { _getBlockTracker () {
return this.provider._blockTracker return this.provider._blockTracker
} }

View File

@ -1,23 +1,24 @@
const EventEmitter = require('events') const EventEmitter = require('events')
const log = require('loglevel')
const EthQuery = require('ethjs-query') const EthQuery = require('ethjs-query')
/* /**
Utility class for tracking the transactions as they
go from a pending state to a confirmed (mined in a block) state
Event emitter utility class for tracking the transactions as they<br>
go from a pending state to a confirmed (mined in a block) state<br>
<br>
As well as continues broadcast while in the pending state As well as continues broadcast while in the pending state
<br>
@param config {object} - non optional configuration object consists of:
@param {Object} config.provider - A network provider.
@param {Object} config.nonceTracker see nonce tracker
@param {function} config.getPendingTransactions a function for getting an array of transactions,
@param {function} config.publishTransaction a async function for publishing raw transactions,
~config is not optional~
requires a: {
provider: //,
nonceTracker: //see nonce tracker,
getPendingTransactions: //() a function for getting an array of transactions,
publishTransaction: //(rawTx) a async function for publishing raw transactions,
}
@class
*/ */
module.exports = class PendingTransactionTracker extends EventEmitter { class PendingTransactionTracker extends EventEmitter {
constructor (config) { constructor (config) {
super() super()
this.query = new EthQuery(config.provider) this.query = new EthQuery(config.provider)
@ -29,8 +30,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
this._checkPendingTxs() this._checkPendingTxs()
} }
// checks if a signed tx is in a block and /**
// if included sets the tx status as 'confirmed' checks if a signed tx is in a block and
if it is included emits tx status as 'confirmed'
@param block {object}, a full block
@emits tx:confirmed
@emits tx:failed
*/
checkForTxInBlock (block) { checkForTxInBlock (block) {
const signedTxList = this.getPendingTransactions() const signedTxList = this.getPendingTransactions()
if (!signedTxList.length) return if (!signedTxList.length) return
@ -52,6 +58,11 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
}) })
} }
/**
asks the network for the transaction to see if a block number is included on it
if we have skipped/missed blocks
@param object - oldBlock newBlock
*/
queryPendingTxs ({ oldBlock, newBlock }) { queryPendingTxs ({ oldBlock, newBlock }) {
// check pending transactions on start // check pending transactions on start
if (!oldBlock) { if (!oldBlock) {
@ -63,7 +74,11 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
if (diff > 1) this._checkPendingTxs() if (diff > 1) this._checkPendingTxs()
} }
/**
Will resubmit any transactions who have not been confirmed in a block
@param block {object} - a block object
@emits tx:warning
*/
resubmitPendingTxs (block) { resubmitPendingTxs (block) {
const pending = this.getPendingTransactions() const pending = this.getPendingTransactions()
// only try resubmitting if their are transactions to resubmit // only try resubmitting if their are transactions to resubmit
@ -100,6 +115,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
})) }))
} }
/**
resubmits the individual txMeta used in resubmitPendingTxs
@param txMeta {Object} - txMeta object
@param latestBlockNumber {string} - hex string for the latest block number
@emits tx:retry
@returns txHash {string}
*/
async _resubmitTx (txMeta, latestBlockNumber) { async _resubmitTx (txMeta, latestBlockNumber) {
if (!txMeta.firstRetryBlockNumber) { if (!txMeta.firstRetryBlockNumber) {
this.emit('tx:block-update', txMeta, latestBlockNumber) this.emit('tx:block-update', txMeta, latestBlockNumber)
@ -123,7 +145,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
this.emit('tx:retry', txMeta) this.emit('tx:retry', txMeta)
return txHash return txHash
} }
/**
Ask the network for the transaction to see if it has been include in a block
@param txMeta {Object} - the txMeta object
@emits tx:failed
@emits tx:confirmed
@emits tx:warning
*/
async _checkPendingTx (txMeta) { async _checkPendingTx (txMeta) {
const txHash = txMeta.hash const txHash = txMeta.hash
const txId = txMeta.id const txId = txMeta.id
@ -162,8 +190,9 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
} }
} }
// checks the network for signed txs and /**
// if confirmed sets the tx status as 'confirmed' checks the network for signed txs and releases the nonce global lock if it is
*/
async _checkPendingTxs () { async _checkPendingTxs () {
const signedTxList = this.getPendingTransactions() const signedTxList = this.getPendingTransactions()
// in order to keep the nonceTracker accurate we block it while updating pending transactions // in order to keep the nonceTracker accurate we block it while updating pending transactions
@ -171,12 +200,17 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
try { try {
await Promise.all(signedTxList.map((txMeta) => this._checkPendingTx(txMeta))) await Promise.all(signedTxList.map((txMeta) => this._checkPendingTx(txMeta)))
} catch (err) { } catch (err) {
console.error('PendingTransactionWatcher - Error updating pending transactions') log.error('PendingTransactionWatcher - Error updating pending transactions')
console.error(err) log.error(err)
} }
nonceGlobalLock.releaseLock() nonceGlobalLock.releaseLock()
} }
/**
checks to see if a confirmed txMeta has the same nonce
@param txMeta {Object} - txMeta object
@returns {boolean}
*/
async _checkIfNonceIsTaken (txMeta) { async _checkIfNonceIsTaken (txMeta) {
const address = txMeta.txParams.from const address = txMeta.txParams.from
const completed = this.getCompletedTransactions(address) const completed = this.getCompletedTransactions(address)
@ -185,5 +219,6 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
}) })
return sameNonce.length > 0 return sameNonce.length > 0
} }
} }
module.exports = PendingTransactionTracker

View File

@ -3,22 +3,27 @@ const {
hexToBn, hexToBn,
BnMultiplyByFraction, BnMultiplyByFraction,
bnToHex, bnToHex,
} = require('./util') } = require('../../lib/util')
const { addHexPrefix } = require('ethereumjs-util') const { addHexPrefix } = require('ethereumjs-util')
const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send. const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send.
/* /**
tx-utils are utility methods for Transaction manager tx-gas-utils are gas utility methods for Transaction manager
its passed ethquery its passed ethquery
and used to do things like calculate gas of a tx. and used to do things like calculate gas of a tx.
@param {Object} provider - A network provider.
*/ */
module.exports = class TxGasUtil { class TxGasUtil {
constructor (provider) { constructor (provider) {
this.query = new EthQuery(provider) this.query = new EthQuery(provider)
} }
/**
@param txMeta {Object} - the txMeta object
@returns {object} the txMeta object with the gas written to the txParams
*/
async analyzeGasUsage (txMeta) { async analyzeGasUsage (txMeta) {
const block = await this.query.getBlockByNumber('latest', true) const block = await this.query.getBlockByNumber('latest', true)
let estimatedGasHex let estimatedGasHex
@ -38,6 +43,12 @@ module.exports = class TxGasUtil {
return txMeta return txMeta
} }
/**
Estimates the tx's gas usage
@param txMeta {Object} - the txMeta object
@param blockGasLimitHex {string} - hex string of the block's gas limit
@returns {string} the estimated gas limit as a hex string
*/
async estimateTxGas (txMeta, blockGasLimitHex) { async estimateTxGas (txMeta, blockGasLimitHex) {
const txParams = txMeta.txParams const txParams = txMeta.txParams
@ -70,6 +81,12 @@ module.exports = class TxGasUtil {
return await this.query.estimateGas(txParams) return await this.query.estimateGas(txParams)
} }
/**
Writes the gas on the txParams in the txMeta
@param txMeta {Object} - the txMeta object to write to
@param blockGasLimitHex {string} - the block gas limit hex
@param estimatedGasHex {string} - the estimated gas hex
*/
setTxGas (txMeta, blockGasLimitHex, estimatedGasHex) { setTxGas (txMeta, blockGasLimitHex, estimatedGasHex) {
txMeta.estimatedGas = addHexPrefix(estimatedGasHex) txMeta.estimatedGas = addHexPrefix(estimatedGasHex)
const txParams = txMeta.txParams const txParams = txMeta.txParams
@ -87,6 +104,13 @@ module.exports = class TxGasUtil {
return return
} }
/**
Adds a gas buffer with out exceeding the block gas limit
@param initialGasLimitHex {string} - the initial gas limit to add the buffer too
@param blockGasLimitHex {string} - the block gas limit
@returns {string} the buffered gas limit as a hex string
*/
addGasBuffer (initialGasLimitHex, blockGasLimitHex) { addGasBuffer (initialGasLimitHex, blockGasLimitHex) {
const initialGasLimitBn = hexToBn(initialGasLimitHex) const initialGasLimitBn = hexToBn(initialGasLimitHex)
const blockGasLimitBn = hexToBn(blockGasLimitHex) const blockGasLimitBn = hexToBn(blockGasLimitHex)
@ -101,3 +125,5 @@ module.exports = class TxGasUtil {
return bnToHex(upperGasLimitBn) return bnToHex(upperGasLimitBn)
} }
} }
module.exports = TxGasUtil

View File

@ -1,22 +1,33 @@
const extend = require('xtend') const extend = require('xtend')
const EventEmitter = require('events') const EventEmitter = require('events')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const createId = require('./random-id')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const txStateHistoryHelper = require('./tx-state-history-helper') const txStateHistoryHelper = require('./lib/tx-state-history-helper')
const createId = require('../../lib/random-id')
// STATUS METHODS const { getFinalStates } = require('./lib/util')
// statuses: /**
// - `'unapproved'` the user has not responded TransactionStateManager is responsible for the state of a transaction and
// - `'rejected'` the user has responded no! storing the transaction
// - `'approved'` the user has approved the tx it also has some convenience methods for finding subsets of transactions
// - `'signed'` the tx is signed *
// - `'submitted'` the tx is sent to a server *STATUS METHODS
// - `'confirmed'` the tx has been included in a block. <br>statuses:
// - `'failed'` the tx failed for some reason, included on tx data. <br> - `'unapproved'` the user has not responded
// - `'dropped'` the tx nonce was already used <br> - `'rejected'` the user has responded no!
<br> - `'approved'` the user has approved the tx
module.exports = class TransactionStateManager extends EventEmitter { <br> - `'signed'` the tx is signed
<br> - `'submitted'` the tx is sent to a server
<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 {number} [opts.txHistoryLimit] limit for how many finished
transactions can hang around in state
@param {function} opts.getNetwork return network number
@class
*/
class TransactionStateManager extends EventEmitter {
constructor ({ initState, txHistoryLimit, getNetwork }) { constructor ({ initState, txHistoryLimit, getNetwork }) {
super() super()
@ -28,6 +39,10 @@ module.exports = class TransactionStateManager extends EventEmitter {
this.getNetwork = getNetwork this.getNetwork = getNetwork
} }
/**
@param opts {object} - the object to use when overwriting defaults
@returns {txMeta} the default txMeta object
*/
generateTxMeta (opts) { generateTxMeta (opts) {
return extend({ return extend({
id: createId(), id: createId(),
@ -38,17 +53,25 @@ module.exports = class TransactionStateManager extends EventEmitter {
}, opts) }, opts)
} }
/**
@returns {array} of txMetas that have been filtered for only the current network
*/
getTxList () { getTxList () {
const network = this.getNetwork() const network = this.getNetwork()
const fullTxList = this.getFullTxList() const fullTxList = this.getFullTxList()
return fullTxList.filter((txMeta) => txMeta.metamaskNetworkId === network) return fullTxList.filter((txMeta) => txMeta.metamaskNetworkId === network)
} }
/**
@returns {array} of all the txMetas in store
*/
getFullTxList () { getFullTxList () {
return this.store.getState().transactions return this.store.getState().transactions
} }
// Returns the tx list /**
@returns {array} the tx list whos status is unapproved
*/
getUnapprovedTxList () { getUnapprovedTxList () {
const txList = this.getTxsByMetaData('status', 'unapproved') const txList = this.getTxsByMetaData('status', 'unapproved')
return txList.reduce((result, tx) => { return txList.reduce((result, tx) => {
@ -57,18 +80,37 @@ module.exports = 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
*/
getPendingTransactions (address) { getPendingTransactions (address) {
const opts = { status: 'submitted' } const opts = { status: 'submitted' }
if (address) opts.from = address if (address) opts.from = address
return this.getFilteredTxList(opts) 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) { getConfirmedTransactions (address) {
const opts = { status: 'confirmed' } const opts = { status: 'confirmed' }
if (address) opts.from = address if (address) opts.from = address
return this.getFilteredTxList(opts) return this.getFilteredTxList(opts)
} }
/**
Adds the txMeta to the list of transactions in the store.
if the list is over txHistoryLimit it will remove a transaction that
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
*/
addTx (txMeta) { addTx (txMeta) {
this.once(`${txMeta.id}:signed`, function (txId) { this.once(`${txMeta.id}:signed`, function (txId) {
this.removeAllListeners(`${txMeta.id}:rejected`) this.removeAllListeners(`${txMeta.id}:rejected`)
@ -92,7 +134,9 @@ module.exports = class TransactionStateManager extends EventEmitter {
// or rejected tx's. // or rejected tx's.
// not tx's that are pending or unapproved // not tx's that are pending or unapproved
if (txCount > txHistoryLimit - 1) { if (txCount > txHistoryLimit - 1) {
let index = transactions.findIndex((metaTx) => metaTx.status === 'confirmed' || metaTx.status === 'rejected') const index = transactions.findIndex((metaTx) => {
return getFinalStates().includes(metaTx.status)
})
if (index !== -1) { if (index !== -1) {
transactions.splice(index, 1) transactions.splice(index, 1)
} }
@ -101,12 +145,21 @@ module.exports = class TransactionStateManager extends EventEmitter {
this._saveTxList(transactions) this._saveTxList(transactions)
return txMeta return txMeta
} }
// gets tx by Id and returns it /**
@param txId {number}
@returns {object} the txMeta who matches the given id if none found
for the network returns undefined
*/
getTx (txId) { getTx (txId) {
const txMeta = this.getTxsByMetaData('id', txId)[0] const txMeta = this.getTxsByMetaData('id', txId)[0]
return txMeta return txMeta
} }
/**
updates the txMeta in the list and adds a history entry
@param txMeta {Object} - the txMeta to update
@param [note] {string} - a not about the update for history
*/
updateTx (txMeta, note) { updateTx (txMeta, note) {
// validate txParams // validate txParams
if (txMeta.txParams) { if (txMeta.txParams) {
@ -134,16 +187,23 @@ module.exports = class TransactionStateManager extends EventEmitter {
} }
// merges txParams obj onto txData.txParams /**
// use extend to ensure that all fields are filled 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
*/
updateTxParams (txId, txParams) { updateTxParams (txId, txParams) {
const txMeta = this.getTx(txId) const txMeta = this.getTx(txId)
txMeta.txParams = extend(txMeta.txParams, txParams) txMeta.txParams = extend(txMeta.txParams, txParams)
this.updateTx(txMeta, `txStateManager#updateTxParams`) this.updateTx(txMeta, `txStateManager#updateTxParams`)
} }
// validates txParams members by type /**
validateTxParams(txParams) { validates txParams members by type
@param txParams {object} - txParams to validate
*/
validateTxParams (txParams) {
Object.keys(txParams).forEach((key) => { Object.keys(txParams).forEach((key) => {
const value = txParams[key] const value = txParams[key]
// validate types // validate types
@ -159,17 +219,19 @@ module.exports = class TransactionStateManager extends EventEmitter {
}) })
} }
/* /**
Takes an object of fields to search for eg: @param opts {object} - an object of fields to search for eg:<br>
let thingsToLookFor = { let <code>thingsToLookFor = {<br>
to: '0x0..', to: '0x0..',<br>
from: '0x0..', from: '0x0..',<br>
status: 'signed', status: 'signed',<br>
err: undefined, err: undefined,<br>
} }<br></code>
and returns a list of tx with all @param [initialList=this.getTxList()]
@returns a {array} of txMeta with all
options matching options matching
*/
/*
****************HINT**************** ****************HINT****************
| `err: undefined` is like looking | | `err: undefined` is like looking |
| for a tx with no err | | for a tx with no err |
@ -190,7 +252,14 @@ module.exports = class TransactionStateManager extends EventEmitter {
}) })
return filteredTxList return filteredTxList
} }
/**
@param key {string} - the key to check
@param value - the value your looking for
@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
*/
getTxsByMetaData (key, value, txList = this.getTxList()) { getTxsByMetaData (key, value, txList = this.getTxList()) {
return txList.filter((txMeta) => { return txList.filter((txMeta) => {
if (txMeta.txParams[key]) { if (txMeta.txParams[key]) {
@ -203,33 +272,51 @@ module.exports = class TransactionStateManager extends EventEmitter {
// get::set status // get::set status
// should return the status of the tx. /**
@param txId {number} - the txMeta Id
@return {string} the status of the tx.
*/
getTxStatus (txId) { getTxStatus (txId) {
const txMeta = this.getTx(txId) const txMeta = this.getTx(txId)
return txMeta.status return txMeta.status
} }
// should update the status of the tx to 'rejected'. /**
should update the status of the tx to 'rejected'.
@param txId {number} - the txMeta Id
*/
setTxStatusRejected (txId) { setTxStatusRejected (txId) {
this._setTxStatus(txId, 'rejected') this._setTxStatus(txId, 'rejected')
} }
// should update the status of the tx to 'unapproved'. /**
should update the status of the tx to 'unapproved'.
@param txId {number} - the txMeta Id
*/
setTxStatusUnapproved (txId) { setTxStatusUnapproved (txId) {
this._setTxStatus(txId, 'unapproved') this._setTxStatus(txId, 'unapproved')
} }
// should update the status of the tx to 'approved'. /**
should update the status of the tx to 'approved'.
@param txId {number} - the txMeta Id
*/
setTxStatusApproved (txId) { setTxStatusApproved (txId) {
this._setTxStatus(txId, 'approved') this._setTxStatus(txId, 'approved')
} }
// should update the status of the tx to 'signed'. /**
should update the status of the tx to 'signed'.
@param txId {number} - the txMeta Id
*/
setTxStatusSigned (txId) { setTxStatusSigned (txId) {
this._setTxStatus(txId, 'signed') this._setTxStatus(txId, 'signed')
} }
// should update the status of the tx to 'submitted'. /**
// and add a time stamp for when it was called 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
*/
setTxStatusSubmitted (txId) { setTxStatusSubmitted (txId) {
const txMeta = this.getTx(txId) const txMeta = this.getTx(txId)
txMeta.submittedTime = (new Date()).getTime() txMeta.submittedTime = (new Date()).getTime()
@ -237,17 +324,29 @@ module.exports = class TransactionStateManager extends EventEmitter {
this._setTxStatus(txId, 'submitted') this._setTxStatus(txId, 'submitted')
} }
// should update the status of the tx to 'confirmed'. /**
should update the status of the tx to 'confirmed'.
@param txId {number} - the txMeta Id
*/
setTxStatusConfirmed (txId) { setTxStatusConfirmed (txId) {
this._setTxStatus(txId, 'confirmed') this._setTxStatus(txId, 'confirmed')
} }
// should update the status dropped /**
should update the status of the tx to 'dropped'.
@param txId {number} - the txMeta Id
*/
setTxStatusDropped (txId) { setTxStatusDropped (txId) {
this._setTxStatus(txId, 'dropped') this._setTxStatus(txId, 'dropped')
} }
/**
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
*/
setTxStatusFailed (txId, err) { setTxStatusFailed (txId, err) {
const txMeta = this.getTx(txId) const txMeta = this.getTx(txId)
txMeta.err = { txMeta.err = {
@ -258,6 +357,11 @@ module.exports = class TransactionStateManager extends EventEmitter {
this._setTxStatus(txId, 'failed') 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
*/
wipeTransactions (address) { wipeTransactions (address) {
// network only tx // network only tx
const txs = this.getFullTxList() const txs = this.getFullTxList()
@ -273,9 +377,8 @@ module.exports = class TransactionStateManager extends EventEmitter {
// PRIVATE METHODS // PRIVATE METHODS
// //
// Should find the tx in the tx list and // STATUS METHODS
// update it. // statuses:
// should set the status in txData
// - `'unapproved'` the user has not responded // - `'unapproved'` the user has not responded
// - `'rejected'` the user has responded no! // - `'rejected'` the user has responded no!
// - `'approved'` the user has approved the tx // - `'approved'` the user has approved the tx
@ -283,6 +386,15 @@ module.exports = class TransactionStateManager extends EventEmitter {
// - `'submitted'` the tx is sent to a server // - `'submitted'` the tx is sent to a server
// - `'confirmed'` the tx has been included in a block. // - `'confirmed'` the tx has been included in a block.
// - `'failed'` the tx failed for some reason, included on tx data. // - `'failed'` the tx failed for some reason, included on tx data.
// - `'dropped'` the tx nonce was already used
/**
@param txId {number} - the txMeta Id
@param status {string} - 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
*/
_setTxStatus (txId, status) { _setTxStatus (txId, status) {
const txMeta = this.getTx(txId) const txMeta = this.getTx(txId)
txMeta.status = status txMeta.status = status
@ -295,9 +407,14 @@ module.exports = class TransactionStateManager extends EventEmitter {
this.emit('update:badge') this.emit('update:badge')
} }
// Saves the new/updated txList. /**
Saves the new/updated txList.
@param transactions {array} - the list of transactions to save
*/
// Function is intended only for internal use // Function is intended only for internal use
_saveTxList (transactions) { _saveTxList (transactions) {
this.store.updateState({ transactions }) this.store.updateState({ transactions })
} }
} }
module.exports = TransactionStateManager

View File

@ -7,7 +7,7 @@ This migration updates "transaction state history" to diffs style
*/ */
const clone = require('clone') const clone = require('clone')
const txStateHistoryHelper = require('../lib/tx-state-history-helper') const txStateHistoryHelper = require('../controllers/transactions/lib/tx-state-history-helper')
module.exports = { module.exports = {

BIN
docs/transaction-flow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

View File

@ -1,5 +1,5 @@
const assert = require('assert') const assert = require('assert')
const NonceTracker = require('../../app/scripts/lib/nonce-tracker') const NonceTracker = require('../../app/scripts/controllers/transactions/nonce-tracker')
const MockTxGen = require('../lib/mock-tx-gen') const MockTxGen = require('../lib/mock-tx-gen')
let providerResultStub = {} let providerResultStub = {}

View File

@ -4,7 +4,7 @@ const EthTx = require('ethereumjs-tx')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const clone = require('clone') const clone = require('clone')
const { createTestProviderTools } = require('../stub/provider') const { createTestProviderTools } = require('../stub/provider')
const PendingTransactionTracker = require('../../app/scripts/lib/pending-tx-tracker') const PendingTransactionTracker = require('../../app/scripts/controllers/transactions/pending-tx-tracker')
const MockTxGen = require('../lib/mock-tx-gen') const MockTxGen = require('../lib/mock-tx-gen')
const sinon = require('sinon') const sinon = require('sinon')
const noop = () => true const noop = () => true

View File

@ -5,7 +5,7 @@ const EthjsQuery = require('ethjs-query')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const sinon = require('sinon') const sinon = require('sinon')
const TransactionController = require('../../app/scripts/controllers/transactions') const TransactionController = require('../../app/scripts/controllers/transactions')
const TxGasUtils = require('../../app/scripts/lib/tx-gas-utils') const TxGasUtils = require('../../app/scripts/controllers/transactions/tx-gas-utils')
const { createTestProviderTools } = require('../stub/provider') const { createTestProviderTools } = require('../stub/provider')
const noop = () => true const noop = () => true
@ -188,7 +188,7 @@ describe('Transaction Controller', function () {
}) })
describe('#addTxDefaults', function () { describe('#addTxGasDefaults', 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) {
const txMeta = { const txMeta = {
'txParams': { 'txParams': {
@ -199,7 +199,7 @@ describe('Transaction Controller', function () {
providerResultStub.eth_gasPrice = '4a817c800' providerResultStub.eth_gasPrice = '4a817c800'
providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' } providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' }
providerResultStub.eth_estimateGas = '5209' providerResultStub.eth_estimateGas = '5209'
txController.addTxDefaults(txMeta) txController.addTxGasDefaults(txMeta)
.then((txMetaWithDefaults) => { .then((txMetaWithDefaults) => {
assert(txMetaWithDefaults.txParams.value, '0x0', 'should have added 0x0 as the value') assert(txMetaWithDefaults.txParams.value, '0x0', 'should have added 0x0 as the value')
assert(txMetaWithDefaults.txParams.gasPrice, 'should have added the gas price') assert(txMetaWithDefaults.txParams.gasPrice, 'should have added the gas price')
@ -210,99 +210,6 @@ describe('Transaction Controller', function () {
}) })
}) })
describe('#_validateTxParams', function () {
it('does not throw for positive values', function () {
var sample = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
value: '0x01',
}
txController._validateTxParams(sample)
})
it('returns error for negative values', function () {
var sample = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
value: '-0x01',
}
try {
txController._validateTxParams(sample)
} catch (err) {
assert.ok(err, 'error')
}
})
})
describe('#_normalizeTxParams', () => {
it('should normalize txParams', () => {
let txParams = {
chainId: '0x1',
from: 'a7df1beDBF813f57096dF77FCd515f0B3900e402',
to: null,
data: '68656c6c6f20776f726c64',
random: 'hello world',
}
let normalizedTxParams = txController._normalizeTxParams(txParams)
assert(!normalizedTxParams.chainId, 'their should be no chainId')
assert(!normalizedTxParams.to, 'their should be no to address if null')
assert.equal(normalizedTxParams.from.slice(0, 2), '0x', 'from should be hexPrefixd')
assert.equal(normalizedTxParams.data.slice(0, 2), '0x', 'data should be hexPrefixd')
assert(!('random' in normalizedTxParams), 'their should be no random key in normalizedTxParams')
txParams.to = 'a7df1beDBF813f57096dF77FCd515f0B3900e402'
normalizedTxParams = txController._normalizeTxParams(txParams)
assert.equal(normalizedTxParams.to.slice(0, 2), '0x', 'to should be hexPrefixd')
})
})
describe('#_validateRecipient', () => {
it('removes recipient for txParams with 0x when contract data is provided', function () {
const zeroRecipientandDataTxParams = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
to: '0x',
data: 'bytecode',
}
const sanitizedTxParams = txController._validateRecipient(zeroRecipientandDataTxParams)
assert.deepEqual(sanitizedTxParams, { from: '0x1678a085c290ebd122dc42cba69373b5953b831d', data: 'bytecode' }, 'no recipient with 0x')
})
it('should error when recipient is 0x', function () {
const zeroRecipientTxParams = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
to: '0x',
}
assert.throws(() => { txController._validateRecipient(zeroRecipientTxParams) }, Error, 'Invalid recipient address')
})
})
describe('#_validateFrom', () => {
it('should error when from is not a hex string', function () {
// where from is undefined
const txParams = {}
assert.throws(() => { txController._validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`)
// where from is array
txParams.from = []
assert.throws(() => { txController._validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`)
// where from is a object
txParams.from = {}
assert.throws(() => { txController._validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`)
// where from is a invalid address
txParams.from = 'im going to fail'
assert.throws(() => { txController._validateFrom(txParams) }, Error, `Invalid from address`)
// should run
txParams.from ='0x1678a085c290ebd122dc42cba69373b5953b831d'
txController._validateFrom(txParams)
})
})
describe('#addTx', function () { describe('#addTx', function () {
it('should emit updates', function (done) { it('should emit updates', function (done) {
const txMeta = { const txMeta = {

View File

@ -1,14 +1,77 @@
const assert = require('assert') const assert = require('assert')
const TxGasUtils = require('../../app/scripts/lib/tx-gas-utils') const Transaction = require('ethereumjs-tx')
const { createTestProviderTools } = require('../stub/provider') const BN = require('bn.js')
describe('Tx Gas Util', function () {
let txGasUtil, provider, providerResultStub const { hexToBn, bnToHex } = require('../../app/scripts/lib/util')
beforeEach(function () { const TxUtils = require('../../app/scripts/controllers/transactions/tx-gas-utils')
providerResultStub = {}
provider = createTestProviderTools({ scaffold: providerResultStub }).provider
txGasUtil = new TxGasUtils({ describe('txUtils', function () {
provider, let txUtils
before(function () {
txUtils = new TxUtils(new Proxy({}, {
get: (obj, name) => {
return () => {}
},
}))
})
describe('chain Id', function () {
it('prepares a transaction with the provided chainId', function () {
const txParams = {
to: '0x70ad465e0bab6504002ad58c744ed89c7da38524',
from: '0x69ad465e0bab6504002ad58c744ed89c7da38525',
value: '0x0',
gas: '0x7b0c',
gasPrice: '0x199c82cc00',
data: '0x',
nonce: '0x3',
chainId: 42,
}
const ethTx = new Transaction(txParams)
assert.equal(ethTx.getChainId(), 42, 'chainId is set from tx params')
})
})
describe('addGasBuffer', function () {
it('multiplies by 1.5, when within block gas limit', function () {
// naive estimatedGas: 0x16e360 (1.5 mil)
const inputHex = '0x16e360'
// dummy gas limit: 0x3d4c52 (4 mil)
const blockGasLimitHex = '0x3d4c52'
const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex)
const inputBn = hexToBn(inputHex)
const outputBn = hexToBn(output)
const expectedBn = inputBn.muln(1.5)
assert(outputBn.eq(expectedBn), 'returns 1.5 the input value')
})
it('uses original estimatedGas, when above block gas limit', function () {
// naive estimatedGas: 0x16e360 (1.5 mil)
const inputHex = '0x16e360'
// dummy gas limit: 0x0f4240 (1 mil)
const blockGasLimitHex = '0x0f4240'
const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex)
// const inputBn = hexToBn(inputHex)
const outputBn = hexToBn(output)
const expectedBn = hexToBn(inputHex)
assert(outputBn.eq(expectedBn), 'returns the original estimatedGas value')
})
it('buffers up to recommend gas limit recommended ceiling', function () {
// naive estimatedGas: 0x16e360 (1.5 mil)
const inputHex = '0x16e360'
// dummy gas limit: 0x1e8480 (2 mil)
const blockGasLimitHex = '0x1e8480'
const blockGasLimitBn = hexToBn(blockGasLimitHex)
const ceilGasLimitBn = blockGasLimitBn.muln(0.9)
const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex)
// const inputBn = hexToBn(inputHex)
// const outputBn = hexToBn(output)
const expectedHex = bnToHex(ceilGasLimitBn)
assert.equal(output, expectedHex, 'returns the gas limit recommended ceiling value')
}) })
}) })
}) })

View File

@ -1,6 +1,6 @@
const assert = require('assert') const assert = require('assert')
const clone = require('clone') const clone = require('clone')
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper') const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper')
describe('deepCloneFromTxMeta', function () { describe('deepCloneFromTxMeta', function () {
it('should clone deep', function () { it('should clone deep', function () {

View File

@ -1,5 +1,5 @@
const assert = require('assert') const assert = require('assert')
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper') const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper')
const testVault = require('../data/v17-long-history.json') const testVault = require('../data/v17-long-history.json')

View File

@ -1,8 +1,8 @@
const assert = require('assert') const assert = require('assert')
const clone = require('clone') const clone = require('clone')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const TxStateManager = require('../../app/scripts/lib/tx-state-manager') const TxStateManager = require('../../app/scripts/controllers/transactions/tx-state-manager')
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper') const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper')
const noop = () => true const noop = () => true
describe('TransactionStateManager', function () { describe('TransactionStateManager', function () {

View File

@ -1,77 +1,98 @@
const assert = require('assert') const assert = require('assert')
const Transaction = require('ethereumjs-tx') const txUtils = require('../../app/scripts/controllers/transactions/lib/util')
const BN = require('bn.js')
const { hexToBn, bnToHex } = require('../../app/scripts/lib/util')
const TxUtils = require('../../app/scripts/lib/tx-gas-utils')
describe('txUtils', function () { describe('txUtils', function () {
let txUtils describe('#validateTxParams', function () {
it('does not throw for positive values', function () {
before(function () { var sample = {
txUtils = new TxUtils(new Proxy({}, { from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
get: (obj, name) => { value: '0x01',
return () => {} }
}, txUtils.validateTxParams(sample)
})) })
})
it('returns error for negative values', function () {
describe('chain Id', function () { var sample = {
it('prepares a transaction with the provided chainId', function () { from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
const txParams = { value: '-0x01',
to: '0x70ad465e0bab6504002ad58c744ed89c7da38524', }
from: '0x69ad465e0bab6504002ad58c744ed89c7da38525', try {
value: '0x0', txUtils.validateTxParams(sample)
gas: '0x7b0c', } catch (err) {
gasPrice: '0x199c82cc00', assert.ok(err, 'error')
data: '0x',
nonce: '0x3',
chainId: 42,
} }
const ethTx = new Transaction(txParams)
assert.equal(ethTx.getChainId(), 42, 'chainId is set from tx params')
}) })
}) })
describe('addGasBuffer', function () { describe('#normalizeTxParams', () => {
it('multiplies by 1.5, when within block gas limit', function () { it('should normalize txParams', () => {
// naive estimatedGas: 0x16e360 (1.5 mil) let txParams = {
const inputHex = '0x16e360' chainId: '0x1',
// dummy gas limit: 0x3d4c52 (4 mil) from: 'a7df1beDBF813f57096dF77FCd515f0B3900e402',
const blockGasLimitHex = '0x3d4c52' to: null,
const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex) data: '68656c6c6f20776f726c64',
const inputBn = hexToBn(inputHex) random: 'hello world',
const outputBn = hexToBn(output) }
const expectedBn = inputBn.muln(1.5)
assert(outputBn.eq(expectedBn), 'returns 1.5 the input value') let normalizedTxParams = txUtils.normalizeTxParams(txParams)
assert(!normalizedTxParams.chainId, 'their should be no chainId')
assert(!normalizedTxParams.to, 'their should be no to address if null')
assert.equal(normalizedTxParams.from.slice(0, 2), '0x', 'from should be hexPrefixd')
assert.equal(normalizedTxParams.data.slice(0, 2), '0x', 'data should be hexPrefixd')
assert(!('random' in normalizedTxParams), 'their should be no random key in normalizedTxParams')
txParams.to = 'a7df1beDBF813f57096dF77FCd515f0B3900e402'
normalizedTxParams = txUtils.normalizeTxParams(txParams)
assert.equal(normalizedTxParams.to.slice(0, 2), '0x', 'to should be hexPrefixd')
})
})
describe('#validateRecipient', () => {
it('removes recipient for txParams with 0x when contract data is provided', function () {
const zeroRecipientandDataTxParams = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
to: '0x',
data: 'bytecode',
}
const sanitizedTxParams = txUtils.validateRecipient(zeroRecipientandDataTxParams)
assert.deepEqual(sanitizedTxParams, { from: '0x1678a085c290ebd122dc42cba69373b5953b831d', data: 'bytecode' }, 'no recipient with 0x')
}) })
it('uses original estimatedGas, when above block gas limit', function () { it('should error when recipient is 0x', function () {
// naive estimatedGas: 0x16e360 (1.5 mil) const zeroRecipientTxParams = {
const inputHex = '0x16e360' from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
// dummy gas limit: 0x0f4240 (1 mil) to: '0x',
const blockGasLimitHex = '0x0f4240' }
const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex) assert.throws(() => { txUtils.validateRecipient(zeroRecipientTxParams) }, Error, 'Invalid recipient address')
// const inputBn = hexToBn(inputHex)
const outputBn = hexToBn(output)
const expectedBn = hexToBn(inputHex)
assert(outputBn.eq(expectedBn), 'returns the original estimatedGas value')
}) })
})
it('buffers up to recommend gas limit recommended ceiling', function () {
// naive estimatedGas: 0x16e360 (1.5 mil) describe('#validateFrom', () => {
const inputHex = '0x16e360' it('should error when from is not a hex string', function () {
// dummy gas limit: 0x1e8480 (2 mil)
const blockGasLimitHex = '0x1e8480' // where from is undefined
const blockGasLimitBn = hexToBn(blockGasLimitHex) const txParams = {}
const ceilGasLimitBn = blockGasLimitBn.muln(0.9) assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`)
const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex)
// const inputBn = hexToBn(inputHex) // where from is array
// const outputBn = hexToBn(output) txParams.from = []
const expectedHex = bnToHex(ceilGasLimitBn) assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`)
assert.equal(output, expectedHex, 'returns the gas limit recommended ceiling value')
}) // where from is a object
txParams.from = {}
assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`)
// where from is a invalid address
txParams.from = 'im going to fail'
assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address`)
// should run
txParams.from ='0x1678a085c290ebd122dc42cba69373b5953b831d'
txUtils.validateFrom(txParams)
})
}) })
}) })