diff --git a/CHANGELOG.md b/CHANGELOG.md index cf478a8a3..239203553 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- Add a check for when a tx is included in a block. + ## 2.14.1 2016-12-20 - Temporarily disable extension reload detection causing infinite reload bug. diff --git a/README.md b/README.md index fdbe3c535..3a7277f3f 100644 --- a/README.md +++ b/README.md @@ -117,3 +117,39 @@ To write tests that will be run in the browser using QUnit, add your test files 3. Upload the latest zip file from `builds/metamask-$PLATFORM-$VERSION.zip` as the updated package. [1]: http://www.nomnoml.com/#view/%5B%3Cactor%3Euser%5D%0A%0A%5Bmetamask-ui%7C%0A%20%20%20%5Btools%7C%0A%20%20%20%20%20react%0A%20%20%20%20%20redux%0A%20%20%20%20%20thunk%0A%20%20%20%20%20ethUtils%0A%20%20%20%20%20jazzicon%0A%20%20%20%5D%0A%20%20%20%5Bcomponents%7C%0A%20%20%20%20%20app%0A%20%20%20%20%20account-detail%0A%20%20%20%20%20accounts%0A%20%20%20%20%20locked-screen%0A%20%20%20%20%20restore-vault%0A%20%20%20%20%20identicon%0A%20%20%20%20%20config%0A%20%20%20%20%20info%0A%20%20%20%5D%0A%20%20%20%5Breducers%7C%0A%20%20%20%20%20app%0A%20%20%20%20%20metamask%0A%20%20%20%20%20identities%0A%20%20%20%5D%0A%20%20%20%5Bactions%7C%0A%20%20%20%20%20%5BaccountManager%5D%0A%20%20%20%5D%0A%20%20%20%5Bcomponents%5D%3A-%3E%5Bactions%5D%0A%20%20%20%5Bactions%5D%3A-%3E%5Breducers%5D%0A%20%20%20%5Breducers%5D%3A-%3E%5Bcomponents%5D%0A%5D%0A%0A%5Bweb%20dapp%7C%0A%20%20%5Bui%20code%5D%0A%20%20%5Bweb3%5D%0A%20%20%5Bmetamask-inpage%5D%0A%20%20%0A%20%20%5B%3Cactor%3Eui%20developer%5D%0A%20%20%5Bui%20developer%5D-%3E%5Bui%20code%5D%0A%20%20%5Bui%20code%5D%3C-%3E%5Bweb3%5D%0A%20%20%5Bweb3%5D%3C-%3E%5Bmetamask-inpage%5D%0A%5D%0A%0A%5Bmetamask-background%7C%0A%20%20%5Bprovider-engine%5D%0A%20%20%5Bhooked%20wallet%20subprovider%5D%0A%20%20%5Bid%20store%5D%0A%20%20%0A%20%20%5Bprovider-engine%5D%3C-%3E%5Bhooked%20wallet%20subprovider%5D%0A%20%20%5Bhooked%20wallet%20subprovider%5D%3C-%3E%5Bid%20store%5D%0A%20%20%5Bconfig%20manager%7C%0A%20%20%20%20%5Brpc%20configuration%5D%0A%20%20%20%20%5Bencrypted%20keys%5D%0A%20%20%20%20%5Bwallet%20nicknames%5D%0A%20%20%5D%0A%20%20%0A%20%20%5Bprovider-engine%5D%3C-%5Bconfig%20manager%5D%0A%20%20%5Bid%20store%5D%3C-%3E%5Bconfig%20manager%5D%0A%5D%0A%0A%5Buser%5D%3C-%3E%5Bmetamask-ui%5D%0A%0A%5Buser%5D%3C%3A--%3A%3E%5Bweb%20dapp%5D%0A%0A%5Bmetamask-contentscript%7C%0A%20%20%5Bplugin%20restart%20detector%5D%0A%20%20%5Brpc%20passthrough%5D%0A%5D%0A%0A%5Brpc%20%7C%0A%20%20%5Bethereum%20blockchain%20%7C%0A%20%20%20%20%5Bcontracts%5D%0A%20%20%20%20%5Baccounts%5D%0A%20%20%5D%0A%5D%0A%0A%5Bweb%20dapp%5D%3C%3A--%3A%3E%5Bmetamask-contentscript%5D%0A%5Bmetamask-contentscript%5D%3C-%3E%5Bmetamask-background%5D%0A%5Bmetamask-background%5D%3C-%3E%5Bmetamask-ui%5D%0A%5Bmetamask-background%5D%3C-%3E%5Brpc%5D%0A + + +### Generate Development Visualization + +This will generate a video of the repo commit history. + +Install preqs: +``` +brew install gource +brew install ffmpeg +``` + +From the repo dir, pipe `gource` into `ffmpeg`: +``` +gource \ + --seconds-per-day .1 \ + --user-scale 1.5 \ + --default-user-image "./images/icon-512.png" \ + --viewport 1280x720 \ + --auto-skip-seconds .1 \ + --multi-sampling \ + --stop-at-end \ + --highlight-users \ + --hide mouse,progress \ + --file-idle-time 0 \ + --max-files 0 \ + --background-colour 000000 \ + --font-size 18 \ + --date-format "%b %d, %Y" \ + --highlight-dirs \ + --user-friction 0.1 \ + --title "MetaMask Development History" \ + --output-ppm-stream - \ + --output-framerate 30 \ + | ffmpeg -y -r 30 -f image2pipe -vcodec ppm -i - -b 65536K metamask-dev-history.mp4 +``` diff --git a/app/scripts/background.js b/app/scripts/background.js index 7cb25d8bf..ca2efc114 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -17,13 +17,13 @@ const controller = new MetamaskController({ // User confirmation callbacks: showUnconfirmedMessage: triggerUi, unlockAccountMessage: triggerUi, - showUnconfirmedTx: triggerUi, + showUnapprovedTx: triggerUi, // Persistence Methods: setData, loadData, }) const keyringController = controller.keyringController - +const txManager = controller.txManager function triggerUi () { if (!popupIsOpen) notification.show() } @@ -97,15 +97,14 @@ function setupControllerConnection (stream) { // plugin badge text // -keyringController.on('update', updateBadge) +txManager.on('updateBadge', updateBadge) function updateBadge () { var label = '' - var unconfTxs = controller.configManager.unconfirmedTxs() - var unconfTxLen = Object.keys(unconfTxs).length + var unapprovedTxCount = controller.txManager.unapprovedTxCount var unconfMsgs = messageManager.unconfirmedMsgs() var unconfMsgLen = Object.keys(unconfMsgs).length - var count = unconfTxLen + unconfMsgLen + var count = unapprovedTxCount + unconfMsgLen if (count) { label = String(count) } @@ -113,6 +112,8 @@ function updateBadge () { extension.browserAction.setBadgeBackgroundColor({ color: '#506F8B' }) } +// data :: setters/getters + function loadData () { var oldData = getOldStyleData() var newData diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js index d53de1ab6..857f2e684 100644 --- a/app/scripts/keyring-controller.js +++ b/app/scripts/keyring-controller.js @@ -1,8 +1,5 @@ -const async = require('async') const ethUtil = require('ethereumjs-util') -const EthQuery = require('eth-query') const bip39 = require('bip39') -const Transaction = require('ethereumjs-tx') const EventEmitter = require('events').EventEmitter const filter = require('promise-filter') const encryptor = require('browser-passworder') @@ -36,11 +33,9 @@ module.exports = class KeyringController extends EventEmitter { this.ethStore = opts.ethStore this.encryptor = encryptor this.keyringTypes = keyringTypes - this.keyrings = [] this.identities = {} // Essentially a name hash - this._unconfTxCbs = {} this._unconfMsgCbs = {} this.getNetwork = opts.getNetwork @@ -319,202 +314,18 @@ module.exports = class KeyringController extends EventEmitter { } - // SIGNING RELATED METHODS + // SIGNING METHODS // - // SIGN, SUBMIT TX, CANCEL, AND APPROVE. - // THIS SECTION INVOLVES THE REQUEST, STORING, AND SIGNING OF DATA - // WITH THE KEYS STORED IN THIS CONTROLLER. - - - // Add Unconfirmed Transaction - // @object txParams - // @function onTxDoneCb - // @function cb - // - // Calls back `cb` with @object txData = { txParams } - // Calls back `onTxDoneCb` with `true` or an `error` depending on result. - // - // Prepares the given `txParams` for final confirmation and approval. - // Estimates gas and other preparatory steps. - // Caches the requesting Dapp's callback, `onTxDoneCb`, for resolution later. - addUnconfirmedTransaction (txParams, onTxDoneCb, cb) { - const configManager = this.configManager - - // create txData obj with parameters and meta data - var time = (new Date()).getTime() - var txId = createId() - txParams.metamaskId = txId - txParams.metamaskNetworkId = this.getNetwork() - var txData = { - id: txId, - txParams: txParams, - time: time, - status: 'unconfirmed', - gasMultiplier: configManager.getGasMultiplier() || 1, - metamaskNetworkId: this.getNetwork(), - } - - // keep the onTxDoneCb around for after approval/denial (requires user interaction) - // This onTxDoneCb fires completion to the Dapp's write operation. - this._unconfTxCbs[txId] = onTxDoneCb - - var provider = this.ethStore._query.currentProvider - var query = new EthQuery(provider) - - // calculate metadata for tx - this.analyzeTxGasUsage(query, txData, this.txDidComplete.bind(this, txData, cb)) - } - - estimateTxGas (query, txData, blockGasLimitHex, cb) { - const txParams = txData.txParams - // check if gasLimit is already specified - txData.gasLimitSpecified = Boolean(txParams.gas) - // if not, fallback to block gasLimit - if (!txData.gasLimitSpecified) { - txParams.gas = blockGasLimitHex - } - // run tx, see if it will OOG - query.estimateGas(txParams, cb) - } - - checkForTxGasError (txData, estimatedGasHex, cb) { - txData.estimatedGas = estimatedGasHex - // all gas used - must be an error - if (estimatedGasHex === txData.txParams.gas) { - txData.simulationFails = true - } - cb() - } - - setTxGas (txData, blockGasLimitHex, cb) { - const txParams = txData.txParams - // if OOG, nothing more to do - if (txData.simulationFails) { - cb() - return - } - // if gasLimit was specified and doesnt OOG, - // use original specified amount - if (txData.gasLimitSpecified) { - txData.estimatedGas = txParams.gas - cb() - return - } - // if gasLimit not originally specified, - // try adding an additional gas buffer to our estimation for safety - const estimatedGasBn = new BN(ethUtil.stripHexPrefix(txData.estimatedGas), 16) - const blockGasLimitBn = new BN(ethUtil.stripHexPrefix(blockGasLimitHex), 16) - const estimationWithBuffer = new BN(this.addGasBuffer(estimatedGasBn), 16) - // added gas buffer is too high - if (estimationWithBuffer.gt(blockGasLimitBn)) { - txParams.gas = txData.estimatedGas - // added gas buffer is safe - } else { - const gasWithBufferHex = ethUtil.intToHex(estimationWithBuffer) - txParams.gas = gasWithBufferHex - } - cb() - return - } - - txDidComplete (txData, cb, err) { - if (err) return cb(err) - const configManager = this.configManager - configManager.addTx(txData) - // signal update - this.emit('update') - // signal completion of add tx - cb(null, txData) - } - - analyzeTxGasUsage (query, txData, cb) { - query.getBlockByNumber('latest', true, (err, block) => { - if (err) return cb(err) - async.waterfall([ - this.estimateTxGas.bind(this, query, txData, block.gasLimit), - this.checkForTxGasError.bind(this, txData), - this.setTxGas.bind(this, txData, block.gasLimit), - ], cb) - }) - } - - // Cancel Transaction - // @string txId - // @function cb - // - // Calls back `cb` with no error if provided. - // - // Forgets any tx matching `txId`. - cancelTransaction (txId, cb) { - const configManager = this.configManager - var approvalCb = this._unconfTxCbs[txId] || noop - - // reject tx - approvalCb(null, false) - // clean up - configManager.rejectTx(txId) - delete this._unconfTxCbs[txId] - - if (cb && typeof cb === 'function') { - cb() - } - } - - // Approve Transaction - // @string txId - // @function cb - // - // Calls back `cb` with no error always. - // - // Attempts to sign a Transaction with `txId` - // and submit it to the blockchain. - // - // Calls back the cached Dapp's confirmation callback, also. - approveTransaction (txId, cb) { - const configManager = this.configManager - var approvalCb = this._unconfTxCbs[txId] || noop - - // accept tx - cb() - approvalCb(null, true) - // clean up - configManager.confirmTx(txId) - delete this._unconfTxCbs[txId] - this.emit('update') - } - - signTransaction (txParams, cb) { + // This method signs tx and returns a promise for + // TX Manager to update the state after signing + signTransaction (ethTx, selectedAddress, txId, cb) { try { - const address = normalize(txParams.from) + const address = normalize(selectedAddress) return this.getKeyringForAccount(address) .then((keyring) => { - // Handle gas pricing - var gasMultiplier = this.configManager.getGasMultiplier() || 1 - var gasPrice = new BN(ethUtil.stripHexPrefix(txParams.gasPrice), 16) - gasPrice = gasPrice.mul(new BN(gasMultiplier * 100, 10)).div(new BN(100, 10)) - txParams.gasPrice = ethUtil.intToHex(gasPrice.toNumber()) - - // normalize values - txParams.to = normalize(txParams.to) - txParams.from = normalize(txParams.from) - txParams.value = normalize(txParams.value) - txParams.data = normalize(txParams.data) - txParams.gasLimit = normalize(txParams.gasLimit || txParams.gas) - txParams.nonce = normalize(txParams.nonce) - - const tx = new Transaction(txParams) - return keyring.signTransaction(address, tx) - }) - .then((tx) => { - // Add the tx hash to the persisted meta-tx object - var txHash = ethUtil.bufferToHex(tx.hash()) - var metaTx = this.configManager.getTx(txParams.metamaskId) - metaTx.hash = txHash - this.configManager.updateTx(metaTx) - - // return raw serialized tx - var rawTx = ethUtil.bufferToHex(tx.serialize()) - cb(null, rawTx) + return keyring.signTransaction(address, ethTx) + }).then((tx) => { + this.emit(`${txId}:signed`, {tx, txId, cb}) }) } catch (e) { cb(e) diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js index ede877b76..93501c859 100644 --- a/app/scripts/lib/config-manager.js +++ b/app/scripts/lib/config-manager.js @@ -8,7 +8,6 @@ const normalize = require('./sig-util').normalize const TESTNET_RPC = MetamaskConfig.network.testnet const MAINNET_RPC = MetamaskConfig.network.mainnet const MORDEN_RPC = MetamaskConfig.network.morden -const txLimit = 40 /* The config-manager is a convenience object * wrapping a pojo-migrator. @@ -19,8 +18,6 @@ const txLimit = 40 */ module.exports = ConfigManager function ConfigManager (opts) { - this.txLimit = txLimit - // ConfigManager is observable and will emit updates this._subs = [] @@ -209,61 +206,12 @@ ConfigManager.prototype.getTxList = function () { } } -ConfigManager.prototype.unconfirmedTxs = function () { - var transactions = this.getTxList() - return transactions.filter(tx => tx.status === 'unconfirmed') - .reduce((result, tx) => { result[tx.id] = tx; return result }, {}) -} - -ConfigManager.prototype._saveTxList = function (txList) { +ConfigManager.prototype.setTxList = function (txList) { var data = this.migrator.getData() data.transactions = txList this.setData(data) } -ConfigManager.prototype.addTx = function (tx) { - var transactions = this.getTxList() - while (transactions.length > this.txLimit - 1) { - transactions.shift() - } - transactions.push(tx) - this._saveTxList(transactions) -} - -ConfigManager.prototype.getTx = function (txId) { - var transactions = this.getTxList() - var matching = transactions.filter(tx => tx.id === txId) - return matching.length > 0 ? matching[0] : null -} - -ConfigManager.prototype.confirmTx = function (txId) { - this._setTxStatus(txId, 'confirmed') -} - -ConfigManager.prototype.rejectTx = function (txId) { - this._setTxStatus(txId, 'rejected') -} - -ConfigManager.prototype._setTxStatus = function (txId, status) { - var tx = this.getTx(txId) - tx.status = status - this.updateTx(tx) -} - -ConfigManager.prototype.updateTx = function (tx) { - var transactions = this.getTxList() - var found, index - transactions.forEach((otherTx, i) => { - if (otherTx.id === tx.id) { - found = true - index = i - } - }) - if (found) { - transactions[index] = tx - } - this._saveTxList(transactions) -} // wallet nickname methods diff --git a/app/scripts/lib/idStore.js b/app/scripts/lib/idStore.js index 66e5d966c..e4cbca456 100644 --- a/app/scripts/lib/idStore.js +++ b/app/scripts/lib/idStore.js @@ -1,19 +1,14 @@ const EventEmitter = require('events').EventEmitter const inherits = require('util').inherits -const async = require('async') const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const EthQuery = require('eth-query') const KeyStore = require('eth-lightwallet').keystore const clone = require('clone') const extend = require('xtend') -const createId = require('./random-id') -const ethBinToOps = require('eth-bin-to-ops') const autoFaucet = require('./auto-faucet') -const messageManager = require('./message-manager') const DEFAULT_RPC = 'https://testrpc.metamask.io/' const IdManagement = require('./id-management') + module.exports = IdentityStore inherits(IdentityStore, EventEmitter) @@ -34,10 +29,7 @@ function IdentityStore (opts = {}) { selectedAddress: null, identities: {}, } - // not part of serilized metamask state - only kept in memory - this._unconfTxCbs = {} - this._unconfMsgCbs = {} } // @@ -103,10 +95,6 @@ IdentityStore.prototype.getState = function () { isUnlocked: this._isUnlocked(), seedWords: seedWords, isDisclaimerConfirmed: configManager.getConfirmedDisclaimer(), - unconfTxs: configManager.unconfirmedTxs(), - transactions: configManager.getTxList(), - unconfMsgs: messageManager.unconfirmedMsgs(), - messages: messageManager.getMsgList(), selectedAddress: configManager.getSelectedAccount(), shapeShiftTxList: configManager.getShapeShiftTxList(), currentFiat: configManager.getCurrentFiat(), @@ -206,245 +194,6 @@ IdentityStore.prototype.exportAccount = function (address, cb) { return privateKey } -// -// Transactions -// - -// comes from dapp via zero-client hooked-wallet provider -IdentityStore.prototype.addUnconfirmedTransaction = function (txParams, onTxDoneCb, cb) { - const configManager = this.configManager - - var self = this - // create txData obj with parameters and meta data - var time = (new Date()).getTime() - var txId = createId() - txParams.metamaskId = txId - txParams.metamaskNetworkId = self._currentState.network - var txData = { - id: txId, - txParams: txParams, - time: time, - status: 'unconfirmed', - gasMultiplier: configManager.getGasMultiplier() || 1, - } - - console.log('addUnconfirmedTransaction:', txData) - - // keep the onTxDoneCb around for after approval/denial (requires user interaction) - // This onTxDoneCb fires completion to the Dapp's write operation. - self._unconfTxCbs[txId] = onTxDoneCb - - var provider = self._ethStore._query.currentProvider - var query = new EthQuery(provider) - - // calculate metadata for tx - async.parallel([ - analyzeForDelegateCall, - estimateGas, - ], didComplete) - - // perform static analyis on the target contract code - function analyzeForDelegateCall (cb) { - if (txParams.to) { - query.getCode(txParams.to, (err, result) => { - if (err) return cb(err.message || err) - var containsDelegateCall = self.checkForDelegateCall(result) - txData.containsDelegateCall = containsDelegateCall - cb() - }) - } else { - cb() - } - } - - function estimateGas (cb) { - var estimationParams = extend(txParams) - query.getBlockByNumber('latest', true, function (err, block) { - if (err) return cb(err) - // check if gasLimit is already specified - const gasLimitSpecified = Boolean(txParams.gas) - // if not, fallback to block gasLimit - if (!gasLimitSpecified) { - estimationParams.gas = block.gasLimit - } - // run tx, see if it will OOG - query.estimateGas(estimationParams, function (err, estimatedGasHex) { - if (err) return cb(err.message || err) - // all gas used - must be an error - if (estimatedGasHex === estimationParams.gas) { - txData.simulationFails = true - txData.estimatedGas = estimatedGasHex - txData.txParams.gas = estimatedGasHex - cb() - return - } - // otherwise, did not use all gas, must be ok - - // if specified gasLimit and no error, we're done - if (gasLimitSpecified) { - txData.estimatedGas = txParams.gas - cb() - return - } - - // try adding an additional gas buffer to our estimation for safety - const estimatedGasBn = new BN(ethUtil.stripHexPrefix(estimatedGasHex), 16) - const blockGasLimitBn = new BN(ethUtil.stripHexPrefix(block.gasLimit), 16) - const estimationWithBuffer = self.addGasBuffer(estimatedGasBn) - // added gas buffer is too high - if (estimationWithBuffer.gt(blockGasLimitBn)) { - txData.estimatedGas = estimatedGasHex - txData.txParams.gas = estimatedGasHex - // added gas buffer is safe - } else { - const gasWithBufferHex = ethUtil.intToHex(estimationWithBuffer) - txData.estimatedGas = gasWithBufferHex - txData.txParams.gas = gasWithBufferHex - } - cb() - return - }) - }) - } - - function didComplete (err) { - if (err) return cb(err.message || err) - configManager.addTx(txData) - // signal update - self._didUpdate() - // signal completion of add tx - cb(null, txData) - } -} - -IdentityStore.prototype.checkForDelegateCall = function (codeHex) { - const code = ethUtil.toBuffer(codeHex) - if (code !== '0x') { - const ops = ethBinToOps(code) - const containsDelegateCall = ops.some((op) => op.name === 'DELEGATECALL') - return containsDelegateCall - } else { - return false - } -} - -IdentityStore.prototype.addGasBuffer = function (gasBn) { - // add 20% to specified gas - const gasBuffer = gasBn.div(new BN('5', 10)) - const gasWithBuffer = gasBn.add(gasBuffer) - return gasWithBuffer -} - -// comes from metamask ui -IdentityStore.prototype.approveTransaction = function (txId, cb) { - const configManager = this.configManager - var approvalCb = this._unconfTxCbs[txId] || noop - - // accept tx - cb() - approvalCb(null, true) - // clean up - configManager.confirmTx(txId) - delete this._unconfTxCbs[txId] - this._didUpdate() -} - -// comes from metamask ui -IdentityStore.prototype.cancelTransaction = function (txId) { - const configManager = this.configManager - var approvalCb = this._unconfTxCbs[txId] || noop - - // reject tx - approvalCb(null, false) - // clean up - configManager.rejectTx(txId) - delete this._unconfTxCbs[txId] - this._didUpdate() -} - -// performs the actual signing, no autofill of params -IdentityStore.prototype.signTransaction = function (txParams, cb) { - try { - console.log('signing tx...', txParams) - var rawTx = this._idmgmt.signTx(txParams) - cb(null, rawTx) - } catch (err) { - cb(err) - } -} - -// -// Messages -// - -// comes from dapp via zero-client hooked-wallet provider -IdentityStore.prototype.addUnconfirmedMessage = function (msgParams, cb) { - // create txData obj with parameters and meta data - var time = (new Date()).getTime() - var msgId = createId() - var msgData = { - id: msgId, - msgParams: msgParams, - time: time, - status: 'unconfirmed', - } - messageManager.addMsg(msgData) - console.log('addUnconfirmedMessage:', msgData) - - // keep the cb around for after approval (requires user interaction) - // This cb fires completion to the Dapp's write operation. - this._unconfMsgCbs[msgId] = cb - - // signal update - this._didUpdate() - - return msgId -} - -// comes from metamask ui -IdentityStore.prototype.approveMessage = function (msgId, cb) { - var approvalCb = this._unconfMsgCbs[msgId] || noop - - // accept msg - cb() - approvalCb(null, true) - // clean up - messageManager.confirmMsg(msgId) - delete this._unconfMsgCbs[msgId] - this._didUpdate() -} - -// comes from metamask ui -IdentityStore.prototype.cancelMessage = function (msgId) { - var approvalCb = this._unconfMsgCbs[msgId] || noop - - // reject tx - approvalCb(null, false) - // clean up - messageManager.rejectMsg(msgId) - delete this._unconfTxCbs[msgId] - this._didUpdate() -} - -// performs the actual signing, no autofill of params -IdentityStore.prototype.signMessage = function (msgParams, cb) { - try { - console.log('signing msg...', msgParams.data) - var rawMsg = this._idmgmt.signMsg(msgParams.from, msgParams.data) - if ('metamaskId' in msgParams) { - var id = msgParams.metamaskId - delete msgParams.metamaskId - - this.approveMessage(id, cb) - } else { - cb(null, rawMsg) - } - } catch (err) { - cb(err) - } -} - -// // private // @@ -599,4 +348,3 @@ IdentityStore.prototype._autoFaucet = function () { // util -function noop () {} diff --git a/app/scripts/lib/tx-utils.js b/app/scripts/lib/tx-utils.js new file mode 100644 index 000000000..a976173f5 --- /dev/null +++ b/app/scripts/lib/tx-utils.js @@ -0,0 +1,87 @@ +const async = require('async') +const EthQuery = require('eth-query') +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN + +/* +tx-utils are utility methods for Transaction manager +its passed a provider and that is passed to ethquery +and used to do things like calculate gas of a tx. +*/ + +module.exports = class txProviderUtils { + constructor (provider) { + this.provider = provider + this.query = new EthQuery(provider) + } + analyzeGasUsage (txData, cb) { + var self = this + this.query.getBlockByNumber('latest', true, (err, block) => { + if (err) return cb(err) + async.waterfall([ + self.estimateTxGas.bind(self, txData, block.gasLimit), + self.checkForTxGasError.bind(self, txData), + self.setTxGas.bind(self, txData, block.gasLimit), + ], cb) + }) + } + + estimateTxGas (txData, blockGasLimitHex, cb) { + const txParams = txData.txParams + // check if gasLimit is already specified + txData.gasLimitSpecified = Boolean(txParams.gas) + // if not, fallback to block gasLimit + if (!txData.gasLimitSpecified) { + txParams.gas = blockGasLimitHex + } + // run tx, see if it will OOG + this.query.estimateGas(txParams, cb) + } + + checkForTxGasError (txData, estimatedGasHex, cb) { + txData.estimatedGas = estimatedGasHex + // all gas used - must be an error + if (estimatedGasHex === txData.txParams.gas) { + txData.simulationFails = true + } + cb() + } + + setTxGas (txData, blockGasLimitHex, cb) { + const txParams = txData.txParams + // if OOG, nothing more to do + if (txData.simulationFails) { + cb() + return + } + // if gasLimit was specified and doesnt OOG, + // use original specified amount + if (txData.gasLimitSpecified) { + txData.estimatedGas = txParams.gas + cb() + return + } + // if gasLimit not originally specified, + // try adding an additional gas buffer to our estimation for safety + const estimatedGasBn = new BN(ethUtil.stripHexPrefix(txData.estimatedGas), 16) + const blockGasLimitBn = new BN(ethUtil.stripHexPrefix(blockGasLimitHex), 16) + const estimationWithBuffer = new BN(this.addGasBuffer(estimatedGasBn), 16) + // added gas buffer is too high + if (estimationWithBuffer.gt(blockGasLimitBn)) { + txParams.gas = txData.estimatedGas + // added gas buffer is safe + } else { + const gasWithBufferHex = ethUtil.intToHex(estimationWithBuffer) + txParams.gas = gasWithBufferHex + } + cb() + return + } + + addGasBuffer (gas) { + const gasBuffer = new BN('100000', 10) + const bnGas = new BN(ethUtil.stripHexPrefix(gas), 16) + const correct = bnGas.add(gasBuffer) + return ethUtil.addHexPrefix(correct.toString(16)) + } +} diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 8d4d574ff..46e093000 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -4,6 +4,7 @@ const MetaMaskProvider = require('web3-provider-engine/zero.js') const KeyringController = require('./keyring-controller') const NoticeController = require('./notice-controller') const messageManager = require('./lib/message-manager') +const TxManager = require('./transaction-manager') const HostStore = require('./lib/remote-store.js').HostStore const Web3 = require('web3') const ConfigManager = require('./lib/config-manager') @@ -12,7 +13,6 @@ const autoFaucet = require('./lib/auto-faucet') const nodeify = require('./lib/nodeify') const IdStoreMigrator = require('./lib/idStore-migrator') - module.exports = class MetamaskController { constructor (opts) { @@ -36,6 +36,16 @@ module.exports = class MetamaskController { this.keyringController.setStore(this.ethStore) this.getNetwork() this.messageManager = messageManager + this.txManager = new TxManager({ + txList: this.configManager.getTxList(), + txHistoryLimit: 40, + setTxList: this.configManager.setTxList.bind(this.configManager), + getSelectedAccount: this.configManager.getSelectedAccount.bind(this.configManager), + getGasMultiplier: this.configManager.getGasMultiplier.bind(this.configManager), + getNetwork: this.getStateNetwork.bind(this), + provider: this.provider, + blockTracker: this.provider, + }) this.publicConfigStore = this.initPublicConfigStore() var currentFiat = this.configManager.getCurrentFiat() || 'USD' @@ -69,6 +79,7 @@ module.exports = class MetamaskController { getApi () { const keyringController = this.keyringController + const txManager = this.txManager const noticeController = this.noticeController return { @@ -103,8 +114,8 @@ module.exports = class MetamaskController { exportAccount: nodeify(keyringController.exportAccount).bind(keyringController), // signing methods - approveTransaction: keyringController.approveTransaction.bind(keyringController), - cancelTransaction: keyringController.cancelTransaction.bind(keyringController), + approveTransaction: txManager.approveTransaction.bind(txManager), + cancelTransaction: txManager.cancelTransaction.bind(txManager), signMessage: keyringController.signMessage.bind(keyringController), cancelMessage: keyringController.cancelMessage.bind(keyringController), @@ -168,7 +179,8 @@ module.exports = class MetamaskController { // tx signing approveTransaction: this.newUnsignedTransaction.bind(this), signTransaction: (...args) => { - keyringController.signTransaction(...args) + this.setupSigningListners(...args) + this.txManager.formatTxForSigining(...args) this.sendUpdate() }, @@ -184,7 +196,6 @@ module.exports = class MetamaskController { var web3 = new Web3(provider) this.web3 = web3 keyringController.web3 = web3 - provider.on('block', this.processBlock.bind(this)) provider.on('error', this.getNetwork.bind(this)) @@ -238,16 +249,23 @@ module.exports = class MetamaskController { } newUnsignedTransaction (txParams, onTxDoneCb) { - const keyringController = this.keyringController + const txManager = this.txManager const err = this.enforceTxValidations(txParams) if (err) return onTxDoneCb(err) - keyringController.addUnconfirmedTransaction(txParams, onTxDoneCb, (err, txData) => { + txManager.addUnapprovedTransaction(txParams, onTxDoneCb, (err, txData) => { if (err) return onTxDoneCb(err) this.sendUpdate() - this.opts.showUnconfirmedTx(txParams, txData, onTxDoneCb) + this.opts.showUnapprovedTx(txParams, txData, onTxDoneCb) }) } + setupSigningListners (txParams) { + var txId = txParams.metamaskId + // apply event listeners for signing and formating events + this.txManager.once(`${txId}:formatted`, this.keyringController.signTransaction.bind(this.keyringController)) + this.keyringController.once(`${txId}:signed`, this.txManager.resolveSignedTransaction.bind(this.txManager)) + } + enforceTxValidations (txParams) { if (('value' in txParams) && txParams.value.indexOf('-') === 0) { const msg = `Invalid transaction value of ${txParams.value} not a positive number.` diff --git a/app/scripts/transaction-manager.js b/app/scripts/transaction-manager.js new file mode 100644 index 000000000..fd136a51b --- /dev/null +++ b/app/scripts/transaction-manager.js @@ -0,0 +1,288 @@ +const EventEmitter = require('events') +const extend = require('xtend') +const ethUtil = require('ethereumjs-util') +const Transaction = require('ethereumjs-tx') +const BN = ethUtil.BN +const TxProviderUtil = require('./lib/tx-utils') +const createId = require('./lib/random-id') +const normalize = require('./lib/sig-util').normalize + +module.exports = class TransactionManager extends EventEmitter { + constructor (opts) { + super() + this.txList = opts.txList || [] + this._setTxList = opts.setTxList + this.txHistoryLimit = opts.txHistoryLimit + this.getSelectedAccount = opts.getSelectedAccount + this.provider = opts.provider + this.blockTracker = opts.blockTracker + this.txProviderUtils = new TxProviderUtil(this.provider) + this.blockTracker.on('block', this.checkForTxInBlock.bind(this)) + this.getGasMultiplier = opts.getGasMultiplier + this.getNetwork = opts.getNetwork + } + + getState () { + var selectedAccount = this.getSelectedAccount() + return { + transactions: this.getTxList(), + unconfTxs: this.getUnapprovedTxList(), + selectedAccountTxList: this.getFilteredTxList({metamaskNetworkId: this.getNetwork(), from: selectedAccount}), + } + } + +// Returns the tx list + getTxList () { + return this.txList + } + + // Adds a tx to the txlist + addTx (txMeta, onTxDoneCb = warn) { + var txList = this.getTxList() + var txHistoryLimit = this.txHistoryLimit + + // checks if the length of th tx history is + // longer then desired persistence limit + // and then if it is removes only confirmed + // or rejected tx's. + // not tx's that are pending or unapproved + if (txList.length > txHistoryLimit - 1) { + var index = txList.findIndex((metaTx) => metaTx.status === 'confirmed' || metaTx.status === 'rejected') + txList.splice(index, 1) + } + txList.push(txMeta) + + this._saveTxList(txList) + // keep the onTxDoneCb around in a listener + // for after approval/denial (requires user interaction) + // This onTxDoneCb fires completion to the Dapp's write operation. + this.once(`${txMeta.id}:signed`, function (txId) { + this.removeAllListeners(`${txMeta.id}:rejected`) + onTxDoneCb(null, true) + }) + this.once(`${txMeta.id}:rejected`, function (txId) { + this.removeAllListeners(`${txMeta.id}:signed`) + onTxDoneCb(null, false) + }) + + this.emit('updateBadge') + 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.getTxList() + var index = txList.findIndex(txData => txData.id === txId) + txList[index] = txMeta + this._saveTxList(txList) + } + + get unapprovedTxCount () { + return Object.keys(this.getUnapprovedTxList()).length + } + + get pendingTxCount () { + return this.getTxsByMetaData('status', 'signed').length + } + + addUnapprovedTransaction (txParams, onTxDoneCb, cb) { + // create txData obj with parameters and meta data + var time = (new Date()).getTime() + var txId = createId() + txParams.metamaskId = txId + txParams.metamaskNetworkId = this.getNetwork() + var txData = { + id: txId, + txParams: txParams, + time: time, + status: 'unapproved', + gasMultiplier: this.getGasMultiplier() || 1, + metamaskNetworkId: this.getNetwork(), + } + this.txProviderUtils.analyzeGasUsage(txData, this.txDidComplete.bind(this, txData, onTxDoneCb, cb)) + // calculate metadata for tx + } + + txDidComplete (txMeta, onTxDoneCb, cb, err) { + if (err) return cb(err) + this.addTx(txMeta, onTxDoneCb) + cb(null, txMeta) + } + + getUnapprovedTxList () { + var txList = this.getTxList() + return txList.filter((txMeta) => txMeta.status === 'unapproved') + .reduce((result, tx) => { + result[tx.id] = tx + return result + }, {}) + } + + approveTransaction (txId, cb = warn) { + this.setTxStatusSigned(txId) + cb() + } + + cancelTransaction (txId, cb = warn) { + this.setTxStatusRejected(txId) + cb() + } + + // formats txParams so the keyringController can sign it + formatTxForSigining (txParams, cb) { + var address = txParams.from + var metaTx = this.getTx(txParams.metamaskId) + var gasMultiplier = metaTx.gasMultiplier + var gasPrice = new BN(ethUtil.stripHexPrefix(txParams.gasPrice), 16) + gasPrice = gasPrice.mul(new BN(gasMultiplier * 100, 10)).div(new BN(100, 10)) + txParams.gasPrice = ethUtil.intToHex(gasPrice.toNumber()) + + // normalize values + txParams.to = normalize(txParams.to) + txParams.from = normalize(txParams.from) + txParams.value = normalize(txParams.value) + txParams.data = normalize(txParams.data) + txParams.gasLimit = normalize(txParams.gasLimit || txParams.gas) + txParams.nonce = normalize(txParams.nonce) + const ethTx = new Transaction(txParams) + + // listener is assigned in metamaskController + this.emit(`${txParams.metamaskId}:formatted`, ethTx, address, txParams.metamaskId, cb) + } + + // receives a signed tx object and updates the tx hash + // and pass it to the cb to be sent off + resolveSignedTransaction ({tx, txId, cb = warn}) { + // Add the tx hash to the persisted meta-tx object + var txHash = ethUtil.bufferToHex(tx.hash()) + var metaTx = this.getTx(txId) + metaTx.hash = txHash + this.updateTx(metaTx) + var rawTx = ethUtil.bufferToHex(tx.serialize()) + cb(null, rawTx) + } + + /* + Takes an object of fields to search for eg: + var thingsToLookFor = { + to: '0x0..', + from: '0x0..', + status: 'signed', + } + and returns a list of tx with all + options matching + + 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 + and that have been 'confirmed' + */ + getFilteredTxList (opts) { + var filteredTxList + Object.keys(opts).forEach((key) => { + filteredTxList = this.getTxsByMetaData(key, opts[key], filteredTxList) + }) + return filteredTxList + } + + getTxsByMetaData (key, value, txList = this.getTxList()) { + return txList.filter((txMeta) => { + if (key in txMeta.txParams) { + return txMeta.txParams[key] === value + } else { + return txMeta[key] === value + } + }) + } + + // STATUS METHODS + // get::set status + + // should return the status of the tx. + getTxStatus (txId) { + const txMeta = this.getTx(txId) + return txMeta.status + } + + + // should update the status of the tx to 'signed'. + setTxStatusSigned (txId) { + this._setTxStatus(txId, 'signed') + this.emit('updateBadge') + } + + // should update the status of the tx to 'rejected'. + setTxStatusRejected (txId) { + this._setTxStatus(txId, 'rejected') + this.emit('updateBadge') + } + + setTxStatusConfirmed (txId) { + this._setTxStatus(txId, 'confirmed') + } + + // merges txParams obj onto txData.txParams + // use extend to ensure that all fields are filled + updateTxParams (txId, txParams) { + var txMeta = this.getTx(txId) + txMeta.txParams = extend(txMeta.txParams, txParams) + this.updateTx(txMeta) + } + + // checks if a signed tx is in a block and + // if included sets the tx status as 'confirmed' + checkForTxInBlock () { + var signedTxList = this.getFilteredTxList({status: 'signed', err: undefined}) + if (!signedTxList.length) return + signedTxList.forEach((tx) => { + var txHash = tx.hash + var txId = tx.id + if (!txHash) return + this.txProviderUtils.query.getTransactionByHash(txHash, (err, txMeta) => { + if (err || !txMeta) { + tx.err = err || 'Tx could possibly have not been submitted' + this.updateTx(tx) + return txMeta ? console.error(err) : console.debug(`txMeta is ${txMeta} for:`, tx) + } + if (txMeta.blockNumber) { + this.setTxStatusConfirmed(txId) + } + }) + }) + } + + // PRIVATE METHODS + + // Should find the tx in the tx list and + // update it. + // should set the status in txData + // - `'unapproved'` the user has not responded + // - `'rejected'` the user has responded no! + // - `'signed'` the tx is signed + // - `'submitted'` the tx is sent to a server + // - `'confirmed'` the tx has been included in a block. + _setTxStatus (txId, status) { + var txMeta = this.getTx(txId) + txMeta.status = status + this.emit(`${txMeta.id}:${status}`, txId) + this.updateTx(txMeta) + } + + // Saves the new/updated txList. + // Function is intended only for internal use + _saveTxList (txList) { + this.txList = txList + this._setTxList(txList) + } +} + + +const warn = () => console.warn('warn was used no cb provided') diff --git a/library/controller.js b/library/controller.js index 07419550a..5823287cc 100644 --- a/library/controller.js +++ b/library/controller.js @@ -21,7 +21,7 @@ function initializeZeroClient() { // User confirmation callbacks: showUnconfirmedMessage, unlockAccountMessage, - showUnconfirmedTx, + showUnapprovedTx, // Persistence Methods: setData, loadData, @@ -36,8 +36,8 @@ function initializeZeroClient() { console.log('notif stub - showUnconfirmedMessage') } - function showUnconfirmedTx (txParams, txData, onTxDoneCb) { - console.log('notif stub - showUnconfirmedTx') + function showUnapprovedTx (txParams, txData, onTxDoneCb) { + console.log('notif stub - showUnapprovedTx') } // @@ -156,4 +156,4 @@ function initializeZeroClient() { } } -} \ No newline at end of file +} diff --git a/mock-dev.js b/mock-dev.js index 84f2d234a..283bc2c79 100644 --- a/mock-dev.js +++ b/mock-dev.js @@ -46,7 +46,7 @@ const controller = new MetamaskController({ // User confirmation callbacks: showUnconfirmedMessage: noop, unlockAccountMessage: noop, - showUnconfirmedTx: noop, + showUnapprovedTx: noop, // Persistence Methods: setData, loadData, diff --git a/test/lib/mock-config-manager.js b/test/lib/mock-config-manager.js index ccd518c68..b79f63090 100644 --- a/test/lib/mock-config-manager.js +++ b/test/lib/mock-config-manager.js @@ -9,6 +9,7 @@ module.exports = function() { function loadData () { var oldData = getOldStyleData() var newData + try { newData = JSON.parse(window.localStorage[STORAGE_KEY]) } catch (e) {} diff --git a/test/unit/config-manager-test.js b/test/unit/config-manager-test.js index 477188b67..075abbe65 100644 --- a/test/unit/config-manager-test.js +++ b/test/unit/config-manager-test.js @@ -215,7 +215,7 @@ describe('config-manager', function() { describe('transactions', function() { beforeEach(function() { - configManager._saveTxList([]) + configManager.setTxList([]) }) describe('#getTxList', function() { @@ -226,90 +226,13 @@ describe('config-manager', function() { }) }) - describe('#_saveTxList', function() { + describe('#setTxList', function() { it('saves the submitted data to the tx list', function() { var target = [{ foo: 'bar' }] - configManager._saveTxList(target) + configManager.setTxList(target) var result = configManager.getTxList() assert.equal(result[0].foo, 'bar') }) }) - - describe('#addTx', function() { - it('adds a tx returned in getTxList', function() { - var tx = { id: 1 } - configManager.addTx(tx) - var result = configManager.getTxList() - assert.ok(Array.isArray(result)) - assert.equal(result.length, 1) - assert.equal(result[0].id, 1) - }) - - it('cuts off early txs beyond a limit', function() { - const limit = configManager.txLimit - for (let i = 0; i < limit + 1; i++) { - let tx = { id: i } - configManager.addTx(tx) - } - var result = configManager.getTxList() - assert.equal(result.length, limit, `limit of ${limit} txs enforced`) - assert.equal(result[0].id, 1, 'early txs truncted') - }) - }) - - describe('#confirmTx', function() { - it('sets the tx status to confirmed', function() { - var tx = { id: 1, status: 'unconfirmed' } - configManager.addTx(tx) - configManager.confirmTx(1) - var result = configManager.getTxList() - assert.ok(Array.isArray(result)) - assert.equal(result.length, 1) - assert.equal(result[0].status, 'confirmed') - }) - }) - - describe('#rejectTx', function() { - it('sets the tx status to rejected', function() { - var tx = { id: 1, status: 'unconfirmed' } - configManager.addTx(tx) - configManager.rejectTx(1) - var result = configManager.getTxList() - assert.ok(Array.isArray(result)) - assert.equal(result.length, 1) - assert.equal(result[0].status, 'rejected') - }) - }) - - describe('#updateTx', function() { - it('replaces the tx with the same id', function() { - configManager.addTx({ id: '1', status: 'unconfirmed' }) - configManager.addTx({ id: '2', status: 'confirmed' }) - configManager.updateTx({ id: '1', status: 'blah', hash: 'foo' }) - var result = configManager.getTx('1') - assert.equal(result.hash, 'foo') - }) - }) - - describe('#unconfirmedTxs', function() { - it('returns unconfirmed txs in a hash', function() { - configManager.addTx({ id: '1', status: 'unconfirmed' }) - configManager.addTx({ id: '2', status: 'confirmed' }) - let result = configManager.unconfirmedTxs() - assert.equal(typeof result, 'object') - assert.equal(result['1'].status, 'unconfirmed') - assert.equal(result['0'], undefined) - assert.equal(result['2'], undefined) - }) - }) - - describe('#getTx', function() { - it('returns a tx with the requested id', function() { - configManager.addTx({ id: '1', status: 'unconfirmed' }) - configManager.addTx({ id: '2', status: 'confirmed' }) - assert.equal(configManager.getTx('1').status, 'unconfirmed') - assert.equal(configManager.getTx('2').status, 'confirmed') - }) - }) }) }) diff --git a/test/unit/idStore-migration-test.js b/test/unit/idStore-migration-test.js index ad4ce2096..54f38fb2f 100644 --- a/test/unit/idStore-migration-test.js +++ b/test/unit/idStore-migration-test.js @@ -68,6 +68,10 @@ describe('IdentityStore to KeyringController migration', function() { addAccount(acct) { newAccounts.push(ethUtil.addHexPrefix(acct)) }, del(acct) { delete newAccounts[acct] }, }, + txManager: { + getTxList: () => [], + getUnapprovedTxList: () => [] + }, }) // Stub out the browser crypto for a mock encryptor. diff --git a/test/unit/idStore-test.js b/test/unit/idStore-test.js index 3ca89cd38..000c58a82 100644 --- a/test/unit/idStore-test.js +++ b/test/unit/idStore-test.js @@ -139,54 +139,4 @@ describe('IdentityStore', function() { }) }) }) - - describe('#addGasBuffer', function() { - it('formats the result correctly', function() { - const idStore = new IdentityStore({ - configManager: configManagerGen(), - ethStore: { - addAccount(acct) { accounts.push(ethUtil.addHexPrefix(acct)) }, - }, - }) - - const gas = '0x01' - const bnGas = new BN(gas, 16) - const bnResult = idStore.addGasBuffer(bnGas) - - assert.ok(bnResult.gt(gas), 'added more gas as buffer.') - }) - - it('buffers 20%', function() { - const idStore = new IdentityStore({ - configManager: configManagerGen(), - ethStore: { - addAccount(acct) { accounts.push(ethUtil.addHexPrefix(acct)) }, - }, - }) - - const gas = '0x04ee59' // Actual estimated gas example - const bnGas = new BN(ethUtil.stripHexPrefix(gas), 16) - const five = new BN('5', 10) - const correctBuffer = bnGas.div(five) - const correct = bnGas.add(correctBuffer) - - const bnResult = idStore.addGasBuffer(bnGas) - - assert(bnResult.gt(bnGas), 'Estimate increased in value.') - assert.equal(bnResult.sub(bnGas).toString(10), correctBuffer.toString(10), 'added 20% gas') - }) - }) - - describe('#checkForDelegateCall', function() { - const idStore = new IdentityStore({ - configManager: configManagerGen(), - ethStore: { - addAccount(acct) { accounts.push(ethUtil.addHexPrefix(acct)) }, - }, - }) - - var result = idStore.checkForDelegateCall(delegateCallCode) - assert.equal(result, true, 'no delegate call in provided code') - }) - }) diff --git a/test/unit/keyring-controller-test.js b/test/unit/keyring-controller-test.js index 8bb1126fa..37fd7175e 100644 --- a/test/unit/keyring-controller-test.js +++ b/test/unit/keyring-controller-test.js @@ -23,6 +23,10 @@ describe('KeyringController', function() { keyringController = new KeyringController({ configManager: configManagerGen(), + txManager: { + getTxList: () => [], + getUnapprovedTxList: () => [] + }, ethStore: { addAccount(acct) { accounts.push(ethUtil.addHexPrefix(acct)) }, }, diff --git a/test/unit/metamask-controller-test.js b/test/unit/metamask-controller-test.js index b87169ca2..414610404 100644 --- a/test/unit/metamask-controller-test.js +++ b/test/unit/metamask-controller-test.js @@ -9,7 +9,7 @@ describe('MetaMaskController', function() { let controller = new MetaMaskController({ showUnconfirmedMessage: noop, unlockAccountMessage: noop, - showUnconfirmedTx: noop, + showUnapprovedTx: noop, setData, loadData, }) diff --git a/test/unit/tx-manager-test.js b/test/unit/tx-manager-test.js new file mode 100644 index 000000000..be16facad --- /dev/null +++ b/test/unit/tx-manager-test.js @@ -0,0 +1,190 @@ +const assert = require('assert') +const extend = require('xtend') +const EventEmitter = require('events') +const STORAGE_KEY = 'metamask-persistance-key' +const TransactionManager = require('../../app/scripts/transaction-manager') + +describe('Transaction Manager', function() { + let txManager + + const onTxDoneCb = () => true + beforeEach(function() { + txManager = new TransactionManager ({ + txList: [], + setTxList: () => {}, + provider: "testnet", + txHistoryLimit: 10, + blockTracker: new EventEmitter(), + }) + }) + + describe('#getTxList', function() { + it('when new should return empty array', function() { + var result = txManager.getTxList() + assert.ok(Array.isArray(result)) + assert.equal(result.length, 0) + }) + it('should also return transactions from local storage if any', function() { + + }) + }) + + describe('#_saveTxList', function() { + it('saves the submitted data to the tx list', function() { + var target = [{ foo: 'bar' }] + txManager._saveTxList(target) + var result = txManager.getTxList() + assert.equal(result[0].foo, 'bar') + }) + }) + + describe('#addTx', function() { + it('adds a tx returned in getTxList', function() { + var tx = { id: 1, status: 'confirmed',} + txManager.addTx(tx, onTxDoneCb) + var result = txManager.getTxList() + assert.ok(Array.isArray(result)) + assert.equal(result.length, 1) + assert.equal(result[0].id, 1) + }) + + it('cuts off early txs beyond a limit', function() { + const limit = txManager.txHistoryLimit + for (let i = 0; i < limit + 1; i++) { + let tx = { id: i, time: new Date(), status: 'confirmed'} + txManager.addTx(tx, onTxDoneCb) + } + var result = txManager.getTxList() + assert.equal(result.length, limit, `limit of ${limit} txs enforced`) + assert.equal(result[0].id, 1, 'early txs truncted') + }) + + it('cuts off early txs beyond a limit weather or not it is confirmed or rejected', function() { + const limit = txManager.txHistoryLimit + for (let i = 0; i < limit + 1; i++) { + let tx = { id: i, time: new Date(), status: 'rejected'} + txManager.addTx(tx, onTxDoneCb) + } + var result = txManager.getTxList() + assert.equal(result.length, limit, `limit of ${limit} txs enforced`) + assert.equal(result[0].id, 1, 'early txs truncted') + }) + + it('cuts off early txs beyond a limit but does not cut unapproved txs', function() { + var unconfirmedTx = { id: 0, time: new Date(), status: 'unapproved'} + txManager.addTx(unconfirmedTx, onTxDoneCb) + const limit = txManager.txHistoryLimit + for (let i = 1; i < limit + 1; i++) { + let tx = { id: i, time: new Date(), status: 'confirmed'} + txManager.addTx(tx, onTxDoneCb) + } + var result = txManager.getTxList() + assert.equal(result.length, limit, `limit of ${limit} txs enforced`) + assert.equal(result[0].id, 0, 'first tx should still be there') + assert.equal(result[0].status, 'unapproved', 'first tx should be unapproved') + assert.equal(result[1].id, 2, 'early txs truncted') + }) + }) + + describe('#setTxStatusSigned', function() { + it('sets the tx status to signed', function() { + var tx = { id: 1, status: 'unapproved' } + txManager.addTx(tx, onTxDoneCb) + txManager.setTxStatusSigned(1) + var result = txManager.getTxList() + assert.ok(Array.isArray(result)) + assert.equal(result.length, 1) + assert.equal(result[0].status, 'signed') + }) + + it('should emit a signed event to signal the exciton of callback', (done) => { + this.timeout(10000) + var tx = { id: 1, status: 'unapproved' } + let onTxDoneCb = function (err, txId) { + assert(true, 'event listener has been triggered and onTxDoneCb executed') + done() + } + txManager.addTx(tx, onTxDoneCb) + txManager.setTxStatusSigned(1) + }) + }) + + describe('#setTxStatusRejected', function() { + it('sets the tx status to rejected', function() { + var tx = { id: 1, status: 'unapproved' } + txManager.addTx(tx, onTxDoneCb) + txManager.setTxStatusRejected(1) + var result = txManager.getTxList() + assert.ok(Array.isArray(result)) + assert.equal(result.length, 1) + assert.equal(result[0].status, 'rejected') + }) + + it('should emit a rejected event to signal the exciton of callback', (done) => { + this.timeout(10000) + var tx = { id: 1, status: 'unapproved' } + let onTxDoneCb = function (err, txId) { + assert(true, 'event listener has been triggered and onTxDoneCb executed') + done() + } + txManager.addTx(tx, onTxDoneCb) + txManager.setTxStatusRejected(1) + }) + + }) + + describe('#updateTx', function() { + it('replaces the tx with the same id', function() { + txManager.addTx({ id: '1', status: 'unapproved' }, onTxDoneCb) + txManager.addTx({ id: '2', status: 'confirmed' }, onTxDoneCb) + txManager.updateTx({ id: '1', status: 'blah', hash: 'foo' }) + var result = txManager.getTx('1') + assert.equal(result.hash, 'foo') + }) + }) + + describe('#getUnapprovedTxList', function() { + it('returns unapproved txs in a hash', function() { + txManager.addTx({ id: '1', status: 'unapproved' }, onTxDoneCb) + txManager.addTx({ id: '2', status: 'confirmed' }, onTxDoneCb) + let result = txManager.getUnapprovedTxList() + assert.equal(typeof result, 'object') + assert.equal(result['1'].status, 'unapproved') + assert.equal(result['2'], undefined) + }) + }) + + describe('#getTx', function() { + it('returns a tx with the requested id', function() { + txManager.addTx({ id: '1', status: 'unapproved' }, onTxDoneCb) + txManager.addTx({ id: '2', status: 'confirmed' }, onTxDoneCb) + assert.equal(txManager.getTx('1').status, 'unapproved') + assert.equal(txManager.getTx('2').status, 'confirmed') + }) + }) + + describe('#getFilteredTxList', function() { + it('returns a tx with the requested data', function() { + var foop = 0 + var zoop = 0 + for (let i = 0; i < 10; ++i ){ + let everyOther = i % 2 + txManager.addTx({ id: i, + status: everyOther ? 'unapproved' : 'confirmed', + txParams: { + from: everyOther ? 'foop' : 'zoop', + to: everyOther ? 'zoop' : 'foop', + } + }, onTxDoneCb) + everyOther ? ++foop : ++zoop + } + assert.equal(txManager.getFilteredTxList({status: 'confirmed', from: 'zoop'}).length, zoop) + assert.equal(txManager.getFilteredTxList({status: 'confirmed', to: 'foop'}).length, zoop) + assert.equal(txManager.getFilteredTxList({status: 'confirmed', from: 'foop'}).length, 0) + assert.equal(txManager.getFilteredTxList({status: 'confirmed'}).length, zoop) + assert.equal(txManager.getFilteredTxList({from: 'foop'}).length, foop) + assert.equal(txManager.getFilteredTxList({from: 'zoop'}).length, zoop) + }) + }) + +}) diff --git a/ui/app/components/transaction-list-item.js b/ui/app/components/transaction-list-item.js index d1306549e..bb685abda 100644 --- a/ui/app/components/transaction-list-item.js +++ b/ui/app/components/transaction-list-item.js @@ -31,7 +31,7 @@ TransactionListItem.prototype.render = function () { var isMsg = ('msgParams' in transaction) var isTx = ('txParams' in transaction) - var isPending = transaction.status === 'unconfirmed' + var isPending = transaction.status === 'unapproved' let txParams if (isTx) { @@ -59,7 +59,7 @@ TransactionListItem.prototype.render = function () { }, [ h('.identicon-wrapper.flex-column.flex-center.select-none', [ - transaction.status === 'unconfirmed' ? h('i.fa.fa-ellipsis-h', { + transaction.status === 'unapproved' ? h('i.fa.fa-ellipsis-h', { style: { fontSize: '27px', },