diff --git a/app/scripts/controllers/transactions/README.md b/app/scripts/controllers/transactions/README.md new file mode 100644 index 000000000..b414762dc --- /dev/null +++ b/app/scripts/controllers/transactions/README.md @@ -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 +} +``` diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions/index.js similarity index 66% rename from app/scripts/controllers/transactions.js rename to app/scripts/controllers/transactions/index.js index c8211ebd7..541f1db73 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions/index.js @@ -3,28 +3,42 @@ const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') const Transaction = require('ethereumjs-tx') const EthQuery = require('ethjs-query') -const TransactionStateManager = require('../lib/tx-state-manager') -const TxGasUtil = require('../lib/tx-gas-utils') -const PendingTransactionTracker = require('../lib/pending-tx-tracker') -const NonceTracker = require('../lib/nonce-tracker') +const TransactionStateManager = require('./tx-state-manager') +const TxGasUtil = require('./tx-gas-utils') +const PendingTransactionTracker = require('./pending-tx-tracker') +const NonceTracker = require('./nonce-tracker') +const txUtils = require('./lib/util') const log = require('loglevel') -/* +/** Transaction Controller is an aggregate of sub-controllers and trackers composing them in a way to be exposed to the metamask controller - - txStateManager +
- txStateManager responsible for the state of a transaction and storing the transaction - - pendingTxTracker +
- pendingTxTracker watching blocks for transactions to be include and emitting confirmed events - - txGasUtil +
- txGasUtil gas calculations and safety buffering - - nonceTracker +
- nonceTracker 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) { super() this.networkStore = opts.networkStore || new ObservableStore({}) @@ -38,45 +52,19 @@ module.exports = class TransactionController extends EventEmitter { this.query = new EthQuery(this.provider) this.txGasUtil = new TxGasUtil(this.provider) + this._mapMethods() this.txStateManager = new TransactionStateManager({ initState: opts.initState, txHistoryLimit: opts.txHistoryLimit, getNetwork: this.getNetwork.bind(this), }) - - 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._onBootCleanUp() this.store = this.txStateManager.store - this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update')) this.nonceTracker = new NonceTracker({ provider: this.provider, getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager), - getConfirmedTransactions: (address) => { - return this.txStateManager.getFilteredTxList({ - from: address, - status: 'confirmed', - err: undefined, - }) - }, + getConfirmedTransactions: this.txStateManager.getConfirmedTransactions.bind(this.txStateManager), }) this.pendingTxTracker = new PendingTransactionTracker({ @@ -88,60 +76,14 @@ module.exports = class TransactionController extends EventEmitter { }) this.txStateManager.store.subscribe(() => this.emit('update:badge')) - - 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)) + this._setupListners() // memstore is computed from a few different stores this._updateMemstore() this.txStateManager.store.subscribe(() => this._updateMemstore()) this.networkStore.subscribe(() => this._updateMemstore()) this.preferencesStore.subscribe(() => this._updateMemstore()) } - - 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) - } - + /** @returns {number} the chainId*/ getChainId () { const networkState = this.networkStore.getState() 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 +/** + Adds a tx to the txlist + @emits ${txMeta.id}:unapproved +*/ addTx (txMeta) { this.txStateManager.addTx(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} 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 = {}) { log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(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) { // validate - const normalizedTxParams = this._normalizeTxParams(txParams) - this._validateTxParams(normalizedTxParams) + const normalizedTxParams = txUtils.normalizeTxParams(txParams) + txUtils.validateTxParams(normalizedTxParams) // construct txMeta let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams }) this.addTx(txMeta) this.emit('newUnapprovedTx', txMeta) // add default tx params try { - txMeta = await this.addTxDefaults(txMeta) + txMeta = await this.addTxGasDefaults(txMeta) } catch (error) { console.log(error) this.txStateManager.setTxStatusFailed(txMeta.id, error) @@ -206,21 +169,33 @@ module.exports = class TransactionController extends EventEmitter { return txMeta } - - async addTxDefaults (txMeta) { +/** + adds the tx gas defaults: gas && gasPrice + @param txMeta {Object} - the txMeta object + @returns {Promise} resolves with txMeta +*/ + async addTxGasDefaults (txMeta) { const txParams = txMeta.txParams // ensure value + txParams.value = txParams.value ? ethUtil.addHexPrefix(txParams.value) : '0x0' txMeta.gasPriceSpecified = Boolean(txParams.gasPrice) let gasPrice = txParams.gasPrice if (!gasPrice) { gasPrice = this.getGasPrice ? this.getGasPrice() : await this.query.gasPrice() } txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16)) - txParams.value = txParams.value || '0x0' // set gasLimit 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) { const originalTxMeta = this.txStateManager.getTx(originalTxId) const lastGasPrice = originalTxMeta.txParams.gasPrice @@ -234,15 +209,31 @@ module.exports = class TransactionController extends EventEmitter { return txMeta } + /** + updates the txMeta in the txStateManager + @param txMeta {Object} - the updated txMeta + */ async updateTransaction (txMeta) { this.txStateManager.updateTx(txMeta, 'confTx: user updated transaction') } + /** + updates and approves the transaction + @param txMeta {Object} + */ async updateAndApproveTransaction (txMeta) { this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction') 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) { let nonceLock try { @@ -274,7 +265,11 @@ module.exports = class TransactionController extends EventEmitter { 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) { const txMeta = this.txStateManager.getTx(txId) // add network/chain id @@ -290,6 +285,12 @@ module.exports = class TransactionController extends EventEmitter { 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} + */ async publishTransaction (txId, rawTx) { const txMeta = this.txStateManager.getTx(txId) txMeta.rawTx = rawTx @@ -299,11 +300,20 @@ module.exports = class TransactionController extends EventEmitter { this.txStateManager.setTxStatusSubmitted(txId) } + /** + Convenience method for the ui thats sets the transaction to rejected + @param txId {number} - the tx's Id + @returns {Promise} + */ async cancelTransaction (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) { // Add the tx hash to the persisted meta-tx object const txMeta = this.txStateManager.getTx(txId) @@ -314,63 +324,92 @@ module.exports = class TransactionController extends EventEmitter { // // 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 - const whiteList = { - from: from => ethUtil.addHexPrefix(from).toLowerCase(), - to: to => ethUtil.addHexPrefix(txParams.to).toLowerCase(), - nonce: nonce => ethUtil.addHexPrefix(nonce), - value: value => ethUtil.addHexPrefix(value), - data: data => ethUtil.addHexPrefix(data), - gas: gas => ethUtil.addHexPrefix(gas), - gasPrice: gasPrice => ethUtil.addHexPrefix(gasPrice), - } + /** + If transaction controller was rebooted with transactions that are uncompleted + in steps of the transaction signing or user confirmation process it will either + transition txMetas to a failed state or try to redo those tasks. + */ - // apply only keys in the whiteList - const normalizedTxParams = {} - Object.keys(whiteList).forEach((key) => { - if (txParams[key]) normalizedTxParams[key] = whiteList[key](txParams[key]) + _onBootCleanUp () { + this.txStateManager.getFilteredTxList({ + status: 'unapproved', + 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) - this._validateRecipient(txParams) - if ('value' in txParams) { - const value = txParams.value.toString() - if (value.includes('-')) { - throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`) + /** + is called in constructor applies the listeners for pendingTxTracker txStateManager + and blockTracker + */ + _setupListners () { + this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update')) + 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') + }) - if (value.includes('.')) { - throw new Error(`Invalid transaction value of ${txParams.value} number must be in wei`) - } - } - } + 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)) - _validateFrom (txParams) { - if ( !(typeof txParams.from === 'string') ) throw new Error(`Invalid from address ${txParams.from} not a string`) - if (!ethUtil.isValidAddress(txParams.from)) throw new Error('Invalid from address') } - _validateRecipient (txParams) { - if (txParams.to === '0x' || txParams.to === null ) { - if (txParams.data) { - delete txParams.to - } else { - throw new Error('Invalid recipient address') - } - } else if ( txParams.to !== undefined && !ethUtil.isValidAddress(txParams.to) ) { - throw new Error('Invalid recipient address') - } - return txParams - } + /** + Sets other txMeta statuses to dropped if the txMeta that has been confirmed has other transactions + in the list have the same nonce + @param txId {Number} - the txId of the transaction that has been confirmed in a block + */ _markNonceDuplicatesDropped (txId) { - this.txStateManager.setTxStatusConfirmed(txId) // get the confirmed transactions nonce and from address const txMeta = this.txStateManager.getTx(txId) const { nonce, from } = txMeta.txParams @@ -385,6 +424,9 @@ module.exports = class TransactionController extends EventEmitter { }) } + /** + Updates the memStore in transaction controller + */ _updateMemstore () { const unapprovedTxs = this.txStateManager.getUnapprovedTxList() const selectedAddressTxList = this.txStateManager.getFilteredTxList({ @@ -394,3 +436,5 @@ module.exports = class TransactionController extends EventEmitter { this.memStore.updateState({ unapprovedTxs, selectedAddressTxList }) } } + +module.exports = TransactionController diff --git a/app/scripts/lib/tx-state-history-helper.js b/app/scripts/controllers/transactions/lib/tx-state-history-helper.js similarity index 60% rename from app/scripts/lib/tx-state-history-helper.js rename to app/scripts/controllers/transactions/lib/tx-state-history-helper.js index 94c7b6792..59a4b562c 100644 --- a/app/scripts/lib/tx-state-history-helper.js +++ b/app/scripts/controllers/transactions/lib/tx-state-history-helper.js @@ -1,6 +1,6 @@ const jsonDiffer = require('fast-json-patch') const clone = require('clone') - +/** @module*/ module.exports = { generateHistoryEntry, replayHistory, @@ -8,7 +8,11 @@ module.exports = { migrateFromSnapshotsToDiffs, } - +/** + converts non-initial history entries into diffs + @param longHistory {array} + @returns {array} +*/ function migrateFromSnapshotsToDiffs (longHistory) { return ( 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) { const entry = jsonDiffer.compare(previousState, newState) // 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 } +/** + Recovers previous txMeta state obj + @return {object} +*/ function replayHistory (_shortHistory) { const shortHistory = clone(_shortHistory) 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) { // create txMeta snapshot for history const snapshot = clone(txMeta) diff --git a/app/scripts/controllers/transactions/lib/util.js b/app/scripts/controllers/transactions/lib/util.js new file mode 100644 index 000000000..84f7592a0 --- /dev/null +++ b/app/scripts/controllers/transactions/lib/util.js @@ -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 + ] +} + diff --git a/app/scripts/lib/nonce-tracker.js b/app/scripts/controllers/transactions/nonce-tracker.js similarity index 74% rename from app/scripts/lib/nonce-tracker.js rename to app/scripts/controllers/transactions/nonce-tracker.js index 5b1cd7f43..f8cdc5523 100644 --- a/app/scripts/lib/nonce-tracker.js +++ b/app/scripts/controllers/transactions/nonce-tracker.js @@ -1,7 +1,15 @@ 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, getPendingTransactions, getConfirmedTransactions }) { @@ -12,6 +20,9 @@ class NonceTracker { this.lockMap = {} } + /** + @returns {Promise} with the key releaseLock (the gloabl mutex) + */ async getGlobalLock () { const globalMutex = this._lookupMutex('global') // await global mutex free @@ -19,8 +30,20 @@ class NonceTracker { 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} + */ async getNonceLock (address) { // await global mutex free await this._globalMutexFree() @@ -123,6 +146,17 @@ class NonceTracker { 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 @@ -140,6 +174,10 @@ class NonceTracker { // this is a hotfix for the fact that the blockTracker will // change when the network changes + + /** + @returns {Object} the current blockTracker + */ _getBlockTracker () { return this.provider._blockTracker } diff --git a/app/scripts/lib/pending-tx-tracker.js b/app/scripts/controllers/transactions/pending-tx-tracker.js similarity index 74% rename from app/scripts/lib/pending-tx-tracker.js rename to app/scripts/controllers/transactions/pending-tx-tracker.js index e8869e6b8..6e2fcb40b 100644 --- a/app/scripts/lib/pending-tx-tracker.js +++ b/app/scripts/controllers/transactions/pending-tx-tracker.js @@ -1,23 +1,24 @@ const EventEmitter = require('events') +const log = require('loglevel') 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
+ go from a pending state to a confirmed (mined in a block) state
+
As well as continues broadcast while in the pending state +
+@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) { super() this.query = new EthQuery(config.provider) @@ -29,8 +30,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter { 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) { const signedTxList = this.getPendingTransactions() 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 }) { // check pending transactions on start if (!oldBlock) { @@ -63,7 +74,11 @@ module.exports = class PendingTransactionTracker extends EventEmitter { 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) { const pending = this.getPendingTransactions() // 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) { if (!txMeta.firstRetryBlockNumber) { this.emit('tx:block-update', txMeta, latestBlockNumber) @@ -123,7 +145,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter { this.emit('tx:retry', txMeta) 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) { const txHash = txMeta.hash 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 () { const signedTxList = this.getPendingTransactions() // 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 { await Promise.all(signedTxList.map((txMeta) => this._checkPendingTx(txMeta))) } catch (err) { - console.error('PendingTransactionWatcher - Error updating pending transactions') - console.error(err) + log.error('PendingTransactionWatcher - Error updating pending transactions') + log.error(err) } nonceGlobalLock.releaseLock() } + /** + checks to see if a confirmed txMeta has the same nonce + @param txMeta {Object} - txMeta object + @returns {boolean} + */ async _checkIfNonceIsTaken (txMeta) { const address = txMeta.txParams.from const completed = this.getCompletedTransactions(address) @@ -185,5 +219,6 @@ module.exports = class PendingTransactionTracker extends EventEmitter { }) return sameNonce.length > 0 } - } + +module.exports = PendingTransactionTracker diff --git a/app/scripts/lib/tx-gas-utils.js b/app/scripts/controllers/transactions/tx-gas-utils.js similarity index 74% rename from app/scripts/lib/tx-gas-utils.js rename to app/scripts/controllers/transactions/tx-gas-utils.js index c579e462a..36b5cdbc9 100644 --- a/app/scripts/lib/tx-gas-utils.js +++ b/app/scripts/controllers/transactions/tx-gas-utils.js @@ -3,22 +3,27 @@ const { hexToBn, BnMultiplyByFraction, bnToHex, -} = require('./util') +} = require('../../lib/util') const { addHexPrefix } = require('ethereumjs-util') const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send. -/* -tx-utils are utility methods for Transaction manager +/** +tx-gas-utils are gas utility methods for Transaction manager its passed ethquery 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) { 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) { const block = await this.query.getBlockByNumber('latest', true) let estimatedGasHex @@ -38,6 +43,12 @@ module.exports = class TxGasUtil { 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) { const txParams = txMeta.txParams @@ -70,6 +81,12 @@ module.exports = class TxGasUtil { 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) { txMeta.estimatedGas = addHexPrefix(estimatedGasHex) const txParams = txMeta.txParams @@ -87,6 +104,13 @@ module.exports = class TxGasUtil { 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) { const initialGasLimitBn = hexToBn(initialGasLimitHex) const blockGasLimitBn = hexToBn(blockGasLimitHex) @@ -100,4 +124,6 @@ module.exports = class TxGasUtil { // otherwise use blockGasLimit return bnToHex(upperGasLimitBn) } -} \ No newline at end of file +} + +module.exports = TxGasUtil \ No newline at end of file diff --git a/app/scripts/lib/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js similarity index 57% rename from app/scripts/lib/tx-state-manager.js rename to app/scripts/controllers/transactions/tx-state-manager.js index c6d10ee62..53428c333 100644 --- a/app/scripts/lib/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -1,22 +1,33 @@ const extend = require('xtend') const EventEmitter = require('events') const ObservableStore = require('obs-store') -const createId = require('./random-id') const ethUtil = require('ethereumjs-util') -const txStateHistoryHelper = require('./tx-state-history-helper') - -// STATUS METHODS - // statuses: - // - `'unapproved'` the user has not responded - // - `'rejected'` the user has responded no! - // - `'approved'` the user has approved the tx - // - `'signed'` the tx is signed - // - `'submitted'` the tx is sent to a server - // - `'confirmed'` the tx has been included in a block. - // - `'failed'` the tx failed for some reason, included on tx data. - // - `'dropped'` the tx nonce was already used - -module.exports = class TransactionStateManager extends EventEmitter { +const txStateHistoryHelper = require('./lib/tx-state-history-helper') +const createId = require('../../lib/random-id') +const { getFinalStates } = require('./lib/util') +/** + TransactionStateManager is responsible for the state of a transaction and + storing the transaction + it also has some convenience methods for finding subsets of transactions + * + *STATUS METHODS +
statuses: +
- `'unapproved'` the user has not responded +
- `'rejected'` the user has responded no! +
- `'approved'` the user has approved the tx +
- `'signed'` the tx is signed +
- `'submitted'` the tx is sent to a server +
- `'confirmed'` the tx has been included in a block. +
- `'failed'` the tx failed for some reason, included on tx data. +
- `'dropped'` the tx nonce was already used + @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 }) { super() @@ -28,6 +39,10 @@ module.exports = class TransactionStateManager extends EventEmitter { this.getNetwork = getNetwork } + /** + @param opts {object} - the object to use when overwriting defaults + @returns {txMeta} the default txMeta object + */ generateTxMeta (opts) { return extend({ id: createId(), @@ -38,17 +53,25 @@ module.exports = class TransactionStateManager extends EventEmitter { }, opts) } + /** + @returns {array} of txMetas that have been filtered for only the current network + */ getTxList () { const network = this.getNetwork() const fullTxList = this.getFullTxList() return fullTxList.filter((txMeta) => txMeta.metamaskNetworkId === network) } + /** + @returns {array} of all the txMetas in store + */ getFullTxList () { return this.store.getState().transactions } - // Returns the tx list + /** + @returns {array} the tx list whos status is unapproved + */ getUnapprovedTxList () { const txList = this.getTxsByMetaData('status', 'unapproved') 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) { 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 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) { this.once(`${txMeta.id}:signed`, function (txId) { this.removeAllListeners(`${txMeta.id}:rejected`) @@ -92,7 +134,9 @@ module.exports = class TransactionStateManager extends EventEmitter { // or rejected tx's. // not tx's that are pending or unapproved 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) { transactions.splice(index, 1) } @@ -101,12 +145,21 @@ module.exports = class TransactionStateManager extends EventEmitter { this._saveTxList(transactions) 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) { const txMeta = this.getTxsByMetaData('id', txId)[0] 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) { // validate 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) { const txMeta = this.getTx(txId) txMeta.txParams = extend(txMeta.txParams, txParams) 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) => { const value = txParams[key] // validate types @@ -159,17 +219,19 @@ module.exports = class TransactionStateManager extends EventEmitter { }) } -/* - Takes an object of fields to search for eg: - let thingsToLookFor = { - to: '0x0..', - from: '0x0..', - status: 'signed', - err: undefined, - } - and returns a list of tx with all +/** + @param opts {object} - an object of fields to search for eg:
+ let thingsToLookFor = {
+ to: '0x0..',
+ from: '0x0..',
+ status: 'signed',
+ err: undefined,
+ }
+ @param [initialList=this.getTxList()] + @returns a {array} of txMeta with all options matching - + */ + /* ****************HINT**************** | `err: undefined` is like looking | | for a tx with no err | @@ -190,7 +252,14 @@ module.exports = class TransactionStateManager extends EventEmitter { }) 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()) { return txList.filter((txMeta) => { if (txMeta.txParams[key]) { @@ -203,33 +272,51 @@ module.exports = class TransactionStateManager extends EventEmitter { // 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) { const txMeta = this.getTx(txId) 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) { 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) { 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) { 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) { 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) { const txMeta = this.getTx(txId) txMeta.submittedTime = (new Date()).getTime() @@ -237,17 +324,29 @@ module.exports = class TransactionStateManager extends EventEmitter { 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) { 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) { 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) { const txMeta = this.getTx(txId) txMeta.err = { @@ -258,6 +357,11 @@ module.exports = class TransactionStateManager extends EventEmitter { 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) { // network only tx const txs = this.getFullTxList() @@ -273,9 +377,8 @@ module.exports = class TransactionStateManager extends EventEmitter { // PRIVATE METHODS // - // Should find the tx in the tx list and - // update it. - // should set the status in txData + // STATUS METHODS + // statuses: // - `'unapproved'` the user has not responded // - `'rejected'` the user has responded no! // - `'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 // - `'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 + + /** + @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) { const txMeta = this.getTx(txId) txMeta.status = status @@ -295,9 +407,14 @@ module.exports = class TransactionStateManager extends EventEmitter { 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 _saveTxList (transactions) { this.store.updateState({ transactions }) } } + +module.exports = TransactionStateManager diff --git a/app/scripts/migrations/018.js b/app/scripts/migrations/018.js index bea1fe3da..ffbf24a4b 100644 --- a/app/scripts/migrations/018.js +++ b/app/scripts/migrations/018.js @@ -7,7 +7,7 @@ This migration updates "transaction state history" to diffs style */ const clone = require('clone') -const txStateHistoryHelper = require('../lib/tx-state-history-helper') +const txStateHistoryHelper = require('../controllers/transactions/lib/tx-state-history-helper') module.exports = { diff --git a/docs/transaction-flow.png b/docs/transaction-flow.png new file mode 100644 index 000000000..1059b60d8 Binary files /dev/null and b/docs/transaction-flow.png differ diff --git a/test/unit/nonce-tracker-test.js b/test/unit/nonce-tracker-test.js index 5a27882ef..cf26945d3 100644 --- a/test/unit/nonce-tracker-test.js +++ b/test/unit/nonce-tracker-test.js @@ -1,5 +1,5 @@ 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') let providerResultStub = {} diff --git a/test/unit/pending-tx-test.js b/test/unit/pending-tx-test.js index f0b4e3bfc..001b86dd1 100644 --- a/test/unit/pending-tx-test.js +++ b/test/unit/pending-tx-test.js @@ -4,7 +4,7 @@ const EthTx = require('ethereumjs-tx') const ObservableStore = require('obs-store') const clone = require('clone') 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 sinon = require('sinon') const noop = () => true diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index 824574ff2..20d6f8573 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -5,7 +5,7 @@ const EthjsQuery = require('ethjs-query') const ObservableStore = require('obs-store') const sinon = require('sinon') 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 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) { const txMeta = { 'txParams': { @@ -199,7 +199,7 @@ describe('Transaction Controller', function () { providerResultStub.eth_gasPrice = '4a817c800' providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' } providerResultStub.eth_estimateGas = '5209' - txController.addTxDefaults(txMeta) + txController.addTxGasDefaults(txMeta) .then((txMetaWithDefaults) => { assert(txMetaWithDefaults.txParams.value, '0x0', 'should have added 0x0 as the value') 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 () { it('should emit updates', function (done) { const txMeta = { diff --git a/test/unit/tx-gas-util-test.js b/test/unit/tx-gas-util-test.js index 40ea8a7d6..c1d5966da 100644 --- a/test/unit/tx-gas-util-test.js +++ b/test/unit/tx-gas-util-test.js @@ -1,14 +1,77 @@ const assert = require('assert') -const TxGasUtils = require('../../app/scripts/lib/tx-gas-utils') -const { createTestProviderTools } = require('../stub/provider') - -describe('Tx Gas Util', function () { - let txGasUtil, provider, providerResultStub - beforeEach(function () { - providerResultStub = {} - provider = createTestProviderTools({ scaffold: providerResultStub }).provider - txGasUtil = new TxGasUtils({ - provider, +const Transaction = require('ethereumjs-tx') +const BN = require('bn.js') + + +const { hexToBn, bnToHex } = require('../../app/scripts/lib/util') +const TxUtils = require('../../app/scripts/controllers/transactions/tx-gas-utils') + + +describe('txUtils', function () { + 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') }) }) }) diff --git a/test/unit/tx-state-history-helper-test.js b/test/unit/tx-state-history-helper-test.js index 90cb10713..35e9ef188 100644 --- a/test/unit/tx-state-history-helper-test.js +++ b/test/unit/tx-state-history-helper-test.js @@ -1,6 +1,6 @@ const assert = require('assert') 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 () { it('should clone deep', function () { diff --git a/test/unit/tx-state-history-helper.js b/test/unit/tx-state-history-helper.js index 79ee26d6e..35f7dac57 100644 --- a/test/unit/tx-state-history-helper.js +++ b/test/unit/tx-state-history-helper.js @@ -1,5 +1,5 @@ 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') diff --git a/test/unit/tx-state-manager-test.js b/test/unit/tx-state-manager-test.js index a5ac13664..e5fe68d0b 100644 --- a/test/unit/tx-state-manager-test.js +++ b/test/unit/tx-state-manager-test.js @@ -1,8 +1,8 @@ const assert = require('assert') const clone = require('clone') const ObservableStore = require('obs-store') -const TxStateManager = require('../../app/scripts/lib/tx-state-manager') -const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper') +const TxStateManager = require('../../app/scripts/controllers/transactions/tx-state-manager') +const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper') const noop = () => true describe('TransactionStateManager', function () { diff --git a/test/unit/tx-utils-test.js b/test/unit/tx-utils-test.js index 8ca13412e..ae7afd285 100644 --- a/test/unit/tx-utils-test.js +++ b/test/unit/tx-utils-test.js @@ -1,77 +1,98 @@ const assert = require('assert') -const Transaction = require('ethereumjs-tx') -const BN = require('bn.js') - - -const { hexToBn, bnToHex } = require('../../app/scripts/lib/util') -const TxUtils = require('../../app/scripts/lib/tx-gas-utils') +const txUtils = require('../../app/scripts/controllers/transactions/lib/util') describe('txUtils', function () { - let txUtils - - before(function () { - txUtils = new TxUtils(new Proxy({}, { - get: (obj, name) => { - return () => {} - }, - })) - }) + describe('#validateTxParams', function () { + it('does not throw for positive values', function () { + var sample = { + from: '0x1678a085c290ebd122dc42cba69373b5953b831d', + value: '0x01', + } + txUtils.validateTxParams(sample) + }) - 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, + it('returns error for negative values', function () { + var sample = { + from: '0x1678a085c290ebd122dc42cba69373b5953b831d', + value: '-0x01', + } + try { + txUtils.validateTxParams(sample) + } catch (err) { + assert.ok(err, 'error') } - 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') + describe('#normalizeTxParams', () => { + it('should normalize txParams', () => { + let txParams = { + chainId: '0x1', + from: 'a7df1beDBF813f57096dF77FCd515f0B3900e402', + to: null, + data: '68656c6c6f20776f726c64', + random: 'hello world', + } + + 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') + }) + }) - 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') + 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('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') + it('should error when recipient is 0x', function () { + const zeroRecipientTxParams = { + from: '0x1678a085c290ebd122dc42cba69373b5953b831d', + to: '0x', + } + assert.throws(() => { txUtils.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(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`) + + // where from is array + txParams.from = [] + assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`) + + // 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) + }) + }) }) \ No newline at end of file