diff --git a/CHANGELOG.md b/CHANGELOG.md index e74a095dd..6a4e7fd3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Current Master +- Pending transactions are now persisted to localStorage and resume even after browser is closed. +- Completed transactions are now persisted and can be displayed via UI. +- Fix bug on config screen where current RPC address was always displayed wrong. + # 1.5.1 2016-04-15 - Corrected text above account list. Selected account is visible to all sites, not just the current domain. diff --git a/README.md b/README.md index 1d15fa204..1c5508a49 100644 --- a/README.md +++ b/README.md @@ -44,18 +44,11 @@ To enjoy the live-reloading that `gulp dev` offers while working on the `web3-pr ### Running Tests -Currently the tests are split between two suites (we recently merged the UI into the main plugin repository). There are two different test suites to be concerned with: - -Plugin tests, `npm test`. -UI tests, `npm run testUi`. - -You can also run both of these with continuously watching processes, via `npm run watch` and `npm run watchUi`. - -#### UI Testing Particulars - Requires `mocha` installed. Run `npm install -g mocha`. -You can either run the test suite once with `npm testUi`, or you can reload on file changes, by running `mocha watch ui/test/**/**`. +Then just run `npm test`. + +You can also test with a continuously watching process, via `npm run watch`. ### Deploying the UI diff --git a/app/scripts/background.js b/app/scripts/background.js index db4927083..1519f63db 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -191,7 +191,8 @@ idStore.on('update', updateBadge) function updateBadge(state){ var label = '' - var count = Object.keys(state.unconfTxs).length + var unconfTxs = configManager.unconfirmedTxs() + var count = Object.keys(unconfTxs).length if (count) { label = String(count) } diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 105f24988..a256a3f5b 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -7,7 +7,8 @@ var scriptTag = document.createElement('script') scriptTag.src = chrome.extension.getURL('scripts/inpage.js') scriptTag.onload = function() { this.parentNode.removeChild(this) } var container = document.head || document.documentElement -container.appendChild(scriptTag) +// append as first child +container.insertBefore(scriptTag, container.children[0]) // setup communication to page and plugin var pageStream = new LocalMessageDuplexStream({ diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js index f024729cc..356d53c22 100644 --- a/app/scripts/lib/config-manager.js +++ b/app/scripts/lib/config-manager.js @@ -134,6 +134,56 @@ ConfigManager.prototype.setData = function(data) { this.migrator.saveData(data) } +ConfigManager.prototype.getTxList = function() { + var data = this.migrator.getData() + if ('transactions' in data) { + return data.transactions + } else { + return [] + } +} + +ConfigManager.prototype._saveTxList = function(txList) { + var data = this.migrator.getData() + data.transactions = txList + this.setData(data) +} + +ConfigManager.prototype.addTx = function(tx) { + var transactions = this.getTxList() + 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 transactions = this.getTxList() + transactions.forEach((tx) => { + if (tx.id === txId) { + tx.status = status + } + }) + this._saveTxList(transactions) +} +ConfigManager.prototype.unconfirmedTxs = function() { + var transactions = this.getTxList() + return transactions.filter(tx => tx.status === 'unconfirmed') + .reduce((result, tx) => { result[tx.id] = tx; return result }, {}) +} + // observable ConfigManager.prototype.subscribe = function(fn){ diff --git a/app/scripts/lib/idStore.js b/app/scripts/lib/idStore.js index f44300273..b451fd6d4 100644 --- a/app/scripts/lib/idStore.js +++ b/app/scripts/lib/idStore.js @@ -31,7 +31,6 @@ function IdentityStore(ethStore) { this._currentState = { selectedAddress: null, identities: {}, - unconfTxs: {}, } // not part of serilized metamask state - only kept in memory this._unconfTxCbs = {} @@ -83,6 +82,8 @@ IdentityStore.prototype.getState = function(){ isInitialized: !!configManager.getWallet() && !seedWords, isUnlocked: this._isUnlocked(), seedWords: seedWords, + unconfTxs: configManager.unconfirmedTxs(), + transactions: configManager.getTxList(), })) } @@ -140,10 +141,11 @@ IdentityStore.prototype.addUnconfirmedTransaction = function(txParams, cb){ time: time, status: 'unconfirmed', } - this._currentState.unconfTxs[txId] = txData + configManager.addTx(txData) console.log('addUnconfirmedTransaction:', txData) // keep the cb around for after approval (requires user interaction) + // This cb fires completion to the Dapp's write operation. this._unconfTxCbs[txId] = cb // signal update @@ -154,7 +156,7 @@ IdentityStore.prototype.addUnconfirmedTransaction = function(txParams, cb){ // comes from metamask ui IdentityStore.prototype.approveTransaction = function(txId, cb){ - var txData = this._currentState.unconfTxs[txId] + var txData = configManager.getTx(txId) var txParams = txData.txParams var approvalCb = this._unconfTxCbs[txId] || noop @@ -162,20 +164,20 @@ IdentityStore.prototype.approveTransaction = function(txId, cb){ cb() approvalCb(null, true) // clean up - delete this._currentState.unconfTxs[txId] + configManager.confirmTx(txId) delete this._unconfTxCbs[txId] this._didUpdate() } // comes from metamask ui IdentityStore.prototype.cancelTransaction = function(txId){ - var txData = this._currentState.unconfTxs[txId] + var txData = configManager.getTx(txId) var approvalCb = this._unconfTxCbs[txId] || noop // reject tx approvalCb(null, false) // clean up - delete this._currentState.unconfTxs[txId] + configManager.rejectTx(txId) delete this._unconfTxCbs[txId] this._didUpdate() } diff --git a/package.json b/package.json index db0e2823a..b2e2b3c61 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,8 @@ "private": true, "scripts": { "start": "gulp dev", - "test": "npm run testUi", - "testPlugin": "mocha --require test/helper.js --compilers js:babel-register --recursive", - "watch": "mocha watch --compilers js:babel-register --recursive", - "testUi": "mocha ui/test/**/**/*test.js", - "watchUi": "mocha watch ui/test/**/*test.js" + "test": "mocha --require test/helper.js --compilers js:babel-register --recursive", + "watch": "mocha watch --compilers js:babel-register --recursive" }, "browserify": { "transform": [ @@ -35,19 +32,19 @@ "eth-store": "^1.1.0", "ethereumjs-tx": "^1.0.0", "ethereumjs-util": "^4.3.0", - "faux-jax": "git+https://github.com/kumavis/faux-jax.git#c3648de04804f3895c5b4972750cae5b51ddb103", "hat": "0.0.3", "inject-css": "^0.1.1", "metamask-logo": "^1.1.5", "multiplex": "^6.7.0", "pojo-migrator": "^2.1.0", + "polyfill-crypto.getrandomvalues": "^1.0.0", "pumpify": "^1.3.4", "react": "^0.14.3", "react-addons-css-transition-group": "^0.14.7", "react-dom": "^0.14.3", "react-hyperscript": "^2.2.2", - "readable-stream": "^2.0.5", "react-redux": "^4.0.3", + "readable-stream": "^2.0.5", "redux": "^3.0.5", "redux-logger": "^2.3.1", "redux-thunk": "^1.0.2", @@ -55,7 +52,7 @@ "three.js": "^0.73.2", "through2": "^2.0.1", "web3": "^0.15.1", - "web3-provider-engine": "^7.2.1", + "web3-provider-engine": "^7.4.0", "xtend": "^4.0.1" }, "devDependencies": { diff --git a/test/helper.js b/test/helper.js index 4c7f8b4c6..64fe5bd07 100644 --- a/test/helper.js +++ b/test/helper.js @@ -1,2 +1,5 @@ require('jsdom-global')() window.localStorage = {} + +if (!('crypto' in window)) { window.crypto = {} } +window.crypto.getRandomValues = require('polyfill-crypto.getrandomvalues') diff --git a/ui/test/unit/actions/config_test.js b/test/unit/actions/config_test.js similarity index 84% rename from ui/test/unit/actions/config_test.js rename to test/unit/actions/config_test.js index d38210bfc..6a0d20f31 100644 --- a/ui/test/unit/actions/config_test.js +++ b/test/unit/actions/config_test.js @@ -3,8 +3,8 @@ var assert = require('assert') var freeze = require('deep-freeze-strict') var path = require('path') -var actions = require(path.join(__dirname, '..', '..', '..', 'app', 'actions.js')) -var reducers = require(path.join(__dirname, '..', '..', '..', 'app', 'reducers.js')) +var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) +var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) describe ('config view actions', function() { diff --git a/ui/test/unit/actions/restore_vault_test.js b/test/unit/actions/restore_vault_test.js similarity index 89% rename from ui/test/unit/actions/restore_vault_test.js rename to test/unit/actions/restore_vault_test.js index da0d71ce7..5873a0181 100644 --- a/ui/test/unit/actions/restore_vault_test.js +++ b/test/unit/actions/restore_vault_test.js @@ -4,8 +4,8 @@ var freeze = require('deep-freeze-strict') var path = require('path') var sinon = require('sinon') -var actions = require(path.join(__dirname, '..', '..', '..', 'app', 'actions.js')) -var reducers = require(path.join(__dirname, '..', '..', '..', 'app', 'reducers.js')) +var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) +var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) describe('#recoverFromSeed(password, seed)', function() { diff --git a/ui/test/unit/actions/set_selected_account_test.js b/test/unit/actions/set_selected_account_test.js similarity index 78% rename from ui/test/unit/actions/set_selected_account_test.js rename to test/unit/actions/set_selected_account_test.js index 1af6c964f..0487bc5f0 100644 --- a/ui/test/unit/actions/set_selected_account_test.js +++ b/test/unit/actions/set_selected_account_test.js @@ -3,8 +3,8 @@ var assert = require('assert') var freeze = require('deep-freeze-strict') var path = require('path') -var actions = require(path.join(__dirname, '..', '..', '..', 'app', 'actions.js')) -var reducers = require(path.join(__dirname, '..', '..', '..', 'app', 'reducers.js')) +var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) +var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) describe('SET_SELECTED_ACCOUNT', function() { diff --git a/ui/test/unit/actions/tx_test.js b/test/unit/actions/tx_test.js similarity index 96% rename from ui/test/unit/actions/tx_test.js rename to test/unit/actions/tx_test.js index d83ae16c0..b15bee393 100644 --- a/ui/test/unit/actions/tx_test.js +++ b/test/unit/actions/tx_test.js @@ -3,8 +3,8 @@ var assert = require('assert') var freeze = require('deep-freeze-strict') var path = require('path') -var actions = require(path.join(__dirname, '..', '..', '..', 'app', 'actions.js')) -var reducers = require(path.join(__dirname, '..', '..', '..', 'app', 'reducers.js')) +var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) +var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) describe('tx confirmation screen', function() { var initialState, result diff --git a/ui/test/unit/actions/view_info_test.js b/test/unit/actions/view_info_test.js similarity index 75% rename from ui/test/unit/actions/view_info_test.js rename to test/unit/actions/view_info_test.js index 888712c67..0558c6e42 100644 --- a/ui/test/unit/actions/view_info_test.js +++ b/test/unit/actions/view_info_test.js @@ -3,8 +3,8 @@ var assert = require('assert') var freeze = require('deep-freeze-strict') var path = require('path') -var actions = require(path.join(__dirname, '..', '..', '..', 'app', 'actions.js')) -var reducers = require(path.join(__dirname, '..', '..', '..', 'app', 'reducers.js')) +var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) +var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) describe('SHOW_INFO_PAGE', function() { diff --git a/ui/test/unit/actions/warning_test.js b/test/unit/actions/warning_test.js similarity index 76% rename from ui/test/unit/actions/warning_test.js rename to test/unit/actions/warning_test.js index eee198656..37be9ee85 100644 --- a/ui/test/unit/actions/warning_test.js +++ b/test/unit/actions/warning_test.js @@ -3,8 +3,8 @@ var assert = require('assert') var freeze = require('deep-freeze-strict') var path = require('path') -var actions = require(path.join(__dirname, '..', '..', '..', 'app', 'actions.js')) -var reducers = require(path.join(__dirname, '..', '..', '..', 'app', 'reducers.js')) +var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) +var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) describe('action DISPLAY_WARNING', function() { diff --git a/test/unit/config-manager-test.js b/test/unit/config-manager-test.js index 10b6716d6..84632b0ea 100644 --- a/test/unit/config-manager-test.js +++ b/test/unit/config-manager-test.js @@ -68,4 +68,83 @@ describe('config-manager', function() { assert.equal(secondResult, secondRpc) }) }) + + describe('transactions', function() { + beforeEach(function() { + configManager._saveTxList([]) + }) + + describe('#getTxList', function() { + it('when new should return empty array', function() { + var result = configManager.getTxList() + assert.ok(Array.isArray(result)) + assert.equal(result.length, 0) + }) + }) + + describe('#_saveTxList', function() { + it('saves the submitted data to the tx list', function() { + var target = [{ foo: 'bar' }] + configManager._saveTxList(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) + }) + }) + + 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('#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/migrations-test.js b/test/unit/migrations-test.js index 3a3213ac5..092c0eccd 100644 --- a/test/unit/migrations-test.js +++ b/test/unit/migrations-test.js @@ -1,14 +1,16 @@ -var test = require('tape') +var assert = require('assert') var path = require('path') var wallet1 = require(path.join('..', 'lib', 'migrations', '001.json')) var migration2 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '002')) -test('wallet1 is migrated successfully', function(t) { - - var result = migration2.migrate(wallet1.data) - t.equal(result.config.provider.type, 'rpc', 'provider should be rpc') - t.equal(result.config.provider.rpcTarget, 'https://rpc.metamask.io/', 'provider should be our rpc') +describe('wallet1 is migrated successfully', function() { + it('should convert etherscan provider', function(done) { + var result = migration2.migrate(wallet1.data) + assert.equal(result.config.provider.type, 'rpc', 'provider should be rpc') + assert.equal(result.config.provider.rpcTarget, 'https://rpc.metamask.io/', 'provider should be our rpc') + done() + }) }) diff --git a/ui/test/unit/util_test.js b/test/unit/util_test.js similarity index 97% rename from ui/test/unit/util_test.js rename to test/unit/util_test.js index b35d60812..7f8103d3b 100644 --- a/ui/test/unit/util_test.js +++ b/test/unit/util_test.js @@ -3,7 +3,7 @@ var sinon = require('sinon') const ethUtil = require('ethereumjs-util') var path = require('path') -var util = require(path.join(__dirname, '..', '..', 'app', 'util.js')) +var util = require(path.join(__dirname, '..', '..', 'ui', 'app', 'util.js')) describe('util', function() { var ethInWei = '1' diff --git a/ui/app/config.js b/ui/app/config.js index 33d87bcc2..878c9955f 100644 --- a/ui/app/config.js +++ b/ui/app/config.js @@ -47,7 +47,6 @@ ConfigScreen.prototype.render = function() { currentProviderDisplay(metamaskState), - h('div', [ h('input', { placeholder: 'New RPC URL', @@ -95,7 +94,7 @@ ConfigScreen.prototype.render = function() { } function currentProviderDisplay(metamaskState) { - var rpc = metamaskState.rpcTarget + var rpc = metamaskState.provider.rpcTarget return h('div', [ h('h3', {style: { fontWeight: 'bold' }}, 'Currently using RPC'), h('p', rpc) diff --git a/ui/test/setup.js b/ui/test/setup.js deleted file mode 100644 index 7985e9a00..000000000 --- a/ui/test/setup.js +++ /dev/null @@ -1,8 +0,0 @@ -if (typeof process === 'object') { - // Initialize node environment - global.expect = require('chai').expect - require('mocha-jsdom')() -} else { - window.expect = window.chai.expect - window.require = function () { /* noop */ } -}