diff options
Diffstat (limited to 'app/scripts/controllers/transactions.js')
-rw-r--r-- | app/scripts/controllers/transactions.js | 351 |
1 files changed, 186 insertions, 165 deletions
diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 5f3d84ebe..308d43cb0 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -1,9 +1,9 @@ const EventEmitter = require('events') -const async = require('async') const extend = require('xtend') +const clone = require('clone') const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') -const pify = require('pify') +const EthQuery = require('ethjs-query') const TxProviderUtil = require('../lib/tx-utils') const createId = require('../lib/random-id') const NonceTracker = require('../lib/nonce-tracker') @@ -22,7 +22,6 @@ module.exports = class TransactionController extends EventEmitter { this.blockTracker = opts.blockTracker this.nonceTracker = new NonceTracker({ provider: this.provider, - blockTracker: this.provider._blockTracker, getPendingTransactions: (address) => { return this.getFilteredTxList({ from: address, @@ -31,7 +30,7 @@ module.exports = class TransactionController extends EventEmitter { }) }, }) - this.query = opts.ethQuery + this.query = new EthQuery(this.provider) this.txProviderUtils = new TxProviderUtil(this.query) this.blockTracker.on('rawBlock', this.checkForTxInBlock.bind(this)) // this is a little messy but until ethstore has been either @@ -60,13 +59,6 @@ module.exports = class TransactionController extends EventEmitter { return this.preferencesStore.getState().selectedAddress } - // Returns the tx list - getTxList () { - const network = this.getNetwork() - const fullTxList = this.getFullTxList() - return fullTxList.filter(txMeta => txMeta.metamaskNetworkId === network) - } - // Returns the number of txs for the current network. getTxCount () { return this.getTxList().length @@ -77,6 +69,56 @@ module.exports = class TransactionController extends EventEmitter { return this.store.getState().transactions } + getUnapprovedTxCount () { + return Object.keys(this.getUnapprovedTxList()).length + } + + getPendingTxCount () { + return this.getTxsByMetaData('status', 'signed').length + } + + // Returns the tx list + getTxList () { + const network = this.getNetwork() + const fullTxList = this.getFullTxList() + return this.getTxsByMetaData('metamaskNetworkId', network, fullTxList) + } + + // gets tx by Id and returns it + getTx (txId) { + const txList = this.getTxList() + const txMeta = txList.find(txData => txData.id === txId) + return txMeta + } + getUnapprovedTxList () { + const txList = this.getTxList() + return txList.filter((txMeta) => txMeta.status === 'unapproved') + .reduce((result, tx) => { + result[tx.id] = tx + return result + }, {}) + } + + updateTx (txMeta) { + // create txMeta snapshot for history + const txMetaForHistory = clone(txMeta) + // dont include previous history in this snapshot + delete txMetaForHistory.history + // add snapshot to tx history + if (!txMeta.history) txMeta.history = [] + txMeta.history.push(txMetaForHistory) + + const txId = txMeta.id + const txList = this.getFullTxList() + const index = txList.findIndex(txData => txData.id === txId) + if (!txMeta.history) txMeta.history = [] + txMeta.history.push(txMetaForHistory) + + txList[index] = txMeta + this._saveTxList(txList) + this.emit('update') + } + // Adds a tx to the txlist addTx (txMeta) { const txCount = this.getTxCount() @@ -90,7 +132,7 @@ module.exports = class TransactionController extends EventEmitter { // or rejected tx's. // not tx's that are pending or unapproved if (txCount > txHistoryLimit - 1) { - var index = fullTxList.findIndex((metaTx) => ((metaTx.status === 'confirmed' || metaTx.status === 'rejected') && network === txMeta.metamaskNetworkId)) + const index = fullTxList.findIndex((metaTx) => ((metaTx.status === 'confirmed' || metaTx.status === 'rejected') && network === txMeta.metamaskNetworkId)) fullTxList.splice(index, 1) } fullTxList.push(txMeta) @@ -108,79 +150,59 @@ module.exports = class TransactionController extends EventEmitter { this.emit(`${txMeta.id}:unapproved`, txMeta) } - // gets tx by Id and returns it - getTx (txId, cb) { - var txList = this.getTxList() - var txMeta = txList.find(txData => txData.id === txId) - return cb ? cb(txMeta) : txMeta - } - - // - updateTx (txMeta) { - var txId = txMeta.id - var txList = this.getFullTxList() - var index = txList.findIndex(txData => txData.id === txId) - txList[index] = txMeta - this._saveTxList(txList) - this.emit('update') - } - - get unapprovedTxCount () { - return Object.keys(this.getUnapprovedTxList()).length - } - - get pendingTxCount () { - return this.getTxsByMetaData('status', 'signed').length + async newUnapprovedTransaction (txParams) { + log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`) + const txMeta = await this.addUnapprovedTransaction(txParams) + this.emit('newUnaprovedTx', txMeta) + // listen for tx completion (success, fail) + return new Promise((resolve, reject) => { + this.once(`${txMeta.id}:finished`, (completedTx) => { + switch (completedTx.status) { + case 'submitted': + return resolve(completedTx.hash) + case 'rejected': + return reject(new Error('MetaMask Tx Signature: User denied transaction signature.')) + default: + return reject(new Error(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(completedTx.txParams)}`)) + } + }) + }) } - addUnapprovedTransaction (txParams, done) { - let txMeta - async.waterfall([ - // validate - (cb) => this.txProviderUtils.validateTxParams(txParams, cb), - // construct txMeta - (cb) => { - txMeta = { - id: createId(), - time: (new Date()).getTime(), - status: 'unapproved', - metamaskNetworkId: this.getNetwork(), - txParams: txParams, - } - cb() - }, - // add default tx params - (cb) => this.addTxDefaults(txMeta, cb), - // save txMeta - (cb) => { - this.addTx(txMeta) - cb(null, txMeta) - }, - ], done) + async addUnapprovedTransaction (txParams) { + // validate + await this.txProviderUtils.validateTxParams(txParams) + // construct txMeta + const txMeta = { + id: createId(), + time: (new Date()).getTime(), + status: 'unapproved', + metamaskNetworkId: this.getNetwork(), + txParams: txParams, + history: [], + } + // add default tx params + await this.addTxDefaults(txMeta) + // save txMeta + this.addTx(txMeta) + return txMeta } - addTxDefaults (txMeta, cb) { + async addTxDefaults (txMeta) { const txParams = txMeta.txParams // ensure value txParams.value = txParams.value || '0x0' if (!txParams.gasPrice) { - this.query.gasPrice((err, gasPrice) => { - if (err) return cb(err) - // set gasPrice - txParams.gasPrice = gasPrice - }) + const gasPrice = await this.query.gasPrice() + txParams.gasPrice = gasPrice } // set gasLimit - this.txProviderUtils.analyzeGasUsage(txMeta, cb) + return await this.txProviderUtils.analyzeGasUsage(txMeta) } - getUnapprovedTxList () { - var txList = this.getTxList() - return txList.filter((txMeta) => txMeta.status === 'unapproved') - .reduce((result, tx) => { - result[tx.id] = tx - return result - }, {}) + async updateAndApproveTransaction (txMeta) { + this.updateTx(txMeta) + await this.approveTransaction(txMeta.id) } async approveTransaction (txId) { @@ -191,8 +213,12 @@ module.exports = class TransactionController extends EventEmitter { // get next nonce const txMeta = this.getTx(txId) const fromAddress = txMeta.txParams.from + // wait for a nonce nonceLock = await this.nonceTracker.getNonceLock(fromAddress) + // add nonce to txParams txMeta.txParams.nonce = nonceLock.nextNonce + // add nonce debugging information to txMeta + txMeta.nonceDetails = nonceLock.nonceDetails this.updateTx(txMeta) // sign transaction const rawTx = await this.signTransaction(txId) @@ -201,6 +227,7 @@ module.exports = class TransactionController extends EventEmitter { nonceLock.releaseLock() } catch (err) { this.setTxStatusFailed(txId, { + stack: err.stack || err.message, errCode: err.errCode || err, message: err.message || 'Transaction failed during approval', }) @@ -211,26 +238,6 @@ module.exports = class TransactionController extends EventEmitter { } } - cancelTransaction (txId, cb = warn) { - this.setTxStatusRejected(txId) - cb() - } - - async updateAndApproveTransaction (txMeta) { - this.updateTx(txMeta) - await this.approveTransaction(txMeta.id) - } - - getChainId () { - const networkState = this.networkStore.getState() - const getChainId = parseInt(networkState) - if (Number.isNaN(getChainId)) { - return 0 - } else { - return getChainId - } - } - async signTransaction (txId) { const txMeta = this.getTx(txId) const txParams = txMeta.txParams @@ -238,10 +245,9 @@ module.exports = class TransactionController extends EventEmitter { // add network/chain id txParams.chainId = this.getChainId() const ethTx = this.txProviderUtils.buildEthTxFromParams(txParams) - const rawTx = await this.signEthTx(ethTx, fromAddress).then(() => { - this.setTxStatusSigned(txMeta.id) - return ethUtil.bufferToHex(ethTx.serialize()) - }) + await this.signEthTx(ethTx, fromAddress) + this.setTxStatusSigned(txMeta.id) + const rawTx = ethUtil.bufferToHex(ethTx.serialize()) return rawTx } @@ -249,10 +255,24 @@ module.exports = class TransactionController extends EventEmitter { const txMeta = this.getTx(txId) txMeta.rawTx = rawTx this.updateTx(txMeta) - await this.txProviderUtils.publishTransaction(rawTx).then((txHash) => { - this.setTxHash(txId, txHash) - this.setTxStatusSubmitted(txId) - }) + const txHash = await this.txProviderUtils.publishTransaction(rawTx) + this.setTxHash(txId, txHash) + this.setTxStatusSubmitted(txId) + } + + async cancelTransaction (txId) { + this.setTxStatusRejected(txId) + } + + + getChainId () { + const networkState = this.networkStore.getState() + const getChainId = parseInt(networkState) + if (Number.isNaN(getChainId)) { + return 0 + } else { + return getChainId + } } // receives a txHash records the tx as signed @@ -265,7 +285,7 @@ module.exports = class TransactionController extends EventEmitter { /* Takes an object of fields to search for eg: - var thingsToLookFor = { + let thingsToLookFor = { to: '0x0..', from: '0x0..', status: 'signed', @@ -288,7 +308,7 @@ module.exports = class TransactionController extends EventEmitter { and that have been 'confirmed' */ getFilteredTxList (opts) { - var filteredTxList + let filteredTxList Object.keys(opts).forEach((key) => { filteredTxList = this.getTxsByMetaData(key, opts[key], filteredTxList) }) @@ -349,7 +369,7 @@ module.exports = class TransactionController extends EventEmitter { // merges txParams obj onto txData.txParams // use extend to ensure that all fields are filled updateTxParams (txId, txParams) { - var txMeta = this.getTx(txId) + const txMeta = this.getTx(txId) txMeta.txParams = extend(txMeta.txParams, txParams) this.updateTx(txMeta) } @@ -357,20 +377,19 @@ module.exports = class TransactionController extends EventEmitter { // checks if a signed tx is in a block and // if included sets the tx status as 'confirmed' checkForTxInBlock (block) { - var signedTxList = this.getFilteredTxList({status: 'submitted'}) + const signedTxList = this.getFilteredTxList({status: 'submitted'}) if (!signedTxList.length) return signedTxList.forEach((txMeta) => { - var txHash = txMeta.hash - var txId = txMeta.id + const txHash = txMeta.hash + const txId = txMeta.id if (!txHash) { - const errReason = { - errCode: 'No hash was provided', - message: 'We had an error while submitting this transaction, please try again.', - } - return this.setTxStatusFailed(txId, errReason) + const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.') + noTxHashErr.name = 'NoTxHashError' + this.setTxStatusFailed(txId, noTxHashErr) } + block.transactions.forEach((tx) => { if (tx.hash === txHash) this.setTxStatusConfirmed(txId) }) @@ -388,7 +407,45 @@ module.exports = class TransactionController extends EventEmitter { if (diff > 1) this._checkPendingTxs() } - // PRIVATE METHODS + resubmitPendingTxs () { + const pending = this.getTxsByMetaData('status', 'submitted') + // only try resubmitting if their are transactions to resubmit + if (!pending.length) return + pending.forEach((txMeta) => this._resubmitTx(txMeta).catch((err) => { + /* + Dont marked as failed if the error is a "known" transaction warning + "there is already a transaction with the same sender-nonce + but higher/same gas price" + */ + const errorMessage = err.message.toLowerCase() + const isKnownTx = ( + // geth + errorMessage.includes('replacement transaction underpriced') + || errorMessage.includes('known transaction') + // parity + || errorMessage.includes('gas price too low to replace') + || errorMessage.includes('transaction with the same hash was already imported') + // other + || errorMessage.includes('gateway timeout') + || errorMessage.includes('nonce too low') + ) + // ignore resubmit warnings, return early + if (isKnownTx) return + // encountered real error - transition to error state + this.setTxStatusFailed(txMeta.id, { + stack: err.stack || err.message, + errCode: err.errCode || err, + message: err.message, + }) + })) + } + + +/* _____________________________________ +| | +| PRIVATE METHODS | +|______________________________________*/ + // Should find the tx in the tx list and // update it. @@ -401,7 +458,7 @@ module.exports = class TransactionController extends EventEmitter { // - `'confirmed'` the tx has been included in a block. // - `'failed'` the tx failed for some reason, included on tx data. _setTxStatus (txId, status) { - var txMeta = this.getTx(txId) + const txMeta = this.getTx(txId) txMeta.status = status this.emit(`${txMeta.id}:${status}`, txId) if (status === 'submitted' || status === 'rejected') { @@ -426,39 +483,7 @@ module.exports = class TransactionController extends EventEmitter { this.memStore.updateState({ unapprovedTxs, selectedAddressTxList }) } - resubmitPendingTxs () { - const pending = this.getTxsByMetaData('status', 'submitted') - // only try resubmitting if their are transactions to resubmit - if (!pending.length) return - pending.forEach((txMeta) => this._resubmitTx(txMeta).catch((err) => { - /* - Dont marked as failed if the error is a "known" transaction warning - "there is already a transaction with the same sender-nonce - but higher/same gas price" - */ - const errorMessage = err.message.toLowerCase() - const isKnownTx = ( - // geth - errorMessage.includes('replacement transaction underpriced') - || errorMessage.includes('known transaction') - // parity - || errorMessage.includes('gas price too low to replace') - || errorMessage.includes('transaction with the same hash was already imported') - // other - || errorMessage.includes('gateway timeout') - || errorMessage.includes('nonce too low') - ) - // ignore resubmit warnings, return early - if (isKnownTx) return - // encountered real error - transition to error state - this.setTxStatusFailed(txMeta.id, { - errCode: err.errCode || err, - message: err.message, - }) - })) - } - - async _resubmitTx (txMeta, cb) { + async _resubmitTx (txMeta) { const address = txMeta.txParams.from const balance = this.ethStore.getState().accounts[address].balance if (!('retryCount' in txMeta)) txMeta.retryCount = 0 @@ -466,18 +491,21 @@ module.exports = class TransactionController extends EventEmitter { // if the value of the transaction is greater then the balance, fail. if (!this.txProviderUtils.sufficientBalance(txMeta.txParams, balance)) { const message = 'Insufficient balance.' - this.setTxStatusFailed(txMeta.id, { message }) - cb() - return log.error(message) + this.setTxStatusFailed(txMeta.id, { + stack: '_resubmitTx: custom tx-controller error', + message, + }) + log.error(message) + return } // Only auto-submit already-signed txs: - if (!('rawTx' in txMeta)) return cb() + if (!('rawTx' in txMeta)) return // Increment a try counter. txMeta.retryCount++ const rawTx = txMeta.rawTx - return await this.txProviderUtils.publishTransaction(rawTx, cb) + return await this.txProviderUtils.publishTransaction(rawTx) } // checks the network for signed txs and @@ -501,17 +529,14 @@ module.exports = class TransactionController extends EventEmitter { // extra check in case there was an uncaught error during the // signature and submission process if (!txHash) { - const errReason = { - errCode: 'No hash was provided', - message: 'We had an error while submitting this transaction, please try again.', - } - this.setTxStatusFailed(txId, errReason) - return + const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.') + noTxHashErr.name = 'NoTxHashError' + this.setTxStatusFailed(txId, noTxHashErr) } // get latest transaction status let txParams try { - txParams = await pify((cb) => this.query.getTransactionByHash(txHash, cb))() + txParams = await this.query.getTransactionByHash(txHash) if (!txParams) return if (txParams.blockNumber) { this.setTxStatusConfirmed(txId) @@ -524,12 +549,8 @@ module.exports = class TransactionController extends EventEmitter { message: 'There was a problem loading this transaction.', } this.updateTx(txMeta) - log.error(err) + throw err } } } - -} - - -const warn = () => log.warn('warn was used no cb provided') +}
\ No newline at end of file |