Merge branch 'develop' into save-brave

This commit is contained in:
kumavis 2018-06-14 10:11:35 -07:00 committed by GitHub
commit d9ef72cb7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 6857 additions and 6789 deletions

View File

@ -2,8 +2,13 @@
## Current Master
- Attempting to import an empty private key will now show a clear error.
- Fix bug where metamask data would stop being written to disk after prolonged use
- Fix bug where account reset did not work with custom RPC providers.
- Fix for Brave i18n getAcceptLanguages [#4270](https://github.com/MetaMask/metamask-extension/issues/4270)
- Fix bug where nonce mutex was never released
- Stop reloading browser page on Ethereum network change
- Add phishing notice
## 4.7.4 Tue Jun 05 2018

View File

@ -16,7 +16,18 @@ const accountImporter = {
strategies: {
'Private Key': (privateKey) => {
const stripped = ethUtil.stripHexPrefix(privateKey)
if (!privateKey) {
throw new Error('Cannot import an empty key.')
}
const prefixed = ethUtil.addHexPrefix(privateKey)
const buffer = ethUtil.toBuffer(prefixed)
if (!ethUtil.isValidPrivate(buffer)) {
throw new Error('Cannot import invalid private key.')
}
const stripped = ethUtil.stripHexPrefix(prefixed)
return stripped
},
'JSON File': (input, password) => {

View File

@ -16,6 +16,7 @@ const ExtensionPlatform = require('./platforms/extension')
const Migrator = require('./lib/migrator/')
const migrations = require('./migrations/')
const PortStream = require('./lib/port-stream.js')
const createStreamSink = require('./lib/createStreamSink')
const NotificationManager = require('./lib/notification-manager.js')
const MetamaskController = require('./metamask-controller')
const firstTimeState = require('./first-time-state')
@ -273,7 +274,7 @@ function setupController (initState, initLangCode) {
asStream(controller.store),
debounce(1000),
storeTransform(versionifyData),
storeTransform(persistData),
createStreamSink(persistData),
(error) => {
log.error('MetaMask - Persistence pipeline failed', error)
}
@ -289,7 +290,7 @@ function setupController (initState, initLangCode) {
return versionedData
}
function persistData (state) {
async function persistData (state) {
if (!state) {
throw new Error('MetaMask - updated state is missing', state)
}
@ -297,12 +298,13 @@ function setupController (initState, initLangCode) {
throw new Error('MetaMask - updated state does not have data', state)
}
if (localStore.isSupported) {
localStore.set(state)
.catch((err) => {
try {
await localStore.set(state)
} catch (err) {
// log error so we dont break the pipeline
log.error('error setting state in local store:', err)
})
}
}
return state
}
//

View File

@ -165,7 +165,7 @@ class TransactionController extends EventEmitter {
// add default tx params
txMeta = await this.addTxGasDefaults(txMeta)
} catch (error) {
console.log(error)
log.warn(error)
this.txStateManager.setTxStatusFailed(txMeta.id, error)
throw error
}
@ -264,7 +264,12 @@ class TransactionController extends EventEmitter {
// must set transaction to submitted/failed before releasing lock
nonceLock.releaseLock()
} catch (err) {
this.txStateManager.setTxStatusFailed(txId, err)
// this is try-catch wrapped so that we can guarantee that the nonceLock is released
try {
this.txStateManager.setTxStatusFailed(txId, err)
} catch (err) {
log.error(err)
}
// must set transaction to submitted/failed before releasing lock
if (nonceLock) nonceLock.releaseLock()
// continue with error chain

View File

@ -49,29 +49,35 @@ class NonceTracker {
await this._globalMutexFree()
// await lock free, then take lock
const releaseLock = await this._takeMutex(address)
// evaluate multiple nextNonce strategies
const nonceDetails = {}
const networkNonceResult = await this._getNetworkNextNonce(address)
const highestLocallyConfirmed = this._getHighestLocallyConfirmed(address)
const nextNetworkNonce = networkNonceResult.nonce
const highestSuggested = Math.max(nextNetworkNonce, highestLocallyConfirmed)
try {
// evaluate multiple nextNonce strategies
const nonceDetails = {}
const networkNonceResult = await this._getNetworkNextNonce(address)
const highestLocallyConfirmed = this._getHighestLocallyConfirmed(address)
const nextNetworkNonce = networkNonceResult.nonce
const highestSuggested = Math.max(nextNetworkNonce, highestLocallyConfirmed)
const pendingTxs = this.getPendingTransactions(address)
const localNonceResult = this._getHighestContinuousFrom(pendingTxs, highestSuggested) || 0
const pendingTxs = this.getPendingTransactions(address)
const localNonceResult = this._getHighestContinuousFrom(pendingTxs, highestSuggested) || 0
nonceDetails.params = {
highestLocallyConfirmed,
highestSuggested,
nextNetworkNonce,
nonceDetails.params = {
highestLocallyConfirmed,
highestSuggested,
nextNetworkNonce,
}
nonceDetails.local = localNonceResult
nonceDetails.network = networkNonceResult
const nextNonce = Math.max(networkNonceResult.nonce, localNonceResult.nonce)
assert(Number.isInteger(nextNonce), `nonce-tracker - nextNonce is not an integer - got: (${typeof nextNonce}) "${nextNonce}"`)
// return nonce and release cb
return { nextNonce, nonceDetails, releaseLock }
} catch (err) {
// release lock if we encounter an error
releaseLock()
throw err
}
nonceDetails.local = localNonceResult
nonceDetails.network = networkNonceResult
const nextNonce = Math.max(networkNonceResult.nonce, localNonceResult.nonce)
assert(Number.isInteger(nextNonce), `nonce-tracker - nextNonce is not an integer - got: (${typeof nextNonce}) "${nextNonce}"`)
// return nonce and release cb
return { nextNonce, nonceDetails, releaseLock }
}
async _getCurrentBlock () {
@ -85,8 +91,8 @@ class NonceTracker {
async _globalMutexFree () {
const globalMutex = this._lookupMutex('global')
const release = await globalMutex.acquire()
release()
const releaseLock = await globalMutex.acquire()
releaseLock()
}
async _takeMutex (lockId) {

View File

@ -196,14 +196,14 @@ class PendingTransactionTracker extends EventEmitter {
async _checkPendingTxs () {
const signedTxList = this.getPendingTransactions()
// in order to keep the nonceTracker accurate we block it while updating pending transactions
const nonceGlobalLock = await this.nonceTracker.getGlobalLock()
const { releaseLock } = await this.nonceTracker.getGlobalLock()
try {
await Promise.all(signedTxList.map((txMeta) => this._checkPendingTx(txMeta)))
} catch (err) {
log.error('PendingTransactionWatcher - Error updating pending transactions')
log.error(err)
}
nonceGlobalLock.releaseLock()
releaseLock()
}
/**

View File

@ -3,7 +3,6 @@ cleanContextForImports()
require('web3/dist/web3.min.js')
const log = require('loglevel')
const LocalMessageDuplexStream = require('post-message-stream')
const setupDappAutoReload = require('./lib/auto-reload.js')
const MetamaskInpageProvider = require('./lib/inpage-provider.js')
restoreContextAfterImports()
@ -38,8 +37,24 @@ web3.setProvider = function () {
log.debug('MetaMask - overrode web3.setProvider')
}
log.debug('MetaMask - injected web3')
// export global web3, with usage-detection
setupDappAutoReload(web3, inpageProvider.publicConfigStore)
// export global web3, with usage-detection and deprecation warning
let hasBeenWarned = false
global.web3 = new Proxy(web3, {
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/MetaMask/faq/blob/master/detecting_metamask.md#web3-deprecation')
hasBeenWarned = true
}
// return value normally
return _web3[key]
},
set: (_web3, key, value) => {
// set value normally
_web3[key] = value
},
})
// set web3 defaultAccount
inpageProvider.publicConfigStore.subscribe(function (state) {

View File

@ -1,61 +0,0 @@
module.exports = setupDappAutoReload
function setupDappAutoReload (web3, observable) {
// export web3 as a global, checking for usage
let hasBeenWarned = false
let reloadInProgress = false
let lastTimeUsed
let lastSeenNetwork
global.web3 = new Proxy(web3, {
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/MetaMask/faq/blob/master/detecting_metamask.md#web3-deprecation')
hasBeenWarned = true
}
// get the time of use
lastTimeUsed = Date.now()
// return value normally
return _web3[key]
},
set: (_web3, key, value) => {
// set value normally
_web3[key] = value
},
})
observable.subscribe(function (state) {
// if reload in progress, no need to check reload logic
if (reloadInProgress) return
const currentNetwork = state.networkVersion
// set the initial network
if (!lastSeenNetwork) {
lastSeenNetwork = currentNetwork
return
}
// skip reload logic if web3 not used
if (!lastTimeUsed) return
// if network did not change, exit
if (currentNetwork === lastSeenNetwork) return
// initiate page reload
reloadInProgress = true
const timeSinceUse = Date.now() - lastTimeUsed
// if web3 was recently used then delay the reloading of the page
if (timeSinceUse > 500) {
triggerReset()
} else {
setTimeout(triggerReset, 500)
}
})
}
// reload the page
function triggerReset () {
global.location.reload()
}

View File

@ -0,0 +1,24 @@
const WritableStream = require('readable-stream').Writable
const promiseToCallback = require('promise-to-callback')
module.exports = createStreamSink
function createStreamSink(asyncWriteFn, _opts) {
return new AsyncWritableStream(asyncWriteFn, _opts)
}
class AsyncWritableStream extends WritableStream {
constructor (asyncWriteFn, _opts) {
const opts = Object.assign({ objectMode: true }, _opts)
super(opts)
this._asyncWriteFn = asyncWriteFn
}
// write from incomming stream to state
_write (chunk, encoding, callback) {
promiseToCallback(this._asyncWriteFn(chunk, encoding))(callback)
}
}

View File

@ -46,7 +46,6 @@ const GWEI_BN = new BN('1000000000')
const percentile = require('percentile')
const seedPhraseVerifier = require('./lib/seed-phrase-verifier')
const cleanErrorStack = require('./lib/cleanErrorStack')
const DiagnosticsReporter = require('./lib/diagnostics-reporter')
const log = require('loglevel')
module.exports = class MetamaskController extends EventEmitter {
@ -65,12 +64,6 @@ module.exports = class MetamaskController extends EventEmitter {
const initState = opts.initState || {}
this.recordFirstTimeInfo(initState)
// metamask diagnostics reporter
this.diagnostics = opts.diagnostics || new DiagnosticsReporter({
firstTimeInfo: initState.firstTimeInfo,
version,
})
// platform-specific api
this.platform = opts.platform
@ -92,7 +85,6 @@ module.exports = class MetamaskController extends EventEmitter {
this.preferencesController = new PreferencesController({
initState: initState.PreferencesController,
initLangCode: opts.initLangCode,
diagnostics: this.diagnostics,
})
// currency controller
@ -189,9 +181,6 @@ module.exports = class MetamaskController extends EventEmitter {
version,
firstVersion: initState.firstTimeInfo.version,
})
this.noticeController.updateNoticesList()
// to be uncommented when retrieving notices from a remote server.
// this.noticeController.startPolling()
this.shapeshiftController = new ShapeShiftController({
initState: initState.ShapeShiftController,
@ -436,28 +425,24 @@ module.exports = class MetamaskController extends EventEmitter {
* @returns {Object} vault
*/
async createNewVaultAndKeychain (password) {
const release = await this.createVaultMutex.acquire()
let vault
const releaseLock = await this.createVaultMutex.acquire()
try {
let vault
const accounts = await this.keyringController.getAccounts()
if (accounts.length > 0) {
vault = await this.keyringController.fullUpdate()
} else {
vault = await this.keyringController.createNewVaultAndKeychain(password)
const accounts = await this.keyringController.getAccounts()
this.preferencesController.setAddresses(accounts)
this.selectFirstIdentity()
}
release()
releaseLock()
return vault
} catch (err) {
release()
releaseLock()
throw err
}
return vault
}
/**
@ -466,7 +451,7 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {} seed
*/
async createNewVaultAndRestore (password, seed) {
const release = await this.createVaultMutex.acquire()
const releaseLock = await this.createVaultMutex.acquire()
try {
// clear known identities
this.preferencesController.setAddresses([])
@ -476,10 +461,10 @@ module.exports = class MetamaskController extends EventEmitter {
const accounts = await this.keyringController.getAccounts()
this.preferencesController.setAddresses(accounts)
this.selectFirstIdentity()
release()
releaseLock()
return vault
} catch (err) {
release()
releaseLock()
throw err
}
}

View File

@ -2,7 +2,7 @@ const EventEmitter = require('events').EventEmitter
const semver = require('semver')
const extend = require('xtend')
const ObservableStore = require('obs-store')
const hardCodedNotices = require('../../notices/notices.json')
const hardCodedNotices = require('../../notices/notices.js')
const uniqBy = require('lodash.uniqby')
module.exports = class NoticeController extends EventEmitter {
@ -13,11 +13,12 @@ module.exports = class NoticeController extends EventEmitter {
this.firstVersion = opts.firstVersion
this.version = opts.version
const initState = extend({
noticesList: [],
noticesList: this._filterNotices(hardCodedNotices),
}, opts.initState)
this.store = new ObservableStore(initState)
this.memStore = new ObservableStore({})
this.store.subscribe(() => this._updateMemstore())
this._updateMemstore()
}
getNoticesList () {
@ -29,9 +30,9 @@ module.exports = class NoticeController extends EventEmitter {
return notices.filter((notice) => notice.read === false)
}
getLatestUnreadNotice () {
getNextUnreadNotice () {
const unreadNotices = this.getUnreadNotices()
return unreadNotices[unreadNotices.length - 1]
return unreadNotices[0]
}
async setNoticesList (noticesList) {
@ -47,7 +48,7 @@ module.exports = class NoticeController extends EventEmitter {
notices[index].read = true
notices[index].body = ''
this.setNoticesList(notices)
const latestNotice = this.getLatestUnreadNotice()
const latestNotice = this.getNextUnreadNotice()
cb(null, latestNotice)
} catch (err) {
cb(err)
@ -64,15 +65,6 @@ module.exports = class NoticeController extends EventEmitter {
return result
}
startPolling () {
if (this.noticePoller) {
clearInterval(this.noticePoller)
}
this.noticePoller = setInterval(() => {
this.noticeController.updateNoticesList()
}, 300000)
}
_mergeNotices (oldNotices, newNotices) {
return uniqBy(oldNotices.concat(newNotices), 'id')
}
@ -91,19 +83,15 @@ module.exports = class NoticeController extends EventEmitter {
})
}
_mapNoticeIds (notices) {
return notices.map((notice) => notice.id)
}
async _retrieveNoticeData () {
// Placeholder for the API.
return hardCodedNotices
return []
}
_updateMemstore () {
const lastUnreadNotice = this.getLatestUnreadNotice()
const noActiveNotices = !lastUnreadNotice
this.memStore.updateState({ lastUnreadNotice, noActiveNotices })
const nextUnreadNotice = this.getNextUnreadNotice()
const noActiveNotices = !nextUnreadNotice
this.memStore.updateState({ nextUnreadNotice, noActiveNotices })
}
}

View File

@ -52,7 +52,7 @@
"conversionRate": 12.7200827,
"conversionDate": 1487363041,
"noActiveNotices": true,
"lastUnreadNotice": {
"nextUnreadNotice": {
"read": true,
"date": "Thu Feb 09 2017",
"title": "Terms of Use",

View File

@ -12,7 +12,7 @@
"conversionRate": 12.7527416,
"conversionDate": 1487624341,
"noActiveNotices": false,
"lastUnreadNotice": {
"nextUnreadNotice": {
"read": false,
"date": "Thu Feb 09 2017",
"title": "Terms of Use",

View File

@ -13,7 +13,7 @@
"conversionRate": 8.3533002,
"conversionDate": 1481671082,
"noActiveNotices": false,
"lastUnreadNotice": {
"nextUnreadNotice": {
"read": false,
"date": "Tue Dec 13 2016",
"title": "MultiVault Support",

View File

@ -14,7 +14,7 @@ import LoadingScreen from './loading-screen'
class NoticeScreen extends Component {
static propTypes = {
address: PropTypes.string.isRequired,
lastUnreadNotice: PropTypes.shape({
nextUnreadNotice: PropTypes.shape({
title: PropTypes.string,
date: PropTypes.string,
body: PropTypes.string,
@ -31,7 +31,7 @@ class NoticeScreen extends Component {
};
static defaultProps = {
lastUnreadNotice: {},
nextUnreadNotice: {},
};
state = {
@ -47,8 +47,8 @@ class NoticeScreen extends Component {
}
acceptTerms = () => {
const { markNoticeRead, lastUnreadNotice, history } = this.props
markNoticeRead(lastUnreadNotice)
const { markNoticeRead, nextUnreadNotice, history } = this.props
markNoticeRead(nextUnreadNotice)
.then(hasActiveNotices => {
if (!hasActiveNotices) {
history.push(INITIALIZE_BACKUP_PHRASE_ROUTE)
@ -72,7 +72,7 @@ class NoticeScreen extends Component {
render () {
const {
address,
lastUnreadNotice: { title, body },
nextUnreadNotice: { title, body },
isLoading,
} = this.props
const { atBottom } = this.state
@ -113,12 +113,12 @@ class NoticeScreen extends Component {
}
const mapStateToProps = ({ metamask, appState }) => {
const { selectedAddress, lastUnreadNotice, noActiveNotices } = metamask
const { selectedAddress, nextUnreadNotice, noActiveNotices } = metamask
const { isLoading } = appState
return {
address: selectedAddress,
lastUnreadNotice,
nextUnreadNotice,
noActiveNotices,
isLoading,
}

View File

@ -0,0 +1,6 @@
Dear MetaMask Users,
There have been several instances of high-profile legitimate websites such as BTC Manager and Games Workshop that have had their websites temporarily compromised. This involves showing a fake MetaMask window on the page asking for user's seed phrases. MetaMask will never open itself in this way and users are encouraged to report these instances immediately to either [our phishing blacklist](https://github.com/MetaMask/eth-phishing-detect/issues) or our support email at [support@metamask.io](support@metamask.io).
Please read our full article on this ongoing issue at [https://medium.com/metamask/new-phishing-strategy-becoming-common-1b1123837168](https://medium.com/metamask/new-phishing-strategy-becoming-common-1b1123837168).

View File

@ -1,27 +0,0 @@
var fs = require('fs')
var path = require('path')
var prompt = require('prompt')
var open = require('open')
var extend = require('extend')
var notices = require('./notices.json')
console.log('List of Notices')
console.log(`ID \t DATE \t\t\t TITLE`)
notices.forEach((notice) => {
console.log(`${(' ' + notice.id).slice(-2)} \t ${notice.date} \t ${notice.title}`)
})
prompt.get(['id'], (error, res) => {
prompt.start()
if (error) {
console.log("Exiting...")
process.exit()
}
var index = notices.findIndex((notice) => { return notice.id == res.id})
if (index === -1) {
console.log('Notice not found. Exiting...')
}
notices.splice(index, 1)
fs.unlink(`notices/archive/notice_${res.id}.md`)
fs.writeFile(`notices/notices.json`, JSON.stringify(notices))
})

View File

@ -1,33 +0,0 @@
var fsp = require('fs-promise')
var path = require('path')
var prompt = require('prompt')
var open = require('open')
var extend = require('extend')
var notices = require('./notices.json')
var id = Number(require('./notice-nonce.json'))
var date = new Date().toDateString()
var notice = {
read: false,
date: date,
}
fsp.writeFile(`notices/archive/notice_${id}.md`,'Message goes here. Please write out your notice and save before proceeding at the command line.')
.then(() => {
open(`notices/archive/notice_${id}.md`)
prompt.start()
prompt.get(['title'], (err, result) => {
notice.title = result.title
fsp.readFile(`notices/archive/notice_${id}.md`)
.then((body) => {
notice.body = body.toString()
notice.id = id
notices.push(notice)
return fsp.writeFile(`notices/notices.json`, JSON.stringify(notices))
}).then((completion) => {
id += 1
return fsp.writeFile(`notices/notice-nonce.json`, id)
})
})
})

View File

@ -1 +0,0 @@
4

34
notices/notices.js Normal file
View File

@ -0,0 +1,34 @@
// fs.readFileSync is inlined by browserify transform "brfs"
const fs = require('fs')
module.exports = [
{
id: 0,
read: false,
date: 'Thu Feb 09 2017',
title: 'Terms of Use',
body: fs.readFileSync(__dirname + '/archive/notice_0.md', 'utf8'),
},
{
id: 2,
read: false,
date: 'Mon May 08 2017',
title: 'Privacy Notice',
body: fs.readFileSync(__dirname + '/archive/notice_2.md', 'utf8'),
},
{
id: 3,
read: false,
date: 'Tue Nov 28 2017',
title: 'Seed Phrase Alert',
firstVersion: '<=3.12.0',
body: fs.readFileSync(__dirname + '/archive/notice_3.md', 'utf8'),
},
{
id: 4,
read: false,
date: 'Wed Jun 13 2018',
title: 'Phishing Warning',
body: fs.readFileSync(__dirname + '/archive/notice_4.md', 'utf8'),
}
]

File diff suppressed because one or more lines are too long

View File

@ -73,7 +73,7 @@ function mapStateToProps (state) {
network: state.metamask.network,
provider: state.metamask.provider,
forgottenPassword: state.appState.forgottenPassword,
lastUnreadNotice: state.metamask.lastUnreadNotice,
nextUnreadNotice: state.metamask.nextUnreadNotice,
lostAccounts: state.metamask.lostAccounts,
frequentRpcList: state.metamask.frequentRpcList || [],
featureFlags,
@ -460,9 +460,9 @@ App.prototype.renderPrimary = function () {
}, [
h(NoticeScreen, {
notice: props.lastUnreadNotice,
notice: props.nextUnreadNotice,
key: 'NoticeScreen',
onConfirm: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)),
onConfirm: () => props.dispatch(actions.markNoticeRead(props.nextUnreadNotice)),
}),
!props.isInitialized && h('.flex-row.flex-center.flex-grow', [

12990
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@
"dist": "gulp dist",
"doc": "jsdoc -c development/tools/.jsdoc.json",
"test": "npm run test:unit && npm run test:integration && npm run lint",
"watch:test:unit": "nodemon --exec \"npm run test:unit\" ./test ./app ./ui",
"test:unit": "cross-env METAMASK_ENV=test mocha --exit --require test/setup.js --recursive \"test/unit/**/*.js\" \"ui/app/**/*.test.js\" && dot-only-hunter",
"test:single": "cross-env METAMASK_ENV=test mocha --require test/helper.js",
"test:integration": "npm run test:integration:build && npm run test:flat && npm run test:mascara",
@ -45,8 +46,6 @@
"disc": "gulp disc --debug",
"announce": "node development/announcer.js",
"version:bump": "node development/run-version-bump.js",
"generateNotice": "node notices/notice-generator.js",
"deleteNotice": "node notices/notice-delete.js",
"storybook": "start-storybook -p 6006 -c .storybook"
},
"browserify": {

View File

@ -134,21 +134,34 @@ describe('Using MetaMask with an existing account', function () {
await delay(regularDelayMs)
})
it('clicks through the privacy notice', async () => {
const [nextScreen] = await findElements(driver, By.css('.tou button'))
await nextScreen.click()
await delay(regularDelayMs)
it('clicks through the ToS', async () => {
// terms of use
const canClickThrough = await driver.findElement(By.css('.tou button')).isEnabled()
assert.equal(canClickThrough, false, 'disabled continue button')
const element = await findElement(driver, By.linkText('Attributions'))
await driver.executeScript('arguments[0].scrollIntoView(true)', element)
const bottomOfTos = await findElement(driver, By.linkText('Attributions'))
await driver.executeScript('arguments[0].scrollIntoView(true)', bottomOfTos)
await delay(regularDelayMs)
const acceptTos = await findElement(driver, By.xpath(`//button[contains(text(), 'Accept')]`))
const acceptTos = await findElement(driver, By.css('.tou button'))
await acceptTos.click()
await delay(regularDelayMs)
})
it('clicks through the privacy notice', async () => {
// privacy notice
const nextScreen = await findElement(driver, By.css('.tou button'))
await nextScreen.click()
await delay(regularDelayMs)
})
it('clicks through the phishing notice', async () => {
// phishing notice
const noticeElement = await driver.findElement(By.css('.markdown'))
await driver.executeScript('arguments[0].scrollTop = arguments[0].scrollHeight', noticeElement)
await delay(regularDelayMs)
const nextScreen = await findElement(driver, By.css('.tou button'))
await nextScreen.click()
await delay(regularDelayMs)
})
})
describe('Show account information', () => {

View File

@ -128,22 +128,35 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
})
it('clicks through the privacy notice', async () => {
const nextScreen = await findElement(driver, By.css('.tou button'))
await nextScreen.click()
await delay(regularDelayMs)
it('clicks through the ToS', async () => {
// terms of use
const canClickThrough = await driver.findElement(By.css('.tou button')).isEnabled()
assert.equal(canClickThrough, false, 'disabled continue button')
const bottomOfTos = await findElement(driver, By.linkText('Attributions'))
await driver.executeScript('arguments[0].scrollIntoView(true)', bottomOfTos)
await delay(regularDelayMs)
const acceptTos = await findElement(driver, By.css('.tou button'))
await acceptTos.click()
await delay(regularDelayMs)
})
it('clicks through the privacy notice', async () => {
// privacy notice
const nextScreen = await findElement(driver, By.css('.tou button'))
await nextScreen.click()
await delay(regularDelayMs)
})
it('clicks through the phishing notice', async () => {
// phishing notice
const noticeElement = await driver.findElement(By.css('.markdown'))
await driver.executeScript('arguments[0].scrollTop = arguments[0].scrollHeight', noticeElement)
await delay(regularDelayMs)
const nextScreen = await findElement(driver, By.css('.tou button'))
await nextScreen.click()
await delay(regularDelayMs)
})
let seedPhrase
it('reveals the seed phrase', async () => {

View File

@ -21,7 +21,7 @@ function delay (time) {
}
function buildChromeWebDriver (extPath) {
const tmpProfile = path.join(os.tmpdir(), fs.mkdtempSync('mm-chrome-profile'));
const tmpProfile = fs.mkdtempSync(path.join(os.tmpdir(), 'mm-chrome-profile'))
return new webdriver.Builder()
.withCapabilities({
chromeOptions: {

View File

@ -71,13 +71,6 @@ describe('Metamask popup page', function () {
it('matches MetaMask title', async () => {
const title = await driver.getTitle()
assert.equal(title, 'MetaMask', 'title matches MetaMask')
})
it('shows privacy notice', async () => {
await delay(300)
const privacy = await driver.findElement(By.css('.terms-header')).getText()
assert.equal(privacy, 'PRIVACY NOTICE', 'shows privacy notice')
await driver.findElement(By.css('button')).click()
await delay(300)
})
@ -100,6 +93,24 @@ describe('Metamask popup page', function () {
await button.click()
})
it('shows privacy notice', async () => {
const privacy = await driver.findElement(By.css('.terms-header')).getText()
assert.equal(privacy, 'PRIVACY NOTICE', 'shows privacy notice')
await driver.findElement(By.css('button')).click()
await delay(300)
})
it('shows phishing notice', async () => {
await delay(300)
const noticeHeader = await driver.findElement(By.css('.terms-header')).getText()
assert.equal(noticeHeader, 'PHISHING WARNING', 'shows phishing warning')
const element = await driver.findElement(By.css('.markdown'))
await driver.executeScript('arguments[0].scrollTop = arguments[0].scrollHeight', element)
await delay(300)
await driver.findElement(By.css('button')).click()
await delay(300)
})
it('accepts password with length of eight', async () => {
const passwordBox = await driver.findElement(By.id('password-box'))
const passwordBoxConfirm = await driver.findElement(By.id('password-box-confirm'))

View File

@ -1,31 +1,59 @@
const assert = require('assert')
const path = require('path')
const accountImporter = require('../../../app/scripts/account-import-strategies/index')
const ethUtil = require('ethereumjs-util')
const accountImporter = require('../../../app/scripts/account-import-strategies/index')
const { assertRejects } = require('../test-utils')
describe('Account Import Strategies', function () {
const privkey = '0x4cfd3e90fc78b0f86bf7524722150bb8da9c60cd532564d7ff43f5716514f553'
const json = '{"version":3,"id":"dbb54385-0a99-437f-83c0-647de9f244c3","address":"a7f92ce3fba24196cf6f4bd2e1eb3db282ba998c","Crypto":{"ciphertext":"bde13d9ade5c82df80281ca363320ce254a8a3a06535bbf6ffdeaf0726b1312c","cipherparams":{"iv":"fbf93718a57f26051b292f072f2e5b41"},"cipher":"aes-128-ctr","kdf":"scrypt","kdfparams":{"dklen":32,"salt":"7ffe00488319dec48e4c49a120ca49c6afbde9272854c64d9541c83fc6acdffe","n":8192,"r":8,"p":1},"mac":"2adfd9c4bc1cdac4c85bddfb31d9e21a684e0e050247a70c5698facf6b7d4681"}}'
it('imports a private key and strips 0x prefix', async function () {
const importPrivKey = await accountImporter.importAccount('Private Key', [ privkey ])
assert.equal(importPrivKey, ethUtil.stripHexPrefix(privkey))
describe('private key import', function () {
it('imports a private key and strips 0x prefix', async function () {
const importPrivKey = await accountImporter.importAccount('Private Key', [ privkey ])
assert.equal(importPrivKey, ethUtil.stripHexPrefix(privkey))
})
it('throws an error for empty string private key', async () => {
assertRejects(async function() {
await accountImporter.importAccount('Private Key', [ '' ])
}, Error, 'no empty strings')
})
it('throws an error for undefined string private key', async () => {
assertRejects(async function () {
await accountImporter.importAccount('Private Key', [ undefined ])
})
})
it('throws an error for undefined string private key', async () => {
assertRejects(async function () {
await accountImporter.importAccount('Private Key', [])
})
})
it('throws an error for invalid private key', async () => {
assertRejects(async function () {
await accountImporter.importAccount('Private Key', [ 'popcorn' ])
})
})
})
it('fails when password is incorrect for keystore', async function () {
const wrongPassword = 'password2'
describe('JSON keystore import', function () {
it('fails when password is incorrect for keystore', async function () {
const wrongPassword = 'password2'
try {
await accountImporter.importAccount('JSON File', [ json, wrongPassword])
} catch (error) {
assert.equal(error.message, 'Key derivation failed - possibly wrong passphrase')
}
try {
await accountImporter.importAccount('JSON File', [ json, wrongPassword])
} catch (error) {
assert.equal(error.message, 'Key derivation failed - possibly wrong passphrase')
}
})
it('imports json string and password to return a private key', async function () {
const fileContentsPassword = 'password1'
const importJson = await accountImporter.importAccount('JSON File', [ json, fileContentsPassword])
assert.equal(importJson, '0x5733876abe94146069ce8bcbabbde2677f2e35fa33e875e92041ed2ac87e5bc7')
})
})
it('imports json string and password to return a private key', async function () {
const fileContentsPassword = 'password1'
const importJson = await accountImporter.importAccount('JSON File', [ json, fileContentsPassword])
assert.equal(importJson, '0x5733876abe94146069ce8bcbabbde2677f2e35fa33e875e92041ed2ac87e5bc7')
})
})

View File

@ -14,18 +14,6 @@ describe('notice-controller', function () {
})
describe('notices', function () {
describe('#getNoticesList', function () {
it('should return an empty array when new', function (done) {
// const testList = [{
// id: 0,
// read: false,
// title: 'Futuristic Notice',
// }]
var result = noticeController.getNoticesList()
assert.equal(result.length, 0)
done()
})
})
describe('#setNoticesList', function () {
it('should set data appropriately', function (done) {
@ -41,36 +29,6 @@ describe('notice-controller', function () {
})
})
describe('#updateNoticeslist', function () {
it('should integrate the latest changes from the source', function (done) {
var testList = [{
id: 55,
read: false,
title: 'Futuristic Notice',
}]
noticeController.setNoticesList(testList)
noticeController.updateNoticesList().then(() => {
var newList = noticeController.getNoticesList()
assert.ok(newList[0].id === 55)
assert.ok(newList[1])
done()
})
})
it('should not overwrite any existing fields', function (done) {
var testList = [{
id: 0,
read: false,
title: 'Futuristic Notice',
}]
noticeController.setNoticesList(testList)
var newList = noticeController.getNoticesList()
assert.equal(newList[0].id, 0)
assert.equal(newList[0].title, 'Futuristic Notice')
assert.equal(newList.length, 1)
done()
})
})
describe('#markNoticeRead', function () {
it('should mark a notice as read', function (done) {
var testList = [{
@ -86,7 +44,7 @@ describe('notice-controller', function () {
})
})
describe('#getLatestUnreadNotice', function () {
describe('#getNextUnreadNotice', function () {
it('should retrieve the latest unread notice', function (done) {
var testList = [
{id: 0, read: true, title: 'Past Notice'},
@ -94,8 +52,8 @@ describe('notice-controller', function () {
{id: 2, read: false, title: 'Future Notice'},
]
noticeController.setNoticesList(testList)
var latestUnread = noticeController.getLatestUnreadNotice()
assert.equal(latestUnread.id, 2)
var latestUnread = noticeController.getNextUnreadNotice()
assert.equal(latestUnread.id, 1)
done()
})
it('should return undefined if no unread notices exist.', function (done) {
@ -105,7 +63,7 @@ describe('notice-controller', function () {
{id: 2, read: true, title: 'Future Notice'},
]
noticeController.setNoticesList(testList)
var latestUnread = noticeController.getLatestUnreadNotice()
var latestUnread = noticeController.getNextUnreadNotice()
assert.ok(!latestUnread)
done()
})

17
test/unit/test-utils.js Normal file
View File

@ -0,0 +1,17 @@
const assert = require('assert')
module.exports = {
assertRejects,
}
// assert.rejects added in node v10
async function assertRejects (asyncFn, regExp) {
let f = () => {}
try {
await asyncFn()
} catch (error) {
f = () => { throw error }
} finally {
assert.throws(f, regExp)
}
}

View File

@ -314,7 +314,7 @@ function mapStateToProps (state) {
noActiveNotices,
seedWords,
unapprovedTxs,
lastUnreadNotice,
nextUnreadNotice,
lostAccounts,
unapprovedMsgCount,
unapprovedPersonalMsgCount,
@ -348,7 +348,7 @@ function mapStateToProps (state) {
network: state.metamask.network,
provider: state.metamask.provider,
forgottenPassword: state.appState.forgottenPassword,
lastUnreadNotice,
nextUnreadNotice,
lostAccounts,
frequentRpcList: state.metamask.frequentRpcList || [],
currentCurrency: state.metamask.currentCurrency,

View File

@ -86,9 +86,9 @@ class Home extends Component {
// if (!props.noActiveNotices) {
// log.debug('rendering notice screen for unread notices.')
// return h(NoticeScreen, {
// notice: props.lastUnreadNotice,
// notice: props.nextUnreadNotice,
// key: 'NoticeScreen',
// onConfirm: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)),
// onConfirm: () => props.dispatch(actions.markNoticeRead(props.nextUnreadNotice)),
// })
// } else if (props.lostAccounts && props.lostAccounts.length > 0) {
// log.debug('rendering notice screen for lost accounts view.')
@ -279,7 +279,7 @@ function mapStateToProps (state) {
noActiveNotices,
seedWords,
unapprovedTxs,
lastUnreadNotice,
nextUnreadNotice,
lostAccounts,
unapprovedMsgCount,
unapprovedPersonalMsgCount,
@ -313,7 +313,7 @@ function mapStateToProps (state) {
network: state.metamask.network,
provider: state.metamask.provider,
forgottenPassword: state.appState.forgottenPassword,
lastUnreadNotice,
nextUnreadNotice,
lostAccounts,
frequentRpcList: state.metamask.frequentRpcList || [],
currentCurrency: state.metamask.currentCurrency,

View File

@ -154,11 +154,11 @@ class Notice extends Component {
const mapStateToProps = state => {
const { metamask } = state
const { noActiveNotices, lastUnreadNotice, lostAccounts } = metamask
const { noActiveNotices, nextUnreadNotice, lostAccounts } = metamask
return {
noActiveNotices,
lastUnreadNotice,
nextUnreadNotice,
lostAccounts,
}
}
@ -171,21 +171,21 @@ Notice.propTypes = {
const mapDispatchToProps = dispatch => {
return {
markNoticeRead: lastUnreadNotice => dispatch(actions.markNoticeRead(lastUnreadNotice)),
markNoticeRead: nextUnreadNotice => dispatch(actions.markNoticeRead(nextUnreadNotice)),
markAccountsFound: () => dispatch(actions.markAccountsFound()),
}
}
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const { noActiveNotices, lastUnreadNotice, lostAccounts } = stateProps
const { noActiveNotices, nextUnreadNotice, lostAccounts } = stateProps
const { markNoticeRead, markAccountsFound } = dispatchProps
let notice
let onConfirm
if (!noActiveNotices) {
notice = lastUnreadNotice
onConfirm = () => markNoticeRead(lastUnreadNotice)
notice = nextUnreadNotice
onConfirm = () => markNoticeRead(nextUnreadNotice)
} else if (lostAccounts && lostAccounts.length > 0) {
notice = generateLostAccountsNotice(lostAccounts)
onConfirm = () => markAccountsFound()

View File

@ -647,7 +647,7 @@ ConfirmSendEther.prototype.gatherTxMeta = function () {
const state = this.state
const txData = clone(state.txData) || clone(props.txData)
const { gasPrice: sendGasPrice, gas: sendGasLimit } = props.send
const { gasPrice: sendGasPrice, gasLimit: sendGasLimit } = props.send
const {
lastGasPrice,
txParams: {

View File

@ -651,7 +651,7 @@ ConfirmSendToken.prototype.gatherTxMeta = function () {
const state = this.state
const txData = clone(state.txData) || clone(props.txData)
const { gasPrice: sendGasPrice, gas: sendGasLimit } = props.send
const { gasPrice: sendGasPrice, gasLimit: sendGasLimit } = props.send
const {
lastGasPrice,
txParams: {

View File

@ -178,7 +178,14 @@ SignatureRequest.prototype.renderBody = function () {
rows = data
} else if (type === 'eth_sign') {
rows = [{ name: this.context.t('message'), value: data }]
notice = this.context.t('signNotice')
notice = [this.context.t('signNotice'),
h('span.request-signature__help-link', {
onClick: () => {
global.platform.openWindow({
url: 'https://consensys.zendesk.com/hc/en-us/articles/360004427792',
})
},
}, this.context.t('learnMore'))]
}
return h('div.request-signature__body', {}, [

View File

@ -183,6 +183,12 @@
padding: 6px 18px 15px;
}
&__help-link {
cursor: pointer;
text-decoration: underline;
color: $curious-blue;
}
&__footer {
width: 100%;
display: flex;

View File

@ -21,7 +21,7 @@ function reduceMetamask (state, action) {
identities: {},
unapprovedTxs: {},
noActiveNotices: true,
lastUnreadNotice: undefined,
nextUnreadNotice: undefined,
frequentRpcList: [],
addressBook: [],
selectedTokenAddress: null,
@ -65,7 +65,7 @@ function reduceMetamask (state, action) {
case actions.SHOW_NOTICE:
return extend(metamaskState, {
noActiveNotices: false,
lastUnreadNotice: action.value,
nextUnreadNotice: action.value,
})
case actions.CLEAR_NOTICES: