diff --git a/CHANGELOG.md b/CHANGELOG.md index e6835e7bc..f30c04985 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,34 @@ ## Current Master +- Add ability to export private keys as a file. +- Add ability to export seed words as a file. +- Changed state logs to a file download than a clipboard copy. +- Fix link to support center. + +## 3.10.0 2017-9-11 + +- Readded loose keyring label back into the account list. +- Remove cryptonator from chrome permissions. +- Add info on token contract addresses. +- Add validation preventing users from inputting their own addresses as token tracking addresses. +- Added button to reject all transactions (thanks to davidp94! https://github.com/davidp94) + +## 3.9.13 2017-9-8 + +- Changed the way we initialize the inpage provider to fix a bug affecting some developers. + +## 3.9.12 2017-9-6 + +- Fix bug that prevented Web3 1.0 compatibility - Make eth_sign deprecation warning less noisy +- Add useful link to eth_sign deprecation warning. - Fix bug with network version serialization over synchronous RPC - Add MetaMask version to state logs. - Add the total amount of tokens when multiple tokens are added under the token list - Use HTTPS links for Etherscan. - Update Support center link to new one with HTTPS. +- Make web3 deprecation notice more useful by linking to a descriptive article. ## 3.9.11 2017-8-24 diff --git a/app/manifest.json b/app/manifest.json index 7ec32ea78..bd25c1f6f 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.9.11", + "version": "3.10.0", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", @@ -57,9 +57,8 @@ "permissions": [ "storage", "clipboardWrite", - "http://localhost:8545/", - "https://api.cryptonator.com/" - ], + "http://localhost:8545/" + ], "web_accessible_resources": [ "scripts/inpage.js" ], diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js index 2edc8060e..fd57fac70 100644 --- a/app/scripts/keyring-controller.js +++ b/app/scripts/keyring-controller.js @@ -171,9 +171,9 @@ class KeyringController extends EventEmitter { return this.setupAccounts(checkedAccounts) }) .then(() => this.persistAllKeyrings()) + .then(() => this._updateMemStoreKeyrings()) .then(() => this.fullUpdate()) .then(() => { - this._updateMemStoreKeyrings() return keyring }) } @@ -208,6 +208,7 @@ class KeyringController extends EventEmitter { return selectedKeyring.addAccounts(1) .then(this.setupAccounts.bind(this)) .then(this.persistAllKeyrings.bind(this)) + .then(this._updateMemStoreKeyrings.bind(this)) .then(this.fullUpdate.bind(this)) } diff --git a/app/scripts/lib/auto-reload.js b/app/scripts/lib/auto-reload.js index 62aaf405c..cce31c3d2 100644 --- a/app/scripts/lib/auto-reload.js +++ b/app/scripts/lib/auto-reload.js @@ -11,7 +11,7 @@ function setupDappAutoReload (web3, observable) { get: (_web3, key) => { // show warning once on web3 access if (!hasBeenWarned && key !== 'currentProvider') { - console.warn('MetaMask: web3 will be deprecated in the near future in favor of the ethereumProvider \nhttps://github.com/ethereum/mist/releases/tag/v0.9.0') + console.warn('MetaMask: web3 will be deprecated in the near future in favor of the ethereumProvider \nhttps://github.com/MetaMask/faq/blob/master/detecting_metamask.md#web3-deprecation') hasBeenWarned = true } // get the time of use diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js index 7c74a62d3..13888dc67 100644 --- a/app/scripts/lib/inpage-provider.js +++ b/app/scripts/lib/inpage-provider.js @@ -40,30 +40,37 @@ function MetamaskInpageProvider (connectionStream) { // start and stop polling to unblock first block lock self.idMap = {} - // handle sendAsync requests via asyncProvider - self.sendAsync = function (payload, cb) { - // rewrite request ids - var request = eachJsonMessage(payload, (message) => { - var newId = createRandomId() - self.idMap[newId] = message.id - message.id = newId +} + +// handle sendAsync requests via asyncProvider +// also remap ids inbound and outbound +MetamaskInpageProvider.prototype.sendAsync = function (payload, cb) { + const self = this + + // rewrite request ids + const request = eachJsonMessage(payload, (_message) => { + const message = Object.assign({}, _message) + const newId = createRandomId() + self.idMap[newId] = message.id + message.id = newId + return message + }) + + // forward to asyncProvider + self.asyncProvider.sendAsync(request, (err, _res) => { + if (err) return cb(err) + // transform messages to original ids + const res = eachJsonMessage(_res, (message) => { + const oldId = self.idMap[message.id] + delete self.idMap[message.id] + message.id = oldId return message }) - // forward to asyncProvider - asyncProvider.sendAsync(request, function (err, res) { - if (err) return cb(err) - // transform messages to original ids - eachJsonMessage(res, (message) => { - var oldId = self.idMap[message.id] - delete self.idMap[message.id] - message.id = oldId - return message - }) - cb(null, res) - }) - } + cb(null, res) + }) } + MetamaskInpageProvider.prototype.send = function (payload) { const self = this @@ -109,10 +116,6 @@ MetamaskInpageProvider.prototype.send = function (payload) { } } -MetamaskInpageProvider.prototype.sendAsync = function () { - throw new Error('MetamaskInpageProvider - sendAsync not overwritten') -} - MetamaskInpageProvider.prototype.isConnected = function () { return true } diff --git a/circle.yml b/circle.yml index 2ea60bb9d..f5da6857d 100644 --- a/circle.yml +++ b/circle.yml @@ -1,10 +1,17 @@ machine: node: version: 8.1.4 -dependencies: - pre: - - "npm i -g testem" - - "npm i -g mocha" test: override: - - "npm run ci" \ No newline at end of file + - "npm run ci" +dependencies: + pre: + - sudo apt-get update + # get latest stable firefox + - sudo apt-get install firefox + - firefox_cmd=`which firefox`; sudo rm -f $firefox_cmd; sudo ln -s `which firefox.ubuntu` $firefox_cmd + # get latest stable chrome + - wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - + - sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' + - sudo apt-get update + - sudo apt-get install google-chrome-stable \ No newline at end of file diff --git a/development/uiStore.js b/development/uiStore.js index 1299ee1dc..c71d66d3b 100644 --- a/development/uiStore.js +++ b/development/uiStore.js @@ -1,7 +1,7 @@ const createStore = require('redux').createStore const applyMiddleware = require('redux').applyMiddleware -const thunkMiddleware = require('redux-thunk') -const createLogger = require('redux-logger') +const thunkMiddleware = require('redux-thunk').default +const createLogger = require('redux-logger').createLogger const rootReducer = require('../ui/app/reducers') module.exports = configureStore diff --git a/docs/add-to-firef.md b/docs/add-to-firefox.md similarity index 100% rename from docs/add-to-firef.md rename to docs/add-to-firefox.md diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 000000000..8e6d55972 --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,61 @@ +// Karma configuration +// Generated on Mon Sep 11 2017 18:45:48 GMT-0700 (PDT) + +module.exports = function(config) { + config.set({ + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: process.cwd(), + + browserConsoleLogOptions: { + terminal: false, + }, + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['qunit'], + + // list of files / patterns to load in the browser + files: [ + 'development/bundle.js', + 'test/integration/jquery-3.1.0.min.js', + 'test/integration/bundle.js', + { pattern: 'dist/chrome/images/**/*.*', watched: false, included: false, served: true }, + { pattern: 'dist/chrome/fonts/**/*.*', watched: false, included: false, served: true }, + ], + + proxies: { + '/images/': '/base/dist/chrome/images/', + '/fonts/': '/base/dist/chrome/fonts/', + }, + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['progress'], + + // web server port + port: 9876, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: false, + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['Chrome', 'Firefox'], + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: true, + + // Concurrency level + // how many browser should be started simultaneous + concurrency: Infinity + }) +} diff --git a/mock-dev.js b/mock-dev.js index 8e1923a82..b6652bdf7 100644 --- a/mock-dev.js +++ b/mock-dev.js @@ -85,40 +85,47 @@ actions.update = function(stateName) { var css = MetaMaskUiCss() injectCss(css) -const container = document.querySelector('#app-content') - // parse opts var store = configureStore(firstState) // start app -render( - h('.super-dev-container', [ +startApp() - h('button', { - onClick: (ev) => { - ev.preventDefault() - store.dispatch(actions.update('terms')) - }, - style: { - margin: '19px 19px 0px 19px', - }, - }, 'Reset State'), +function startApp(){ + const body = document.body + const container = document.createElement('div') + container.id = 'app-content' + body.appendChild(container) + console.log('container', container) - h(Selector, { actions, selectedKey: selectedView, states, store }), + render( + h('.super-dev-container', [ - h('.mock-app-root', { - style: { - height: '500px', - width: '360px', - boxShadow: 'grey 0px 2px 9px', - margin: '20px', - }, - }, [ - h(Root, { - store: store, - }), - ]), + h('button', { + onClick: (ev) => { + ev.preventDefault() + store.dispatch(actions.update('terms')) + }, + style: { + margin: '19px 19px 0px 19px', + }, + }, 'Reset State'), - ] -), container) + h(Selector, { actions, selectedKey: selectedView, states, store }), + h('.mock-app-root', { + style: { + height: '500px', + width: '360px', + boxShadow: 'grey 0px 2px 9px', + margin: '20px', + }, + }, [ + h(Root, { + store: store, + }), + ]), + + ] + ), container) +} diff --git a/package.json b/package.json index 9afc181a3..12f79ba35 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,8 @@ "test": "npm run lint && npm run test-unit && npm run test-integration", "test-unit": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/**/*.js\"", "single-test": "METAMASK_ENV=test mocha --require test/helper.js", - "test-integration": "npm run buildMock && npm run buildCiUnits && testem ci -P 2", - "test-coverage": "nyc npm run test-unit && nyc report --reporter=text-lcov | coveralls", + "test-integration": "npm run buildMock && npm run buildCiUnits && karma start", + "test-coverage": "nyc npm run test-unit && if [ $COVERALLS_REPO_TOKEN ]; then nyc report --reporter=text-lcov | coveralls; fi", "ci": "npm run lint && npm run test-coverage && npm run test-integration", "lint": "gulp lint", "buildCiUnits": "node test/integration/index.js", @@ -22,7 +22,6 @@ "ui": "npm run genStates && beefy ui-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./", "mock": "beefy mock-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./", "buildMock": "npm run genStates && browserify ./mock-dev.js -o ./development/bundle.js", - "testem": "npm run buildMock && testem", "announce": "node development/announcer.js", "generateNotice": "node notices/notice-generator.js", "deleteNotice": "node notices/notice-delete.js", @@ -138,7 +137,7 @@ }, "devDependencies": { "babel-core": "^6.24.1", - "babel-eslint": "^7.2.3", + "babel-eslint": "^8.0.0", "babel-plugin-transform-async-to-generator": "^6.24.1", "babel-plugin-transform-runtime": "^6.23.0", "babel-polyfill": "^6.23.0", @@ -172,6 +171,11 @@ "jsdom-global": "^3.0.2", "jshint-stylish": "~2.2.1", "json-rpc-engine": "^3.0.1", + "karma": "^1.7.1", + "karma-chrome-launcher": "^2.2.0", + "karma-cli": "^1.0.1", + "karma-firefox-launcher": "^1.0.1", + "karma-qunit": "^1.2.1", "lodash.assign": "^4.0.6", "mocha": "^3.4.2", "mocha-eslint": "^4.0.0", diff --git a/test/integration/helpers.js b/test/integration/helpers.js deleted file mode 100644 index 10cd74e64..000000000 --- a/test/integration/helpers.js +++ /dev/null @@ -1,7 +0,0 @@ -function wait(time) { - return new Promise(function (resolve, reject) { - setTimeout(function () { - resolve() - }, time * 3 || 1500) - }) -} diff --git a/test/integration/index.js b/test/integration/index.js index e089fc39b..144303dbb 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -1,5 +1,6 @@ const fs = require('fs') const path = require('path') +const pump = require('pump') const browserify = require('browserify') const tests = fs.readdirSync(path.join(__dirname, 'lib')) const bundlePath = path.join(__dirname, 'bundle.js') @@ -9,11 +10,17 @@ const b = browserify() const writeStream = fs.createWriteStream(bundlePath) tests.forEach(function (fileName) { - b.add(path.join(__dirname, 'lib', fileName)) + const filePath = path.join(__dirname, 'lib', fileName) + console.log(`bundling test "${filePath}"`) + b.add(filePath) }) -b.bundle() -.pipe(writeStream) -.on('error', (err) => { - throw err -}) +pump( + b.bundle(), + writeStream, + (err) => { + if (err) throw err + console.log(`Integration test build completed: "${bundlePath}"`) + process.exit(0) + } +) \ No newline at end of file diff --git a/test/integration/lib/first-time.js b/test/integration/lib/first-time.js index 0e4b802da..38a94e551 100644 --- a/test/integration/lib/first-time.js +++ b/test/integration/lib/first-time.js @@ -2,125 +2,137 @@ const PASSWORD = 'password123' QUnit.module('first time usage') -QUnit.test('render init screen', function (assert) { - var done = assert.async() - let app - - wait().then(function() { - app = $('iframe').contents().find('#app-content .mock-app-root') - - const recurseNotices = function () { - let button = app.find('button') - if (button.html() === 'Accept') { - let termsPage = app.find('.markdown')[0] - termsPage.scrollTop = termsPage.scrollHeight - return wait().then(() => { - button.click() - return wait() - }).then(() => { - return recurseNotices() - }) - } else { - return wait() - } - } - return recurseNotices() - }).then(function() { - // Scroll through terms - var title = app.find('h1').text() - assert.equal(title, 'MetaMask', 'title screen') - - // enter password - var pwBox = app.find('#password-box')[0] - var confBox = app.find('#password-box-confirm')[0] - pwBox.value = PASSWORD - confBox.value = PASSWORD - - return wait() - }).then(function() { - - // create vault - var createButton = app.find('button.primary')[0] - createButton.click() - - return wait(1500) - }).then(function() { - - var created = app.find('h3')[0] - assert.equal(created.textContent, 'Vault Created', 'Vault created screen') - - // Agree button - var button = app.find('button')[0] - assert.ok(button, 'button present') - button.click() - - return wait(1000) - }).then(function() { - - var detail = app.find('.account-detail-section')[0] - assert.ok(detail, 'Account detail section loaded.') - - var sandwich = app.find('.sandwich-expando')[0] - sandwich.click() - - return wait() - }).then(function() { - - var sandwich = app.find('.menu-droppo')[0] - var children = sandwich.children - var lock = children[children.length - 2] - assert.ok(lock, 'Lock menu item found') - lock.click() - - return wait(1000) - }).then(function() { - - var pwBox = app.find('#password-box')[0] - pwBox.value = PASSWORD - - var createButton = app.find('button.primary')[0] - createButton.click() - - return wait(1000) - }).then(function() { - - var detail = app.find('.account-detail-section')[0] - assert.ok(detail, 'Account detail section loaded again.') - - return wait() - }).then(function (){ - - var qrButton = app.find('.fa.fa-ellipsis-h')[0] // open account settings dropdown - qrButton.click() - - return wait(1000) - }).then(function (){ - - var qrButton = app.find('.dropdown-menu-item')[1] // qr code item - qrButton.click() - - return wait(1000) - }).then(function (){ - - var qrHeader = app.find('.qr-header')[0] - var qrContainer = app.find('#qr-container')[0] - assert.equal(qrHeader.textContent, 'Account 1', 'Should show account label.') - assert.ok(qrContainer, 'QR Container found') - - return wait() - }).then(function (){ - - var networkMenu = app.find('.network-indicator')[0] - networkMenu.click() - - return wait() - }).then(function (){ - - var networkMenu = app.find('.network-indicator')[0] - var children = networkMenu.children - children.length[3] - assert.ok(children, 'All network options present') - +QUnit.test('render init screen', (assert) => { + const done = assert.async() + runFirstTimeUsageTest(assert).then(done).catch((err) => { + assert.notOk(err, `Error was thrown: ${err.stack}`) done() }) }) + +// QUnit.testDone(({ module, name, total, passed, failed, skipped, todo, runtime }) => { +// if (failed > 0) { +// const app = $('iframe').contents()[0].documentElement +// console.warn('Test failures - dumping DOM:') +// console.log(app.innerHTML) +// } +// }) + +async function runFirstTimeUsageTest(assert, done) { + + await timeout() + + const app = $('#app-content .mock-app-root') + + // recurse notices + while (true) { + const button = app.find('button') + if (button.html() === 'Accept') { + // still notices to accept + const termsPage = app.find('.markdown')[0] + termsPage.scrollTop = termsPage.scrollHeight + await timeout() + button.click() + await timeout() + } else { + // exit loop + break + } + } + + await timeout() + + // Scroll through terms + const title = app.find('h1').text() + assert.equal(title, 'MetaMask', 'title screen') + + // enter password + const pwBox = app.find('#password-box')[0] + const confBox = app.find('#password-box-confirm')[0] + pwBox.value = PASSWORD + confBox.value = PASSWORD + + await timeout() + + // create vault + const createButton = app.find('button.primary')[0] + createButton.click() + + await timeout(1500) + + const created = app.find('h3')[0] + assert.equal(created.textContent, 'Vault Created', 'Vault created screen') + + // Agree button + const button = app.find('button')[0] + assert.ok(button, 'button present') + button.click() + + await timeout(1000) + + const detail = app.find('.account-detail-section')[0] + assert.ok(detail, 'Account detail section loaded.') + + const sandwich = app.find('.sandwich-expando')[0] + sandwich.click() + + await timeout() + + const menu = app.find('.menu-droppo')[0] + const children = menu.children + const lock = children[children.length - 2] + assert.ok(lock, 'Lock menu item found') + lock.click() + + await timeout(1000) + + const pwBox2 = app.find('#password-box')[0] + pwBox2.value = PASSWORD + + const createButton2 = app.find('button.primary')[0] + createButton2.click() + + await timeout(1000) + + const detail2 = app.find('.account-detail-section')[0] + assert.ok(detail2, 'Account detail section loaded again.') + + await timeout() + + // open account settings dropdown + const qrButton = app.find('.fa.fa-ellipsis-h')[0] + qrButton.click() + + await timeout(1000) + + // qr code item + const qrButton2 = app.find('.dropdown-menu-item')[1] + qrButton2.click() + + await timeout(1000) + + const qrHeader = app.find('.qr-header')[0] + const qrContainer = app.find('#qr-container')[0] + assert.equal(qrHeader.textContent, 'Account 1', 'Should show account label.') + assert.ok(qrContainer, 'QR Container found') + + await timeout() + + const networkMenu = app.find('.network-indicator')[0] + networkMenu.click() + + await timeout() + + const networkMenu2 = app.find('.network-indicator')[0] + const children2 = networkMenu2.children + children2.length[3] + assert.ok(children2, 'All network options present') +} + +function timeout(time) { + return new Promise(function (resolve, reject) { + setTimeout(function () { + resolve() + }, time * 3 || 1500) + }) +} \ No newline at end of file diff --git a/test/lib/mock-store.js b/test/lib/mock-store.js index 0d50e2d9c..8af8f6d23 100644 --- a/test/lib/mock-store.js +++ b/test/lib/mock-store.js @@ -1,7 +1,7 @@ const createStore = require('redux').createStore const applyMiddleware = require('redux').applyMiddleware -const thunkMiddleware = require('redux-thunk') -const createLogger = require('redux-logger') +const thunkMiddleware = require('redux-thunk').default +const createLogger = require('redux-logger').createLogger const rootReducer = function () {} module.exports = configureStore diff --git a/test/unit/nonce-tracker-test.js b/test/unit/nonce-tracker-test.js index 3312a3bd0..8970cf84d 100644 --- a/test/unit/nonce-tracker-test.js +++ b/test/unit/nonce-tracker-test.js @@ -162,6 +162,25 @@ describe('Nonce Tracker', function () { await nonceLock.releaseLock() }) }) + + describe('Faq issue 67', function () { + beforeEach(function () { + const txGen = new MockTxGen() + const confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 64 }) + const pendingTxs = txGen.generate({ + status: 'submitted', + }, { count: 10 }) + // 0x40 is 64 in hex: + nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x40') + }) + + it('should return nonce after network nonce', async function () { + this.timeout(15000) + const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926') + assert.equal(nonceLock.nextNonce, '74', `nonce should be 74 got ${nonceLock.nextNonce}`) + await nonceLock.releaseLock() + }) + }) }) }) diff --git a/testem.yml b/testem.yml deleted file mode 100644 index 2cf40f7f4..000000000 --- a/testem.yml +++ /dev/null @@ -1,10 +0,0 @@ -launch_in_dev: - - Chrome - - Firefox -launch_in_ci: - - Chrome - - Firefox -framework: - - qunit -before_tests: "npm run buildCiUnits" -test_page: "test/integration/index.html" diff --git a/ui/app/actions.js b/ui/app/actions.js index eebe65ba2..e793e6a21 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -104,6 +104,7 @@ var actions = { txError: txError, nextTx: nextTx, previousTx: previousTx, + cancelAllTx: cancelAllTx, viewPendingTx: viewPendingTx, VIEW_PENDING_TX: 'VIEW_PENDING_TX', // app messages @@ -457,6 +458,16 @@ function cancelTx (txData) { } } +function cancelAllTx (txsData) { + return (dispatch) => { + txsData.forEach((txData, i) => { + background.cancelTransaction(txData.id, () => { + dispatch(actions.completedTx(txData.id)) + i === txsData.length - 1 ? dispatch(actions.goHome()) : null + }) + }) + } +} // // initialize screen // diff --git a/ui/app/add-token.js b/ui/app/add-token.js index 15ef7a852..18adc7eb5 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -3,6 +3,8 @@ const Component = require('react').Component const h = require('react-hyperscript') const connect = require('react-redux').connect const actions = require('./actions') +const Tooltip = require('./components/tooltip.js') + const ethUtil = require('ethereumjs-util') const abi = require('human-standard-token-abi') @@ -15,6 +17,7 @@ module.exports = connect(mapStateToProps)(AddTokenScreen) function mapStateToProps (state) { return { + identities: state.metamask.identities, } } @@ -64,15 +67,25 @@ AddTokenScreen.prototype.render = function () { }, [ h('div', [ - h('span', { - style: { fontWeight: 'bold', paddingRight: '10px'}, - }, 'Token Address'), + h(Tooltip, { + position: 'top', + title: 'The contract of the actual token contract. Click for more info.', + }, [ + h('a', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + href: 'https://consensyssupport.happyfox.com/staff/kb/article/24-what-is-a-token-contract-address', + target: '_blank', + }, [ + h('span', 'Token Contract Address '), + h('i.fa.fa-question-circle'), + ]), + ]), ]), h('section.flex-row.flex-center', [ h('input#token-address', { name: 'address', - placeholder: 'Token Address', + placeholder: 'Token Contract Address', onChange: this.tokenAddressDidChange.bind(this), style: { width: 'inherit', @@ -171,7 +184,9 @@ AddTokenScreen.prototype.tokenAddressDidChange = function (event) { AddTokenScreen.prototype.validateInputs = function () { let msg = '' const state = this.state + const identitiesList = Object.keys(this.props.identities) const { address, symbol, decimals } = state + const standardAddress = ethUtil.addHexPrefix(address).toLowerCase() const validAddress = ethUtil.isValidAddress(address) if (!validAddress) { @@ -189,7 +204,12 @@ AddTokenScreen.prototype.validateInputs = function () { msg += 'Symbol must be between 0 and 10 characters.' } - const isValid = validAddress && validDecimals + const ownAddress = identitiesList.includes(standardAddress) + if (ownAddress) { + msg = 'Personal address detected. Input the token contract address.' + } + + const isValid = validAddress && validDecimals && !ownAddress if (!isValid) { this.setState({ @@ -216,4 +236,3 @@ AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) this.setState({ symbol: symbol[0], decimals: decimals[0].toString() }) } } - diff --git a/ui/app/app.js b/ui/app/app.js index 1f3d5b0f8..ee800ea90 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -42,6 +42,7 @@ function mapStateToProps (state) { identities, accounts, address, + keyrings, } = state.metamask const selected = address || Object.keys(accounts)[0] @@ -69,6 +70,7 @@ function mapStateToProps (state) { // state needed to get account dropdown temporarily rendering from app bar identities, selected, + keyrings, } } @@ -187,6 +189,7 @@ App.prototype.renderAppBar = function () { identities: this.props.identities, selected: this.props.currentView.context, network: this.props.network, + keyrings: this.props.keyrings, }, []), // hamburger diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js index 7c24e70bd..b087a40d4 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/account-dropdowns.js @@ -22,12 +22,19 @@ class AccountDropdowns extends Component { } renderAccounts () { - const { identities, selected } = this.props + const { identities, selected, keyrings } = this.props return Object.keys(identities).map((key, index) => { const identity = identities[key] const isSelected = identity.address === selected + const simpleAddress = identity.address.substring(2).toLowerCase() + + const keyring = keyrings.find((kr) => { + return kr.accounts.includes(simpleAddress) || + kr.accounts.includes(identity.address) + }) + return h( DropdownMenuItem, { @@ -51,6 +58,7 @@ class AccountDropdowns extends Component { }, }, ), + this.indicateIfLoose(keyring), h('span', { style: { marginLeft: '20px', @@ -67,6 +75,14 @@ class AccountDropdowns extends Component { }) } + indicateIfLoose (keyring) { + try { // Sometimes keyrings aren't loaded yet: + const type = keyring.type + const isLoose = type !== 'HD Key Tree' + return isLoose ? h('.keyring-label', 'LOOSE') : null + } catch (e) { return } + } + renderAccountSelector () { const { actions } = this.props const { accountSelectorActive } = this.state @@ -145,6 +161,8 @@ class AccountDropdowns extends Component { ) } + + renderAccountOptions () { const { actions } = this.props const { optionsMenuActive } = this.state @@ -278,6 +296,7 @@ AccountDropdowns.defaultProps = { AccountDropdowns.propTypes = { identities: PropTypes.objectOf(PropTypes.object), selected: PropTypes.string, + keyrings: PropTypes.array, } const mapDispatchToProps = (dispatch) => { diff --git a/ui/app/components/account-export.js b/ui/app/components/account-export.js index 330f73805..32b103c86 100644 --- a/ui/app/components/account-export.js +++ b/ui/app/components/account-export.js @@ -1,6 +1,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits +const exportAsFile = require('../util').exportAsFile const copyToClipboard = require('copy-to-clipboard') const actions = require('../actions') const ethUtil = require('ethereumjs-util') @@ -20,20 +21,21 @@ function mapStateToProps (state) { } ExportAccountView.prototype.render = function () { - var state = this.props - var accountDetail = state.accountDetail + const state = this.props + const accountDetail = state.accountDetail + const nickname = state.identities[state.address].name if (!accountDetail) return h('div') - var accountExport = accountDetail.accountExport + const accountExport = accountDetail.accountExport - var notExporting = accountExport === 'none' - var exportRequested = accountExport === 'requested' - var accountExported = accountExport === 'completed' + const notExporting = accountExport === 'none' + const exportRequested = accountExport === 'requested' + const accountExported = accountExport === 'completed' if (notExporting) return h('div') if (exportRequested) { - var warning = `Export private keys at your own risk.` + const warning = `Export private keys at your own risk.` return ( h('div', { style: { @@ -89,6 +91,8 @@ ExportAccountView.prototype.render = function () { } if (accountExported) { + const plainKey = ethUtil.stripHexPrefix(accountDetail.privateKey) + return h('div.privateKey', { style: { margin: '0 20px', @@ -105,10 +109,16 @@ ExportAccountView.prototype.render = function () { onClick: function (event) { copyToClipboard(ethUtil.stripHexPrefix(accountDetail.privateKey)) }, - }, ethUtil.stripHexPrefix(accountDetail.privateKey)), + }, plainKey), h('button', { onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), }, 'Done'), + h('button', { + style: { + marginLeft: '10px', + }, + onClick: () => exportAsFile(`MetaMask ${nickname} Private Key`, plainKey), + }, 'Save as File'), ]) } } @@ -117,6 +127,6 @@ ExportAccountView.prototype.onExportKeyPress = function (event) { if (event.key !== 'Enter') return event.preventDefault() - var input = document.getElementById('exportAccount').value + const input = document.getElementById('exportAccount').value this.props.dispatch(actions.exportAccount(input, this.props.address)) } diff --git a/ui/app/components/dropdown.js b/ui/app/components/dropdown.js index 34c7149ee..73710acc2 100644 --- a/ui/app/components/dropdown.js +++ b/ui/app/components/dropdown.js @@ -32,7 +32,7 @@ class Dropdown extends Component { 'style', ` li.dropdown-menu-item:hover { color:rgb(225, 225, 225); } - li.dropdown-menu-item { color: rgb(185, 185, 185); } + li.dropdown-menu-item { color: rgb(185, 185, 185); position: relative } ` ), ...children, diff --git a/ui/app/components/network.js b/ui/app/components/network.js index 698a0bbb9..0dbe37cdd 100644 --- a/ui/app/components/network.js +++ b/ui/app/components/network.js @@ -22,7 +22,7 @@ Network.prototype.render = function () { let iconName, hoverText if (networkNumber === 'loading') { - return h('span', { + return h('span.pointer', { style: { display: 'flex', alignItems: 'center', @@ -37,7 +37,7 @@ Network.prototype.render = function () { }, src: 'images/loading.svg', }), - h('i.fa.fa-sort-desc'), + h('i.fa.fa-caret-down'), ]) } else if (providerName === 'mainnet') { hoverText = 'Main Ethereum Network' @@ -73,7 +73,8 @@ Network.prototype.render = function () { style: { color: '#039396', }}, - 'Ethereum Main Net'), + 'Main Network'), + h('i.fa.fa-caret-down.fa-lg'), ]) case 'ropsten-test-network': return h('.network-indicator', [ @@ -83,6 +84,7 @@ Network.prototype.render = function () { color: '#ff6666', }}, 'Ropsten Test Net'), + h('i.fa.fa-caret-down.fa-lg'), ]) case 'kovan-test-network': return h('.network-indicator', [ @@ -92,6 +94,7 @@ Network.prototype.render = function () { color: '#690496', }}, 'Kovan Test Net'), + h('i.fa.fa-caret-down.fa-lg'), ]) case 'rinkeby-test-network': return h('.network-indicator', [ @@ -101,6 +104,7 @@ Network.prototype.render = function () { color: '#e7a218', }}, 'Rinkeby Test Net'), + h('i.fa.fa-caret-down.fa-lg'), ]) default: return h('.network-indicator', [ @@ -116,6 +120,7 @@ Network.prototype.render = function () { color: '#AEAEAE', }}, 'Private Network'), + h('i.fa.fa-caret-down.fa-lg'), ]) } })(), diff --git a/ui/app/components/pending-msg.js b/ui/app/components/pending-msg.js index b7133cda8..834719c53 100644 --- a/ui/app/components/pending-msg.js +++ b/ui/app/components/pending-msg.js @@ -35,10 +35,21 @@ PendingMsg.prototype.render = function () { style: { margin: '10px', }, - }, `Signing this message can have + }, [ + `Signing this message can have dangerous side effects. Only sign messages from sites you fully trust with your entire account. - This dangerous method will be removed in a future version.`), + This dangerous method will be removed in a future version. `, + h('a', { + href: 'https://medium.com/metamask/the-new-secure-way-to-sign-data-in-your-browser-6af9dd2a1527', + style: { color: 'rgb(247, 134, 28)' }, + onClick: (event) => { + event.preventDefault() + const url = 'https://medium.com/metamask/the-new-secure-way-to-sign-data-in-your-browser-6af9dd2a1527' + global.platform.openWindow({ url }) + }, + }, 'Read more here.'), + ]), // message details h(PendingTxDetails, state), diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 5324ccd64..3e53d47f9 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -66,6 +66,8 @@ PendingTx.prototype.render = function () { const balanceBn = hexToBn(balance) const insufficientBalance = balanceBn.lt(maxCost) + const buyDisabled = insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting + const showRejectAll = props.unconfTxListLength > 1 this.inputs = [] @@ -297,14 +299,6 @@ PendingTx.prototype.render = function () { margin: '14px 25px', }, }, [ - - - insufficientBalance ? - h('button.btn-green', { - onClick: props.buyEth, - }, 'Buy Ether') - : null, - h('button', { onClick: (event) => { this.resetGasFields() @@ -312,18 +306,30 @@ PendingTx.prototype.render = function () { }, }, 'Reset'), - // Accept Button - h('input.confirm.btn-green', { - type: 'submit', - value: 'SUBMIT', - style: { marginLeft: '10px' }, - disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting, - }), + // Accept Button or Buy Button + insufficientBalance ? h('button.btn-green', { onClick: props.buyEth }, 'Buy Ether') : + h('input.confirm.btn-green', { + type: 'submit', + value: 'SUBMIT', + style: { marginLeft: '10px' }, + disabled: buyDisabled, + }), h('button.cancel.btn-red', { onClick: props.cancelTransaction, }, 'Reject'), ]), + showRejectAll ? h('.flex-row.flex-space-around.conf-buttons', { + style: { + display: 'flex', + justifyContent: 'flex-end', + margin: '14px 25px', + }, + }, [ + h('button.cancel.btn-red', { + onClick: props.cancelAllTransactions, + }, 'Reject All'), + ]) : null, ]), ]) ) diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index 34727ff78..1ee4166f7 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -52,6 +52,8 @@ ConfirmTxScreen.prototype.render = function () { log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`) if (unconfTxList.length === 0) return h(Loading, { isLoading: true }) + const unconfTxListLength = unconfTxList.length + return ( h('.flex-column.flex-grow', [ @@ -101,10 +103,12 @@ ConfirmTxScreen.prototype.render = function () { conversionRate, currentCurrency, blockGasLimit, + unconfTxListLength, // Actions buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), sendTransaction: this.sendTransaction.bind(this), cancelTransaction: this.cancelTransaction.bind(this, txData), + cancelAllTransactions: this.cancelAllTransactions.bind(this, unconfTxList), signMessage: this.signMessage.bind(this, txData), signPersonalMessage: this.signPersonalMessage.bind(this, txData), cancelMessage: this.cancelMessage.bind(this, txData), @@ -151,6 +155,12 @@ ConfirmTxScreen.prototype.cancelTransaction = function (txData, event) { this.props.dispatch(actions.cancelTx(txData)) } +ConfirmTxScreen.prototype.cancelAllTransactions = function (unconfTxList, event) { + this.stopPropagation(event) + event.preventDefault() + this.props.dispatch(actions.cancelAllTx(unconfTxList)) +} + ConfirmTxScreen.prototype.signMessage = function (msgData, event) { log.info('conf-tx.js: signing message') var params = msgData.msgParams diff --git a/ui/app/config.js b/ui/app/config.js index c6d9c3e5d..8eaaa1384 100644 --- a/ui/app/config.js +++ b/ui/app/config.js @@ -5,7 +5,8 @@ const connect = require('react-redux').connect const actions = require('./actions') const infuraCurrencies = require('./infura-conversion.json').symbols const validUrl = require('valid-url') -const copyToClipboard = require('copy-to-clipboard') +const exportAsFile = require('./util').exportAsFile + module.exports = connect(mapStateToProps)(ConfigScreen) @@ -110,9 +111,9 @@ ConfigScreen.prototype.render = function () { alignSelf: 'center', }, onClick (event) { - copyToClipboard(window.logState()) + exportAsFile('MetaMask State Logs', window.logState()) }, - }, 'Copy State Logs'), + }, 'Download State Logs'), ]), h('hr.horizontal-line'), diff --git a/ui/app/css/lib.css b/ui/app/css/lib.css index 81647d1c1..f3acbee76 100644 --- a/ui/app/css/lib.css +++ b/ui/app/css/lib.css @@ -215,12 +215,13 @@ hr.horizontal-line { z-index: 1; font-size: 11px; background: rgba(255,0,0,0.8); - bottom: -47px; color: white; + bottom: 0px; + left: -8px; border-radius: 10px; height: 20px; min-width: 20px; - position: relative; + position: absolute; display: flex; align-items: center; justify-content: center; diff --git a/ui/app/info.js b/ui/app/info.js index c69d83715..4c7d4cb4c 100644 --- a/ui/app/info.js +++ b/ui/app/info.js @@ -103,7 +103,7 @@ InfoScreen.prototype.render = function () { [ h('div.fa.fa-support', [ h('a.info', { - href: 'https://support.metamask.com', + href: 'https://support.metamask.io', target: '_blank', }, 'Visit our Support Center'), ]), diff --git a/ui/app/keychains/hd/create-vault-complete.js b/ui/app/keychains/hd/create-vault-complete.js index c32751fff..745990351 100644 --- a/ui/app/keychains/hd/create-vault-complete.js +++ b/ui/app/keychains/hd/create-vault-complete.js @@ -3,6 +3,7 @@ const Component = require('react').Component const connect = require('react-redux').connect const h = require('react-hyperscript') const actions = require('../../actions') +const exportAsFile = require('../../util').exportAsFile module.exports = connect(mapStateToProps)(CreateVaultCompleteScreen) @@ -65,8 +66,17 @@ CreateVaultCompleteScreen.prototype.render = function () { style: { margin: '24px', fontSize: '0.9em', + marginBottom: '10px', }, }, 'I\'ve copied it somewhere safe'), + + h('button.primary', { + onClick: () => exportAsFile(`MetaMask Seed Words`, seed), + style: { + margin: '10px', + fontSize: '0.9em', + }, + }, 'Save Seed Words As File'), ]) ) } diff --git a/ui/app/unlock.js b/ui/app/unlock.js index 9bacd5124..4180791c4 100644 --- a/ui/app/unlock.js +++ b/ui/app/unlock.js @@ -80,7 +80,7 @@ UnlockScreen.prototype.render = function () { color: 'rgb(247, 134, 28)', textDecoration: 'underline', }, - }, 'I forgot my password.'), + }, 'Restore from seed phrase'), ]), ]) ) diff --git a/ui/app/util.js b/ui/app/util.js index ac3f42c6b..1368ebf11 100644 --- a/ui/app/util.js +++ b/ui/app/util.js @@ -36,6 +36,7 @@ module.exports = { valueTable: valueTable, bnTable: bnTable, isHex: isHex, + exportAsFile: exportAsFile, } function valuesFor (obj) { @@ -215,3 +216,18 @@ function readableDate (ms) { function isHex (str) { return Boolean(str.match(/^(0x)?[0-9a-fA-F]+$/)) } + +function exportAsFile (filename, data) { + // source: https://stackoverflow.com/a/33542499 by Ludovic Feltz + const blob = new Blob([data], {type: 'text/csv'}) + if (window.navigator.msSaveOrOpenBlob) { + window.navigator.msSaveBlob(blob, filename) + } else { + const elem = window.document.createElement('a') + elem.href = window.URL.createObjectURL(blob) + elem.download = filename + document.body.appendChild(elem) + elem.click() + document.body.removeChild(elem) + } +}