1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Merge branch 'master' into new-currency-test

This commit is contained in:
Kevin Serrano 2017-09-14 08:35:48 -07:00
commit cb8856597c
No known key found for this signature in database
GPG Key ID: BF999DEFC7371BA1
33 changed files with 504 additions and 257 deletions

View File

@ -2,12 +2,34 @@
## Current Master ## 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 - Make eth_sign deprecation warning less noisy
- Add useful link to eth_sign deprecation warning.
- Fix bug with network version serialization over synchronous RPC - Fix bug with network version serialization over synchronous RPC
- Add MetaMask version to state logs. - Add MetaMask version to state logs.
- Add the total amount of tokens when multiple tokens are added under the token list - Add the total amount of tokens when multiple tokens are added under the token list
- Use HTTPS links for Etherscan. - Use HTTPS links for Etherscan.
- Update Support center link to new one with HTTPS. - 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 ## 3.9.11 2017-8-24

View File

@ -1,7 +1,7 @@
{ {
"name": "MetaMask", "name": "MetaMask",
"short_name": "Metamask", "short_name": "Metamask",
"version": "3.9.11", "version": "3.10.0",
"manifest_version": 2, "manifest_version": 2,
"author": "https://metamask.io", "author": "https://metamask.io",
"description": "Ethereum Browser Extension", "description": "Ethereum Browser Extension",
@ -57,9 +57,8 @@
"permissions": [ "permissions": [
"storage", "storage",
"clipboardWrite", "clipboardWrite",
"http://localhost:8545/", "http://localhost:8545/"
"https://api.cryptonator.com/" ],
],
"web_accessible_resources": [ "web_accessible_resources": [
"scripts/inpage.js" "scripts/inpage.js"
], ],

View File

@ -171,9 +171,9 @@ class KeyringController extends EventEmitter {
return this.setupAccounts(checkedAccounts) return this.setupAccounts(checkedAccounts)
}) })
.then(() => this.persistAllKeyrings()) .then(() => this.persistAllKeyrings())
.then(() => this._updateMemStoreKeyrings())
.then(() => this.fullUpdate()) .then(() => this.fullUpdate())
.then(() => { .then(() => {
this._updateMemStoreKeyrings()
return keyring return keyring
}) })
} }
@ -208,6 +208,7 @@ class KeyringController extends EventEmitter {
return selectedKeyring.addAccounts(1) return selectedKeyring.addAccounts(1)
.then(this.setupAccounts.bind(this)) .then(this.setupAccounts.bind(this))
.then(this.persistAllKeyrings.bind(this)) .then(this.persistAllKeyrings.bind(this))
.then(this._updateMemStoreKeyrings.bind(this))
.then(this.fullUpdate.bind(this)) .then(this.fullUpdate.bind(this))
} }

View File

@ -11,7 +11,7 @@ function setupDappAutoReload (web3, observable) {
get: (_web3, key) => { get: (_web3, key) => {
// show warning once on web3 access // show warning once on web3 access
if (!hasBeenWarned && key !== 'currentProvider') { 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 hasBeenWarned = true
} }
// get the time of use // get the time of use

View File

@ -40,30 +40,37 @@ function MetamaskInpageProvider (connectionStream) {
// start and stop polling to unblock first block lock // start and stop polling to unblock first block lock
self.idMap = {} self.idMap = {}
// handle sendAsync requests via asyncProvider }
self.sendAsync = function (payload, cb) {
// rewrite request ids // handle sendAsync requests via asyncProvider
var request = eachJsonMessage(payload, (message) => { // also remap ids inbound and outbound
var newId = createRandomId() MetamaskInpageProvider.prototype.sendAsync = function (payload, cb) {
self.idMap[newId] = message.id const self = this
message.id = newId
// 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 return message
}) })
// forward to asyncProvider cb(null, res)
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)
})
}
} }
MetamaskInpageProvider.prototype.send = function (payload) { MetamaskInpageProvider.prototype.send = function (payload) {
const self = this 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 () { MetamaskInpageProvider.prototype.isConnected = function () {
return true return true
} }

View File

@ -1,10 +1,17 @@
machine: machine:
node: node:
version: 8.1.4 version: 8.1.4
dependencies:
pre:
- "npm i -g testem"
- "npm i -g mocha"
test: test:
override: override:
- "npm run ci" - "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

View File

@ -1,7 +1,7 @@
const createStore = require('redux').createStore const createStore = require('redux').createStore
const applyMiddleware = require('redux').applyMiddleware const applyMiddleware = require('redux').applyMiddleware
const thunkMiddleware = require('redux-thunk') const thunkMiddleware = require('redux-thunk').default
const createLogger = require('redux-logger') const createLogger = require('redux-logger').createLogger
const rootReducer = require('../ui/app/reducers') const rootReducer = require('../ui/app/reducers')
module.exports = configureStore module.exports = configureStore

61
karma.conf.js Normal file
View File

@ -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
})
}

View File

@ -85,40 +85,47 @@ actions.update = function(stateName) {
var css = MetaMaskUiCss() var css = MetaMaskUiCss()
injectCss(css) injectCss(css)
const container = document.querySelector('#app-content')
// parse opts // parse opts
var store = configureStore(firstState) var store = configureStore(firstState)
// start app // start app
render( startApp()
h('.super-dev-container', [
h('button', { function startApp(){
onClick: (ev) => { const body = document.body
ev.preventDefault() const container = document.createElement('div')
store.dispatch(actions.update('terms')) container.id = 'app-content'
}, body.appendChild(container)
style: { console.log('container', container)
margin: '19px 19px 0px 19px',
},
}, 'Reset State'),
h(Selector, { actions, selectedKey: selectedView, states, store }), render(
h('.super-dev-container', [
h('.mock-app-root', { h('button', {
style: { onClick: (ev) => {
height: '500px', ev.preventDefault()
width: '360px', store.dispatch(actions.update('terms'))
boxShadow: 'grey 0px 2px 9px', },
margin: '20px', style: {
}, margin: '19px 19px 0px 19px',
}, [ },
h(Root, { }, 'Reset State'),
store: store,
}),
]),
] h(Selector, { actions, selectedKey: selectedView, states, store }),
), container)
h('.mock-app-root', {
style: {
height: '500px',
width: '360px',
boxShadow: 'grey 0px 2px 9px',
margin: '20px',
},
}, [
h(Root, {
store: store,
}),
]),
]
), container)
}

View File

@ -12,8 +12,8 @@
"test": "npm run lint && npm run test-unit && npm run test-integration", "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\"", "test-unit": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/**/*.js\"",
"single-test": "METAMASK_ENV=test mocha --require test/helper.js", "single-test": "METAMASK_ENV=test mocha --require test/helper.js",
"test-integration": "npm run buildMock && npm run buildCiUnits && testem ci -P 2", "test-integration": "npm run buildMock && npm run buildCiUnits && karma start",
"test-coverage": "nyc npm run test-unit && nyc report --reporter=text-lcov | coveralls", "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", "ci": "npm run lint && npm run test-coverage && npm run test-integration",
"lint": "gulp lint", "lint": "gulp lint",
"buildCiUnits": "node test/integration/index.js", "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 ./", "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 ./", "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", "buildMock": "npm run genStates && browserify ./mock-dev.js -o ./development/bundle.js",
"testem": "npm run buildMock && testem",
"announce": "node development/announcer.js", "announce": "node development/announcer.js",
"generateNotice": "node notices/notice-generator.js", "generateNotice": "node notices/notice-generator.js",
"deleteNotice": "node notices/notice-delete.js", "deleteNotice": "node notices/notice-delete.js",
@ -138,7 +137,7 @@
}, },
"devDependencies": { "devDependencies": {
"babel-core": "^6.24.1", "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-async-to-generator": "^6.24.1",
"babel-plugin-transform-runtime": "^6.23.0", "babel-plugin-transform-runtime": "^6.23.0",
"babel-polyfill": "^6.23.0", "babel-polyfill": "^6.23.0",
@ -172,6 +171,11 @@
"jsdom-global": "^3.0.2", "jsdom-global": "^3.0.2",
"jshint-stylish": "~2.2.1", "jshint-stylish": "~2.2.1",
"json-rpc-engine": "^3.0.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", "lodash.assign": "^4.0.6",
"mocha": "^3.4.2", "mocha": "^3.4.2",
"mocha-eslint": "^4.0.0", "mocha-eslint": "^4.0.0",

View File

@ -1,7 +0,0 @@
function wait(time) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve()
}, time * 3 || 1500)
})
}

View File

@ -1,5 +1,6 @@
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const pump = require('pump')
const browserify = require('browserify') const browserify = require('browserify')
const tests = fs.readdirSync(path.join(__dirname, 'lib')) const tests = fs.readdirSync(path.join(__dirname, 'lib'))
const bundlePath = path.join(__dirname, 'bundle.js') const bundlePath = path.join(__dirname, 'bundle.js')
@ -9,11 +10,17 @@ const b = browserify()
const writeStream = fs.createWriteStream(bundlePath) const writeStream = fs.createWriteStream(bundlePath)
tests.forEach(function (fileName) { 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() pump(
.pipe(writeStream) b.bundle(),
.on('error', (err) => { writeStream,
throw err (err) => {
}) if (err) throw err
console.log(`Integration test build completed: "${bundlePath}"`)
process.exit(0)
}
)

View File

@ -2,125 +2,137 @@ const PASSWORD = 'password123'
QUnit.module('first time usage') QUnit.module('first time usage')
QUnit.test('render init screen', function (assert) { QUnit.test('render init screen', (assert) => {
var done = assert.async() const done = assert.async()
let app runFirstTimeUsageTest(assert).then(done).catch((err) => {
assert.notOk(err, `Error was thrown: ${err.stack}`)
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')
done() 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)
})
}

View File

@ -1,7 +1,7 @@
const createStore = require('redux').createStore const createStore = require('redux').createStore
const applyMiddleware = require('redux').applyMiddleware const applyMiddleware = require('redux').applyMiddleware
const thunkMiddleware = require('redux-thunk') const thunkMiddleware = require('redux-thunk').default
const createLogger = require('redux-logger') const createLogger = require('redux-logger').createLogger
const rootReducer = function () {} const rootReducer = function () {}
module.exports = configureStore module.exports = configureStore

View File

@ -162,6 +162,25 @@ describe('Nonce Tracker', function () {
await nonceLock.releaseLock() 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()
})
})
}) })
}) })

View File

@ -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"

View File

@ -104,6 +104,7 @@ var actions = {
txError: txError, txError: txError,
nextTx: nextTx, nextTx: nextTx,
previousTx: previousTx, previousTx: previousTx,
cancelAllTx: cancelAllTx,
viewPendingTx: viewPendingTx, viewPendingTx: viewPendingTx,
VIEW_PENDING_TX: 'VIEW_PENDING_TX', VIEW_PENDING_TX: 'VIEW_PENDING_TX',
// app messages // 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 // initialize screen
// //

View File

@ -3,6 +3,8 @@ const Component = require('react').Component
const h = require('react-hyperscript') const h = require('react-hyperscript')
const connect = require('react-redux').connect const connect = require('react-redux').connect
const actions = require('./actions') const actions = require('./actions')
const Tooltip = require('./components/tooltip.js')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const abi = require('human-standard-token-abi') const abi = require('human-standard-token-abi')
@ -15,6 +17,7 @@ module.exports = connect(mapStateToProps)(AddTokenScreen)
function mapStateToProps (state) { function mapStateToProps (state) {
return { return {
identities: state.metamask.identities,
} }
} }
@ -64,15 +67,25 @@ AddTokenScreen.prototype.render = function () {
}, [ }, [
h('div', [ h('div', [
h('span', { h(Tooltip, {
style: { fontWeight: 'bold', paddingRight: '10px'}, position: 'top',
}, 'Token Address'), 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('section.flex-row.flex-center', [
h('input#token-address', { h('input#token-address', {
name: 'address', name: 'address',
placeholder: 'Token Address', placeholder: 'Token Contract Address',
onChange: this.tokenAddressDidChange.bind(this), onChange: this.tokenAddressDidChange.bind(this),
style: { style: {
width: 'inherit', width: 'inherit',
@ -171,7 +184,9 @@ AddTokenScreen.prototype.tokenAddressDidChange = function (event) {
AddTokenScreen.prototype.validateInputs = function () { AddTokenScreen.prototype.validateInputs = function () {
let msg = '' let msg = ''
const state = this.state const state = this.state
const identitiesList = Object.keys(this.props.identities)
const { address, symbol, decimals } = state const { address, symbol, decimals } = state
const standardAddress = ethUtil.addHexPrefix(address).toLowerCase()
const validAddress = ethUtil.isValidAddress(address) const validAddress = ethUtil.isValidAddress(address)
if (!validAddress) { if (!validAddress) {
@ -189,7 +204,12 @@ AddTokenScreen.prototype.validateInputs = function () {
msg += 'Symbol must be between 0 and 10 characters.' 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) { if (!isValid) {
this.setState({ this.setState({
@ -216,4 +236,3 @@ AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address)
this.setState({ symbol: symbol[0], decimals: decimals[0].toString() }) this.setState({ symbol: symbol[0], decimals: decimals[0].toString() })
} }
} }

View File

@ -42,6 +42,7 @@ function mapStateToProps (state) {
identities, identities,
accounts, accounts,
address, address,
keyrings,
} = state.metamask } = state.metamask
const selected = address || Object.keys(accounts)[0] 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 // state needed to get account dropdown temporarily rendering from app bar
identities, identities,
selected, selected,
keyrings,
} }
} }
@ -187,6 +189,7 @@ App.prototype.renderAppBar = function () {
identities: this.props.identities, identities: this.props.identities,
selected: this.props.currentView.context, selected: this.props.currentView.context,
network: this.props.network, network: this.props.network,
keyrings: this.props.keyrings,
}, []), }, []),
// hamburger // hamburger

View File

@ -22,12 +22,19 @@ class AccountDropdowns extends Component {
} }
renderAccounts () { renderAccounts () {
const { identities, selected } = this.props const { identities, selected, keyrings } = this.props
return Object.keys(identities).map((key, index) => { return Object.keys(identities).map((key, index) => {
const identity = identities[key] const identity = identities[key]
const isSelected = identity.address === selected 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( return h(
DropdownMenuItem, DropdownMenuItem,
{ {
@ -51,6 +58,7 @@ class AccountDropdowns extends Component {
}, },
}, },
), ),
this.indicateIfLoose(keyring),
h('span', { h('span', {
style: { style: {
marginLeft: '20px', 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 () { renderAccountSelector () {
const { actions } = this.props const { actions } = this.props
const { accountSelectorActive } = this.state const { accountSelectorActive } = this.state
@ -145,6 +161,8 @@ class AccountDropdowns extends Component {
) )
} }
renderAccountOptions () { renderAccountOptions () {
const { actions } = this.props const { actions } = this.props
const { optionsMenuActive } = this.state const { optionsMenuActive } = this.state
@ -278,6 +296,7 @@ AccountDropdowns.defaultProps = {
AccountDropdowns.propTypes = { AccountDropdowns.propTypes = {
identities: PropTypes.objectOf(PropTypes.object), identities: PropTypes.objectOf(PropTypes.object),
selected: PropTypes.string, selected: PropTypes.string,
keyrings: PropTypes.array,
} }
const mapDispatchToProps = (dispatch) => { const mapDispatchToProps = (dispatch) => {

View File

@ -1,6 +1,7 @@
const Component = require('react').Component const Component = require('react').Component
const h = require('react-hyperscript') const h = require('react-hyperscript')
const inherits = require('util').inherits const inherits = require('util').inherits
const exportAsFile = require('../util').exportAsFile
const copyToClipboard = require('copy-to-clipboard') const copyToClipboard = require('copy-to-clipboard')
const actions = require('../actions') const actions = require('../actions')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
@ -20,20 +21,21 @@ function mapStateToProps (state) {
} }
ExportAccountView.prototype.render = function () { ExportAccountView.prototype.render = function () {
var state = this.props const state = this.props
var accountDetail = state.accountDetail const accountDetail = state.accountDetail
const nickname = state.identities[state.address].name
if (!accountDetail) return h('div') if (!accountDetail) return h('div')
var accountExport = accountDetail.accountExport const accountExport = accountDetail.accountExport
var notExporting = accountExport === 'none' const notExporting = accountExport === 'none'
var exportRequested = accountExport === 'requested' const exportRequested = accountExport === 'requested'
var accountExported = accountExport === 'completed' const accountExported = accountExport === 'completed'
if (notExporting) return h('div') if (notExporting) return h('div')
if (exportRequested) { if (exportRequested) {
var warning = `Export private keys at your own risk.` const warning = `Export private keys at your own risk.`
return ( return (
h('div', { h('div', {
style: { style: {
@ -89,6 +91,8 @@ ExportAccountView.prototype.render = function () {
} }
if (accountExported) { if (accountExported) {
const plainKey = ethUtil.stripHexPrefix(accountDetail.privateKey)
return h('div.privateKey', { return h('div.privateKey', {
style: { style: {
margin: '0 20px', margin: '0 20px',
@ -105,10 +109,16 @@ ExportAccountView.prototype.render = function () {
onClick: function (event) { onClick: function (event) {
copyToClipboard(ethUtil.stripHexPrefix(accountDetail.privateKey)) copyToClipboard(ethUtil.stripHexPrefix(accountDetail.privateKey))
}, },
}, ethUtil.stripHexPrefix(accountDetail.privateKey)), }, plainKey),
h('button', { h('button', {
onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)),
}, 'Done'), }, '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 if (event.key !== 'Enter') return
event.preventDefault() event.preventDefault()
var input = document.getElementById('exportAccount').value const input = document.getElementById('exportAccount').value
this.props.dispatch(actions.exportAccount(input, this.props.address)) this.props.dispatch(actions.exportAccount(input, this.props.address))
} }

View File

@ -32,7 +32,7 @@ class Dropdown extends Component {
'style', 'style',
` `
li.dropdown-menu-item:hover { color:rgb(225, 225, 225); } 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, ...children,

View File

@ -22,7 +22,7 @@ Network.prototype.render = function () {
let iconName, hoverText let iconName, hoverText
if (networkNumber === 'loading') { if (networkNumber === 'loading') {
return h('span', { return h('span.pointer', {
style: { style: {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
@ -37,7 +37,7 @@ Network.prototype.render = function () {
}, },
src: 'images/loading.svg', src: 'images/loading.svg',
}), }),
h('i.fa.fa-sort-desc'), h('i.fa.fa-caret-down'),
]) ])
} else if (providerName === 'mainnet') { } else if (providerName === 'mainnet') {
hoverText = 'Main Ethereum Network' hoverText = 'Main Ethereum Network'
@ -73,7 +73,8 @@ Network.prototype.render = function () {
style: { style: {
color: '#039396', color: '#039396',
}}, }},
'Ethereum Main Net'), 'Main Network'),
h('i.fa.fa-caret-down.fa-lg'),
]) ])
case 'ropsten-test-network': case 'ropsten-test-network':
return h('.network-indicator', [ return h('.network-indicator', [
@ -83,6 +84,7 @@ Network.prototype.render = function () {
color: '#ff6666', color: '#ff6666',
}}, }},
'Ropsten Test Net'), 'Ropsten Test Net'),
h('i.fa.fa-caret-down.fa-lg'),
]) ])
case 'kovan-test-network': case 'kovan-test-network':
return h('.network-indicator', [ return h('.network-indicator', [
@ -92,6 +94,7 @@ Network.prototype.render = function () {
color: '#690496', color: '#690496',
}}, }},
'Kovan Test Net'), 'Kovan Test Net'),
h('i.fa.fa-caret-down.fa-lg'),
]) ])
case 'rinkeby-test-network': case 'rinkeby-test-network':
return h('.network-indicator', [ return h('.network-indicator', [
@ -101,6 +104,7 @@ Network.prototype.render = function () {
color: '#e7a218', color: '#e7a218',
}}, }},
'Rinkeby Test Net'), 'Rinkeby Test Net'),
h('i.fa.fa-caret-down.fa-lg'),
]) ])
default: default:
return h('.network-indicator', [ return h('.network-indicator', [
@ -116,6 +120,7 @@ Network.prototype.render = function () {
color: '#AEAEAE', color: '#AEAEAE',
}}, }},
'Private Network'), 'Private Network'),
h('i.fa.fa-caret-down.fa-lg'),
]) ])
} }
})(), })(),

View File

@ -35,10 +35,21 @@ PendingMsg.prototype.render = function () {
style: { style: {
margin: '10px', margin: '10px',
}, },
}, `Signing this message can have }, [
`Signing this message can have
dangerous side effects. Only sign messages from dangerous side effects. Only sign messages from
sites you fully trust with your entire account. 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 // message details
h(PendingTxDetails, state), h(PendingTxDetails, state),

View File

@ -66,6 +66,8 @@ PendingTx.prototype.render = function () {
const balanceBn = hexToBn(balance) const balanceBn = hexToBn(balance)
const insufficientBalance = balanceBn.lt(maxCost) const insufficientBalance = balanceBn.lt(maxCost)
const buyDisabled = insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting
const showRejectAll = props.unconfTxListLength > 1
this.inputs = [] this.inputs = []
@ -297,14 +299,6 @@ PendingTx.prototype.render = function () {
margin: '14px 25px', margin: '14px 25px',
}, },
}, [ }, [
insufficientBalance ?
h('button.btn-green', {
onClick: props.buyEth,
}, 'Buy Ether')
: null,
h('button', { h('button', {
onClick: (event) => { onClick: (event) => {
this.resetGasFields() this.resetGasFields()
@ -312,18 +306,30 @@ PendingTx.prototype.render = function () {
}, },
}, 'Reset'), }, 'Reset'),
// Accept Button // Accept Button or Buy Button
h('input.confirm.btn-green', { insufficientBalance ? h('button.btn-green', { onClick: props.buyEth }, 'Buy Ether') :
type: 'submit', h('input.confirm.btn-green', {
value: 'SUBMIT', type: 'submit',
style: { marginLeft: '10px' }, value: 'SUBMIT',
disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting, style: { marginLeft: '10px' },
}), disabled: buyDisabled,
}),
h('button.cancel.btn-red', { h('button.cancel.btn-red', {
onClick: props.cancelTransaction, onClick: props.cancelTransaction,
}, 'Reject'), }, '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,
]), ]),
]) ])
) )

