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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

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,
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
//

View File

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

View File

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

View File

@ -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) => {

View File

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

View File

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

View File

@ -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'),
])
}
})(),

View File

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

View File

@ -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,
]),
])
)

View File

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

View File

@ -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'),

View File

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

View File

@ -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'),
]),

View File

@ -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'),
])
)
}

View File

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

View File

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