diff options
Diffstat (limited to 'app/scripts')
-rw-r--r-- | app/scripts/controllers/transactions.js | 97 | ||||
-rw-r--r-- | app/scripts/lib/nonce-tracker.js | 59 | ||||
-rw-r--r-- | app/scripts/metamask-controller.js | 2 |
3 files changed, 116 insertions, 42 deletions
diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 43735a691..45c6fb25a 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -1,12 +1,12 @@ const EventEmitter = require('events') const async = require('async') const extend = require('xtend') -const Semaphore = require('semaphore') const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') +const denodeify = require('denodeify') const TxProviderUtil = require('../lib/tx-utils') const createId = require('../lib/random-id') -const denodeify = require('denodeify') +const NonceTracker = require('../lib/nonce-tracker') module.exports = class TransactionController extends EventEmitter { constructor (opts) { @@ -20,6 +20,17 @@ module.exports = class TransactionController extends EventEmitter { this.txHistoryLimit = opts.txHistoryLimit this.provider = opts.provider this.blockTracker = opts.blockTracker + this.nonceTracker = new NonceTracker({ + provider: this.provider, + blockTracker: this.provider._blockTracker, + getPendingTransactions: (address) => { + return this.getFilteredTxList({ + from: address, + status: 'submitted', + err: undefined, + }) + }, + }) this.query = opts.ethQuery this.txProviderUtils = new TxProviderUtil(this.query) this.blockTracker.on('rawBlock', this.checkForTxInBlock.bind(this)) @@ -29,7 +40,6 @@ module.exports = class TransactionController extends EventEmitter { this.blockTracker.once('latest', () => this.blockTracker.on('latest', this.resubmitPendingTxs.bind(this))) this.blockTracker.on('sync', this.queryPendingTxs.bind(this)) this.signEthTx = opts.signTransaction - this.nonceLock = Semaphore(1) this.ethStore = opts.ethStore // memstore is computed from a few different stores this._updateMemstore() @@ -173,29 +183,33 @@ module.exports = class TransactionController extends EventEmitter { }, {}) } - approveTransaction (txId, cb = warn) { - const self = this - // approve - self.setTxStatusApproved(txId) - // only allow one tx at a time for atomic nonce usage - self.nonceLock.take(() => { - // begin signature process - async.waterfall([ - (cb) => self.fillInTxParams(txId, cb), - (cb) => self.signTransaction(txId, cb), - (rawTx, cb) => self.publishTransaction(txId, rawTx, cb), - ], (err) => { - self.nonceLock.leave() - if (err) { - this.setTxStatusFailed(txId, { - errCode: err.errCode || err, - message: err.message || 'Transaction failed during approval', - }) - return cb(err) - } - cb() + async approveTransaction (txId, cb = warn) { + let nonceLock + try { + // approve + this.setTxStatusApproved(txId) + // get next nonce + const txMeta = this.getTx(txId) + const fromAddress = txMeta.txParams.from + nonceLock = await this.nonceTracker.getNonceLock(fromAddress) + txMeta.txParams.nonce = nonceLock.nextNonce + this.updateTx(txMeta) + // sign transaction + const rawTx = await denodeify(this.signTransaction.bind(this))(txId) + await this.publishTransaction(txId, rawTx) + // must set transaction to submitted/failed before releasing lock + nonceLock.releaseLock() + cb() + } catch (err) { + this.setTxStatusFailed(txId, { + errCode: err.errCode || err, + message: err.message || 'Transaction failed during approval', }) - }) + // must set transaction to submitted/failed before releasing lock + if (nonceLock) nonceLock.releaseLock() + // continue with error chain + cb(err) + } } cancelTransaction (txId, cb = warn) { @@ -203,15 +217,6 @@ module.exports = class TransactionController extends EventEmitter { cb() } - fillInTxParams (txId, cb) { - const txMeta = this.getTx(txId) - this.txProviderUtils.fillInTxParams(txMeta.txParams, (err) => { - if (err) return cb(err) - this.updateTx(txMeta) - cb() - }) - } - getChainId () { const networkState = this.networkStore.getState() const getChainId = parseInt(networkState) @@ -237,16 +242,17 @@ module.exports = class TransactionController extends EventEmitter { }) } - publishTransaction (txId, rawTx, cb = warn) { + publishTransaction (txId, rawTx) { const txMeta = this.getTx(txId) txMeta.rawTx = rawTx this.updateTx(txMeta) - - this.txProviderUtils.publishTransaction(rawTx, (err, txHash) => { - if (err) return cb(err) - this.setTxHash(txId, txHash) - this.setTxStatusSubmitted(txId) - cb() + return new Promise((resolve, reject) => { + this.txProviderUtils.publishTransaction(rawTx, (err, txHash) => { + if (err) reject(err) + this.setTxHash(txId, txHash) + this.setTxStatusSubmitted(txId) + resolve() + }) }) } @@ -264,10 +270,19 @@ module.exports = class TransactionController extends EventEmitter { to: '0x0..', from: '0x0..', status: 'signed', + err: undefined, } and returns a list of tx with all options matching + ****************HINT**************** + | `err: undefined` is like looking | + | for a tx with no err | + | so you can also search txs that | + | dont have something as well by | + | setting the value as undefined | + ************************************ + this is for things like filtering a the tx list for only tx's from 1 account or for filltering for all txs from one account diff --git a/app/scripts/lib/nonce-tracker.js b/app/scripts/lib/nonce-tracker.js new file mode 100644 index 000000000..ab2893b10 --- /dev/null +++ b/app/scripts/lib/nonce-tracker.js @@ -0,0 +1,59 @@ +const EthQuery = require('eth-query') + +class NonceTracker { + + constructor ({ blockTracker, provider, getPendingTransactions }) { + this.blockTracker = blockTracker + this.ethQuery = new EthQuery(provider) + this.getPendingTransactions = getPendingTransactions + this.lockMap = {} + } + + // releaseLock must be called + // releaseLock must be called after adding signed tx to pending transactions (or discarding) + async getNonceLock (address) { + // await lock free + await this.lockMap[address] + // take lock + const releaseLock = this._takeLock(address) + // calculate next nonce + // we need to make sure our base count + // and pending count are from the same block + const currentBlock = await this._getCurrentBlock() + const pendingTransactions = this.getPendingTransactions(address) + const baseCount = await this._getTxCount(address, currentBlock) + const nextNonce = parseInt(baseCount) + pendingTransactions.length + // return next nonce and release cb + return { nextNonce: nextNonce.toString(16), releaseLock } + } + + async _getCurrentBlock () { + const currentBlock = this.blockTracker.getCurrentBlock() + if (currentBlock) return currentBlock + return await Promise((reject, resolve) => { + this.blockTracker.once('latest', resolve) + }) + } + + _takeLock (lockId) { + let releaseLock = null + // create and store lock + const lock = new Promise((resolve, reject) => { releaseLock = resolve }) + this.lockMap[lockId] = lock + // setup lock teardown + lock.then(() => delete this.lockMap[lockId]) + return releaseLock + } + + async _getTxCount (address, currentBlock) { + const blockNumber = currentBlock.number + return new Promise((resolve, reject) => { + this.ethQuery.getTransactionCount(address, blockNumber, (err, result) => { + err ? reject(err) : resolve(result) + }) + }) + } + +} + +module.exports = NonceTracker diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 73093dfad..573594b39 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -312,7 +312,7 @@ module.exports = class MetamaskController extends EventEmitter { exportAccount: nodeify(keyringController.exportAccount).bind(keyringController), // txController - approveTransaction: txController.approveTransaction.bind(txController), + approveTransaction: nodeify(txController.approveTransaction).bind(txController), cancelTransaction: txController.cancelTransaction.bind(txController), updateAndApproveTransaction: this.updateAndApproveTx.bind(this), |