View File

@ -52,6 +52,8 @@ ConfirmTxScreen.prototype.render = function () {
log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`) log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`)
if (unconfTxList.length === 0) return h(Loading, { isLoading: true }) if (unconfTxList.length === 0) return h(Loading, { isLoading: true })
const unconfTxListLength = unconfTxList.length
return ( return (
h('.flex-column.flex-grow', [ h('.flex-column.flex-grow', [
@ -101,10 +103,12 @@ ConfirmTxScreen.prototype.render = function () {
conversionRate, conversionRate,
currentCurrency, currentCurrency,
blockGasLimit, blockGasLimit,
unconfTxListLength,
// Actions // Actions
buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress),
sendTransaction: this.sendTransaction.bind(this), sendTransaction: this.sendTransaction.bind(this),
cancelTransaction: this.cancelTransaction.bind(this, txData), cancelTransaction: this.cancelTransaction.bind(this, txData),
cancelAllTransactions: this.cancelAllTransactions.bind(this, unconfTxList),
signMessage: this.signMessage.bind(this, txData), signMessage: this.signMessage.bind(this, txData),
signPersonalMessage: this.signPersonalMessage.bind(this, txData), signPersonalMessage: this.signPersonalMessage.bind(this, txData),
cancelMessage: this.cancelMessage.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)) 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) { ConfirmTxScreen.prototype.signMessage = function (msgData, event) {
log.info('conf-tx.js: signing message') log.info('conf-tx.js: signing message')
var params = msgData.msgParams var params = msgData.msgParams

View File

@ -5,7 +5,8 @@ const connect = require('react-redux').connect
const actions = require('./actions') const actions = require('./actions')
const infuraCurrencies = require('./infura-conversion.json').symbols const infuraCurrencies = require('./infura-conversion.json').symbols
const validUrl = require('valid-url') const validUrl = require('valid-url')
const copyToClipboard = require('copy-to-clipboard') const exportAsFile = require('./util').exportAsFile
module.exports = connect(mapStateToProps)(ConfigScreen) module.exports = connect(mapStateToProps)(ConfigScreen)
@ -110,9 +111,9 @@ ConfigScreen.prototype.render = function () {
alignSelf: 'center', alignSelf: 'center',
}, },
onClick (event) { onClick (event) {
copyToClipboard(window.logState()) exportAsFile('MetaMask State Logs', window.logState())
}, },
}, 'Copy State Logs'), }, 'Download State Logs'),
]), ]),
h('hr.horizontal-line'), h('hr.horizontal-line'),

View File

@ -215,12 +215,13 @@ hr.horizontal-line {
z-index: 1; z-index: 1;
font-size: 11px; font-size: 11px;
background: rgba(255,0,0,0.8); background: rgba(255,0,0,0.8);
bottom: -47px;
color: white; color: white;
bottom: 0px;
left: -8px;
border-radius: 10px; border-radius: 10px;
height: 20px; height: 20px;
min-width: 20px; min-width: 20px;
position: relative; position: absolute;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View File

@ -103,7 +103,7 @@ InfoScreen.prototype.render = function () {
[ [
h('div.fa.fa-support', [ h('div.fa.fa-support', [
h('a.info', { h('a.info', {
href: 'https://support.metamask.com', href: 'https://support.metamask.io',
target: '_blank', target: '_blank',
}, 'Visit our Support Center'), }, 'Visit our Support Center'),
]), ]),

View File

@ -3,6 +3,7 @@ const Component = require('react').Component
const connect = require('react-redux').connect const connect = require('react-redux').connect
const h = require('react-hyperscript') const h = require('react-hyperscript')
const actions = require('../../actions') const actions = require('../../actions')
const exportAsFile = require('../../util').exportAsFile
module.exports = connect(mapStateToProps)(CreateVaultCompleteScreen) module.exports = connect(mapStateToProps)(CreateVaultCompleteScreen)
@ -65,8 +66,17 @@ CreateVaultCompleteScreen.prototype.render = function () {
style: { style: {
margin: '24px', margin: '24px',
fontSize: '0.9em', fontSize: '0.9em',
marginBottom: '10px',
}, },
}, 'I\'ve copied it somewhere safe'), }, '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'),
]) ])
) )
} }

View File

@ -80,7 +80,7 @@ UnlockScreen.prototype.render = function () {
color: 'rgb(247, 134, 28)', color: 'rgb(247, 134, 28)',
textDecoration: 'underline', textDecoration: 'underline',
}, },
}, 'I forgot my password.'), }, 'Restore from seed phrase'),
]), ]),
]) ])
) )

View File

@ -36,6 +36,7 @@ module.exports = {
valueTable: valueTable, valueTable: valueTable,
bnTable: bnTable, bnTable: bnTable,
isHex: isHex, isHex: isHex,
exportAsFile: exportAsFile,
} }
function valuesFor (obj) { function valuesFor (obj) {
@ -215,3 +216,18 @@ function readableDate (ms) {
function isHex (str) { function isHex (str) {
return Boolean(str.match(/^(0x)?[0-9a-fA-F]+$/)) 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)
}
}