Merge pull request #5025 from MetaMask/v4.9.2

v4.9.2
feature/default_network_editable
Dan Finlay 6 years ago committed by GitHub
commit ef7e638dda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 34
      .github/ISSUE_TEMPLATE/bug-report.md
  2. 14
      .github/ISSUE_TEMPLATE/feature-request.md
  3. 9
      .github/ISSUE_TEMPLATE/support-request-or-question.md
  4. 1
      .npmrc
  5. 14
      CHANGELOG.md
  6. 3
      app/_locales/en/messages.json
  7. 2
      app/manifest.json
  8. 4
      app/scripts/background.js
  9. 2
      app/scripts/controllers/detect-tokens.js
  10. 94
      app/scripts/controllers/preferences.js
  11. 6
      app/scripts/lib/ipfsContent.js
  12. 4
      app/scripts/lib/setupRaven.js
  13. 3
      app/scripts/metamask-controller.js
  14. 40
      app/scripts/migrations/028.js
  15. 1
      app/scripts/migrations/index.js
  16. 1
      mascara/src/app/first-time/confirm-seed-screen.js
  17. 9
      mascara/src/app/first-time/import-seed-phrase-screen.js
  18. 64664
      package-lock.json
  19. 10
      package.json
  20. 5
      test/e2e/beta/contract-test/contract.js
  21. 3
      test/e2e/beta/contract-test/index.html
  22. 2
      test/e2e/beta/from-import-beta-ui.spec.js
  23. 100
      test/e2e/beta/metamask-beta-ui.spec.js
  24. 4
      test/e2e/beta/run-all.sh
  25. 5
      test/integration/lib/tx-list-items.js
  26. 21
      test/unit/app/controllers/detect-tokens-test.js
  27. 158
      test/unit/app/controllers/preferences-controller-test.js
  28. 46
      test/unit/migrations/028-test.js
  29. 23
      ui/app/actions.js
  30. 2
      ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js
  31. 8
      ui/app/components/tx-list-item.js
  32. 18
      ui/app/components/tx-list.js
  33. 2
      ui/app/constants/error-keys.js
  34. 2
      ui/app/first-time/init-menu.js
  35. 6
      ui/app/reducers/app.js
  36. 10
      ui/app/selectors/confirm-transaction.js

@ -0,0 +1,34 @@
---
name: Bug Report
about: Using MetaMask, but it's not working as you expect?
---
<!--
BEFORE SUBMITTING: PLEASE SEARCH TO MAKE SURE THIS ISSUE HAS NOT BEEN SUBMITTED
-->
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Browser details (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- MetaMask Version [e.g. 4.9.0]
- Old UI or New / Beta UI?
**Additional context**
Add any other context about the problem here.

@ -0,0 +1,14 @@
---
name: Feature Request
about: Looking for a feature that doesn't exist? Let us know!
---
**What problem are you trying to solve?**
A short description of what you're trying to do. E.g., "My users need to wrap ETH, but they're intimidated by the confirm screen..." or "I'm trying to debug my application, and XYZ..."
**Describe the solution you'd like**
A clear and concise description of what you want to happen. Try to also include any alternative solutions you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

@ -0,0 +1,9 @@
---
name: Support Request or Question
about: Have a question about how to use MetaMask?
---
FOR USER QUESTIONS, PLEASE DO NOT OPEN A GITHUB ISSUE - IT WILL NOT BE HANDLED HERE.
INSTEAD, PLEASE EMAIL SUPPORT@METAMASK.IO WITH A DESCRIPTION OF YOUR PROBLEM.

@ -1 +0,0 @@
engine-strict=true

@ -2,7 +2,19 @@
## Current Develop Branch
## 4.9.0 Mon Aug 06 2018
## 4.9.2 Mon Aug 09 2018
- [#5020](https://github.com/MetaMask/metamask-extension/pull/5020): Fix bug in migration #28 ( moving tokens to specific accounts )
## 4.9.1 Mon Aug 09 2018
- [#4884](https://github.com/MetaMask/metamask-extension/pull/4884): Allow to have tokens per account and network.
- [#4989](https://github.com/MetaMask/metamask-extension/pull/4989): Continue to use original signedTypedData.
- [#5010](https://github.com/MetaMask/metamask-extension/pull/5010): Fix ENS resolution issues.
- [#5000](https://github.com/MetaMask/metamask-extension/pull/5000): Show error while allowing confirmation of tx where simulation fails.
- [#4995](https://github.com/MetaMask/metamask-extension/pull/4995): Shows retry button on dApp initialized transactions.
## 4.9.0 Mon Aug 07 2018
- [#4926](https://github.com/MetaMask/metamask-extension/pull/4926): Show retry button on the latest tx of the earliest nonce.
- [#4888](https://github.com/MetaMask/metamask-extension/pull/4888): Suggest using the new user interface.

@ -510,6 +510,9 @@
"invalidRPC": {
"message": "Invalid RPC URI"
},
"invalidSeedPhrase": {
"message": "Invalid seed phrase"
},
"jsonFail": {
"message": "Something went wrong. Please make sure your JSON file is properly formatted."
},

@ -1,7 +1,7 @@
{
"name": "__MSG_appName__",
"short_name": "__MSG_appName__",
"version": "4.9.0",
"version": "4.9.2",
"manifest_version": 2,
"author": "https://metamask.io",
"description": "__MSG_appDescription__",

@ -44,8 +44,8 @@ const notificationManager = new NotificationManager()
global.METAMASK_NOTIFIER = notificationManager
// setup sentry error reporting
const releaseVersion = platform.getVersion()
const raven = setupRaven({ releaseVersion })
const release = platform.getVersion()
const raven = setupRaven({ release })
// browser check if it is Edge - https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
// Internet Explorer 6-11

@ -85,7 +85,7 @@ class DetectTokensController {
set preferences (preferences) {
if (!preferences) { return }
this._preferences = preferences
preferences.store.subscribe(({ tokens }) => { this.tokenAddresses = tokens.map((obj) => { return obj.address }) })
preferences.store.subscribe(({ tokens = [] }) => { this.tokenAddresses = tokens.map((obj) => { return obj.address }) })
preferences.store.subscribe(({ selectedAddress }) => {
if (this.selectedAddress !== selectedAddress) {
this.selectedAddress = selectedAddress

@ -13,6 +13,7 @@ class PreferencesController {
* @property {array} store.frequentRpcList A list of custom rpcs to provide the user
* @property {string} store.currentAccountTab Indicates the selected tab in the ui
* @property {array} store.tokens The tokens the user wants display in their token lists
* @property {object} store.accountTokens The tokens stored per account and then per network type
* @property {boolean} store.useBlockie The users preference for blockie identicons within the UI
* @property {object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the
* user wishes to see that feature
@ -24,6 +25,7 @@ class PreferencesController {
const initState = extend({
frequentRpcList: [],
currentAccountTab: 'history',
accountTokens: {},
tokens: [],
useBlockie: false,
featureFlags: {},
@ -33,8 +35,9 @@ class PreferencesController {
}, opts.initState)
this.diagnostics = opts.diagnostics
this.network = opts.network
this.store = new ObservableStore(initState)
this._subscribeProviderType()
}
// PUBLIC METHODS
@ -77,12 +80,19 @@ class PreferencesController {
*/
setAddresses (addresses) {
const oldIdentities = this.store.getState().identities
const oldAccountTokens = this.store.getState().accountTokens
const identities = addresses.reduce((ids, address, index) => {
const oldId = oldIdentities[address] || {}
ids[address] = {name: `Account ${index + 1}`, address, ...oldId}
return ids
}, {})
this.store.updateState({ identities })
const accountTokens = addresses.reduce((tokens, address) => {
const oldTokens = oldAccountTokens[address] || {}
tokens[address] = oldTokens
return tokens
}, {})
this.store.updateState({ identities, accountTokens })
}
/**
@ -93,11 +103,13 @@ class PreferencesController {
*/
removeAddress (address) {
const identities = this.store.getState().identities
const accountTokens = this.store.getState().accountTokens
if (!identities[address]) {
throw new Error(`${address} can't be deleted cause it was not found`)
}
delete identities[address]
this.store.updateState({ identities })
delete accountTokens[address]
this.store.updateState({ identities, accountTokens })
// If the selected account is no longer valid,
// select an arbitrary other account:
@ -117,14 +129,17 @@ class PreferencesController {
*/
addAddresses (addresses) {
const identities = this.store.getState().identities
const accountTokens = this.store.getState().accountTokens
addresses.forEach((address) => {
// skip if already exists
if (identities[address]) return
// add missing identity
const identityCount = Object.keys(identities).length
accountTokens[address] = {}
identities[address] = { name: `Account ${identityCount + 1}`, address }
})
this.store.updateState({ identities })
this.store.updateState({ identities, accountTokens })
}
/*
@ -175,15 +190,15 @@ class PreferencesController {
* Setter for the `selectedAddress` property
*
* @param {string} _address A new hex address for an account
* @returns {Promise<void>} Promise resolves with undefined
* @returns {Promise<void>} Promise resolves with tokens
*
*/
setSelectedAddress (_address) {
return new Promise((resolve, reject) => {
const address = normalizeAddress(_address)
this.store.updateState({ selectedAddress: address })
resolve()
})
const address = normalizeAddress(_address)
this._updateTokens(address)
this.store.updateState({ selectedAddress: address })
const tokens = this.store.getState().tokens
return Promise.resolve(tokens)
}
/**
@ -232,9 +247,7 @@ class PreferencesController {
} else {
tokens.push(newEntry)
}
this.store.updateState({ tokens })
this._updateAccountTokens(tokens)
return Promise.resolve(tokens)
}
@ -247,10 +260,8 @@ class PreferencesController {
*/
removeToken (rawAddress) {
const tokens = this.store.getState().tokens
const updatedTokens = tokens.filter(token => token.address !== rawAddress)
this.store.updateState({ tokens: updatedTokens })
this._updateAccountTokens(updatedTokens)
return Promise.resolve(updatedTokens)
}
@ -376,6 +387,57 @@ class PreferencesController {
//
// PRIVATE METHODS
//
/**
* Subscription to network provider type.
*
*
*/
_subscribeProviderType () {
this.network.providerStore.subscribe(() => {
const { tokens } = this._getTokenRelatedStates()
this.store.updateState({ tokens })
})
}
/**
* Updates `accountTokens` and `tokens` of current account and network according to it.
*
* @param {array} tokens Array of tokens to be updated.
*
*/
_updateAccountTokens (tokens) {
const { accountTokens, providerType, selectedAddress } = this._getTokenRelatedStates()
accountTokens[selectedAddress][providerType] = tokens
this.store.updateState({ accountTokens, tokens })
}
/**
* Updates `tokens` of current account and network.
*
* @param {string} selectedAddress Account address to be updated with.
*
*/
_updateTokens (selectedAddress) {
const { tokens } = this._getTokenRelatedStates(selectedAddress)
this.store.updateState({ tokens })
}
/**
* A getter for `tokens` and `accountTokens` related states.
*
* @param {string} selectedAddress A new hex address for an account
* @returns {Object.<array, object, string, string>} States to interact with tokens in `accountTokens`
*
*/
_getTokenRelatedStates (selectedAddress) {
const accountTokens = this.store.getState().accountTokens
if (!selectedAddress) selectedAddress = this.store.getState().selectedAddress
const providerType = this.network.providerStore.getState().type
if (!(selectedAddress in accountTokens)) accountTokens[selectedAddress] = {}
if (!(providerType in accountTokens[selectedAddress])) accountTokens[selectedAddress][providerType] = []
const tokens = accountTokens[selectedAddress][providerType]
return { tokens, accountTokens, providerType, selectedAddress }
}
}
module.exports = PreferencesController

@ -5,7 +5,7 @@ module.exports = function (provider) {
function ipfsContent (details) {
const name = details.url.substring(7, details.url.length - 1)
let clearTime = null
extension.tabs.getSelected(null, tab => {
extension.tabs.query({active: true}, tab => {
extension.tabs.update(tab.id, { url: 'loading.html' })
clearTime = setTimeout(() => {
@ -34,11 +34,11 @@ module.exports = function (provider) {
return { cancel: true }
}
extension.webRequest.onBeforeRequest.addListener(ipfsContent, {urls: ['*://*.eth/', '*://*.test/']})
extension.webRequest.onErrorOccurred.addListener(ipfsContent, {urls: ['*://*.eth/', '*://*.test/']})
return {
remove () {
extension.webRequest.onBeforeRequest.removeListener(ipfsContent)
extension.webRequest.onErrorOccurred.removeListener(ipfsContent)
},
}
}

@ -8,7 +8,7 @@ module.exports = setupRaven
// Setup raven / sentry remote error reporting
function setupRaven (opts) {
const { releaseVersion } = opts
const { release } = opts
let ravenTarget
// detect brave
const isBrave = Boolean(window.chrome.ipcRenderer)
@ -22,7 +22,7 @@ function setupRaven (opts) {
}
const client = Raven.config(ravenTarget, {
releaseVersion,
release,
transport: function (opts) {
opts.data.extra.isBrave = isBrave
const report = opts.data

@ -87,6 +87,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.preferencesController = new PreferencesController({
initState: initState.PreferencesController,
initLangCode: opts.initLangCode,
network: this.networkController,
})
// currency controller
@ -1436,7 +1437,7 @@ module.exports = class MetamaskController extends EventEmitter {
}
/**
* A method for activating the retrieval of price data and auto detect tokens,
* A method for activating the retrieval of price data,
* which should only be fetched when the UI is visible.
* @private
* @param {boolean} active - True if price data should be getting fetched.

@ -0,0 +1,40 @@
// next version number
const version = 28
/*
normalizes txParams on unconfirmed txs
*/
const clone = require('clone')
module.exports = {
version,
migrate: async function (originalVersionedData) {
const versionedData = clone(originalVersionedData)
versionedData.meta.version = version
const state = versionedData.data
const newState = transformState(state)
versionedData.data = newState
return versionedData
},
}
function transformState (state) {
const newState = state
if (newState.PreferencesController) {
if (newState.PreferencesController.tokens && newState.PreferencesController.identities) {
const identities = newState.PreferencesController.identities
const tokens = newState.PreferencesController.tokens
newState.PreferencesController.accountTokens = {}
for (const identity in identities) {
newState.PreferencesController.accountTokens[identity] = {'mainnet': tokens}
}
newState.PreferencesController.tokens = []
}
}
return newState
}

@ -38,4 +38,5 @@ module.exports = [
require('./025'),
require('./026'),
require('./027'),
require('./028'),
]

@ -106,6 +106,7 @@ class ConfirmSeedScreen extends Component {
key={i}
className={classnames('backup-phrase__confirm-seed-option', {
'backup-phrase__confirm-seed-option--selected': isSelected,
'backup-phrase__confirm-seed-option--unselected': !isSelected,
})}
onClick={() => {
if (!isSelected) {

@ -1,3 +1,4 @@
import {validateMnemonic} from 'bip39'
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
@ -39,8 +40,12 @@ class ImportSeedPhraseScreen extends Component {
handleSeedPhraseChange (seedPhrase) {
let seedPhraseError = null
if (seedPhrase && this.parseSeedPhrase(seedPhrase).split(' ').length !== 12) {
seedPhraseError = this.context.t('seedPhraseReq')
if (seedPhrase) {
if (this.parseSeedPhrase(seedPhrase).split(' ').length !== 12) {
seedPhraseError = this.context.t('seedPhraseReq')
} else if (!validateMnemonic(seedPhrase)) {
seedPhraseError = this.context.t('invalidSeedPhrase')
}
}
this.setState({ seedPhrase, seedPhraseError })

64664
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -74,7 +74,7 @@
]
},
"dependencies": {
"@material-ui/core": "^1.0.0",
"@material-ui/core": "1.0.0",
"abi-decoder": "^1.0.9",
"asmcrypto.js": "0.22.0",
"async": "^2.5.0",
@ -105,7 +105,7 @@
"eth-bin-to-ops": "^1.0.1",
"eth-contract-metadata": "github:MetaMask/eth-contract-metadata#master",
"eth-ens-namehash": "^2.0.8",
"eth-hd-keyring": "^2.0.0",
"eth-hd-keyring": "^1.2.2",
"eth-json-rpc-filters": "^1.2.6",
"eth-json-rpc-infura": "^3.0.0",
"eth-method-registry": "^1.0.0",
@ -198,7 +198,6 @@
"semaphore": "^1.0.5",
"semver": "^5.4.1",
"shallow-copy": "0.0.1",
"superstatic": "^5.0.2",
"sw-controller": "^1.0.3",
"sw-stream": "^2.0.2",
"textarea-caret": "^3.0.1",
@ -228,7 +227,7 @@
"brfs": "^1.6.1",
"browserify": "^16.1.1",
"chai": "^4.1.0",
"chromedriver": "2.36.0",
"chromedriver": "^2.41.0",
"clipboardy": "^1.2.3",
"compression": "^1.7.1",
"coveralls": "^3.0.0",
@ -245,7 +244,7 @@
"eslint-plugin-mocha": "^5.0.0",
"eslint-plugin-react": "^7.4.0",
"eth-json-rpc-middleware": "^1.6.0",
"eth-keyring-controller": "^4.0.0",
"eth-keyring-controller": "^3.3.1",
"file-loader": "^1.1.11",
"fs-promise": "^2.0.3",
"ganache-cli": "^6.1.0",
@ -308,6 +307,7 @@
"shell-parallel": "^1.0.3",
"sinon": "^5.0.0",
"source-map": "^0.7.2",
"static-server": "^2.2.1",
"style-loader": "^0.21.0",
"stylelint-config-standard": "^18.2.0",
"tape": "^4.5.1",

@ -50,15 +50,20 @@ deployButton.addEventListener('click', async function (event) {
console.log(`contract`, contract)
document.getElementById('contractStatus').innerHTML = 'Deployed'
depositButton.addEventListener('click', function (event) {
document.getElementById('contractStatus').innerHTML = 'Deposit initiated'
contract.deposit({ from: web3.eth.accounts[0], value: '0x3782dace9d900000' }, function (result) {
console.log(result)
document.getElementById('contractStatus').innerHTML = 'Deposit completed'
})
})
withdrawButton.addEventListener('click', function (event) {
contract.withdraw('0xde0b6b3a7640000', { from: web3.eth.accounts[0] }, function (result) {
console.log(result)
document.getElementById('contractStatus').innerHTML = 'Withdrawn'
})
})
}

@ -10,6 +10,9 @@
<button id="depositButton">Deposit</button>
<button id="withdrawButton">Withdraw</button>
</div>
<div id="contractStatus" style="display: flex; font-size: 1rem;">
Not yet deployed
</div>
</div>
<div style="display: flex; flex-flow: column;">
<div style="display: flex; font-size: 1.25rem;">Send eth</div>

@ -232,7 +232,7 @@ describe('Using MetaMask with an existing account', function () {
const [localhost] = await findElements(driver, By.xpath(`//span[contains(text(), 'Localhost')]`))
await localhost.click()
await delay(largeDelayMs * 2)
await delay(largeDelayMs)
})
it('choose Create Account from the account menu', async () => {

@ -195,7 +195,16 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
})
async function retypeSeedPhrase (words, wasReloaded) {
async function clickWordAndWait (word) {
const xpathClass = 'backup-phrase__confirm-seed-option backup-phrase__confirm-seed-option--unselected'
const xpath = `//button[@class='${xpathClass}' and contains(text(), '${word}')]`
const word0 = await findElement(driver, By.xpath(xpath), 10000)
await word0.click()
await delay(tinyDelayMs)
}
async function retypeSeedPhrase (words, wasReloaded, count = 0) {
try {
if (wasReloaded) {
const byRevealButton = By.css('.backup-phrase__secret-blocker .backup-phrase__reveal-button')
@ -209,67 +218,26 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
}
const word0 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[0]}')]`), 10000)
await word0.click()
await delay(tinyDelayMs)
const word1 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[1]}')]`), 10000)
await word1.click()
await delay(tinyDelayMs)
const word2 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[2]}')]`), 10000)
await word2.click()
await delay(tinyDelayMs)
const word3 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[3]}')]`), 10000)
await word3.click()
await delay(tinyDelayMs)
const word4 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[4]}')]`), 10000)
await word4.click()
await delay(tinyDelayMs)
const word5 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[5]}')]`), 10000)
await word5.click()
await delay(tinyDelayMs)
const word6 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[6]}')]`), 10000)
await clickWordAndWait(words[0])
await clickWordAndWait(words[1])
await clickWordAndWait(words[2])
await clickWordAndWait(words[3])
await clickWordAndWait(words[4])
await clickWordAndWait(words[5])
await clickWordAndWait(words[6])
await clickWordAndWait(words[7])
await clickWordAndWait(words[8])
await clickWordAndWait(words[9])
await clickWordAndWait(words[10])
await clickWordAndWait(words[11])
await word6.click()
await delay(tinyDelayMs)
const word7 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[7]}')]`), 10000)
await word7.click()
await delay(tinyDelayMs)
const word8 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[8]}')]`), 10000)
await word8.click()
await delay(tinyDelayMs)
const word9 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[9]}')]`), 10000)
await word9.click()
await delay(tinyDelayMs)
const word10 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[10]}')]`), 10000)
await word10.click()
await delay(tinyDelayMs)
const word11 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[11]}')]`), 10000)
await word11.click()
await delay(tinyDelayMs)
} catch (e) {
await loadExtension(driver, extensionId)
await retypeSeedPhrase(words, true)
if (count > 2) {
throw e
} else {
await loadExtension(driver, extensionId)
await retypeSeedPhrase(words, true, count + 1)
}
}
}
@ -516,7 +484,7 @@ describe('MetaMask', function () {
it('displays the contract creation data', async () => {
const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`))
dataTab.click()
await dataTab.click()
await delay(regularDelayMs)
await findElement(driver, By.xpath(`//div[contains(text(), '127.0.0.1')]`))
@ -526,7 +494,7 @@ describe('MetaMask', function () {
assert.equal(confirmDataText.match(/0x608060405234801561001057600080fd5b5033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff/))
const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`))
detailsTab.click()
await detailsTab.click()
await delay(regularDelayMs)
})
@ -547,9 +515,15 @@ describe('MetaMask', function () {
await driver.switchTo().window(dapp)
await delay(regularDelayMs)
let contractStatus = await driver.findElement(By.css('#contractStatus'))
await driver.wait(until.elementTextMatches(contractStatus, /Deployed/))
const depositButton = await findElement(driver, By.css('#depositButton'))
await depositButton.click()
await delay(regularDelayMs)
await delay(largeDelayMs)
contractStatus = await driver.findElement(By.css('#contractStatus'))
await driver.wait(until.elementTextMatches(contractStatus, /Deposit\sinitiated/))
await driver.switchTo().window(extension)
await delay(largeDelayMs)

@ -6,5 +6,5 @@ set -o pipefail
export PATH="$PATH:./node_modules/.bin"
shell-parallel -s 'npm run ganache:start' -x 'sleep 5 && superstatic test/e2e/beta/contract-test/ --port 8080 --host 127.0.0.1' -x 'sleep 5 && mocha test/e2e/beta/metamask-beta-ui.spec'
shell-parallel -s 'npm run ganache:start -- -d' -x 'sleep 5 && superstatic test/e2e/beta/contract-test/ --port 8080 --host 127.0.0.1' -x 'sleep 5 && mocha test/e2e/beta/from-import-beta-ui.spec'
shell-parallel -s 'npm run ganache:start' -x 'sleep 5 && static-server test/e2e/beta/contract-test/ --port 8080' -x 'sleep 5 && mocha test/e2e/beta/metamask-beta-ui.spec'
shell-parallel -s 'npm run ganache:start -- -d' -x 'sleep 5 && static-server test/e2e/beta/contract-test/ --port 8080' -x 'sleep 5 && mocha test/e2e/beta/from-import-beta-ui.spec'

@ -14,6 +14,11 @@ QUnit.test('renders list items successfully', (assert) => {
})
})
global.ethQuery = global.ethQuery || {}
global.ethQuery.getTransactionCount = (_, cb) => {
cb(null, '0x3')
}
async function runTxListItemsTest (assert, done) {
console.log('*** start runTxListItemsTest')
const selectState = await queryAsync($, 'select')

@ -7,10 +7,11 @@ const PreferencesController = require('../../../../app/scripts/controllers/prefe
describe('DetectTokensController', () => {
const sandbox = sinon.createSandbox()
let clock
let keyringMemStore
before(async () => {
let clock, keyringMemStore, network, preferences
beforeEach(async () => {
keyringMemStore = new ObservableStore({ isUnlocked: false})
network = new NetworkController({ provider: { type: 'mainnet' }})
preferences = new PreferencesController({ network })
})
after(() => {
sandbox.restore()
@ -25,9 +26,7 @@ describe('DetectTokensController', () => {
it('should be called on every polling period', async () => {
clock = sandbox.useFakeTimers()
const network = new NetworkController()
network.setProviderType('mainnet')
const preferences = new PreferencesController()
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true
controller.isUnlocked = true
@ -45,9 +44,7 @@ describe('DetectTokensController', () => {
})
it('should not check tokens while in test network', async () => {
const network = new NetworkController()
network.setProviderType('rinkeby')
const preferences = new PreferencesController()
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true
controller.isUnlocked = true
@ -61,9 +58,7 @@ describe('DetectTokensController', () => {
})
it('should only check and add tokens while in main network', async () => {
const network = new NetworkController()
network.setProviderType('mainnet')
const preferences = new PreferencesController()
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true
controller.isUnlocked = true
@ -80,9 +75,7 @@ describe('DetectTokensController', () => {
})
it('should not detect same token while in main network', async () => {
const network = new NetworkController()
network.setProviderType('mainnet')
const preferences = new PreferencesController()
preferences.addToken('0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', 'J8T', 8)
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true
@ -100,9 +93,7 @@ describe('DetectTokensController', () => {
})
it('should trigger detect new tokens when change address', async () => {
const network = new NetworkController()
network.setProviderType('mainnet')
const preferences = new PreferencesController()
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true
controller.isUnlocked = true
@ -112,9 +103,7 @@ describe('DetectTokensController', () => {
})
it('should trigger detect new tokens when submit password', async () => {
const network = new NetworkController()
network.setProviderType('mainnet')
const preferences = new PreferencesController()
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true
controller.selectedAddress = '0x0'
@ -124,9 +113,7 @@ describe('DetectTokensController', () => {
})
it('should not trigger detect new tokens when not open or not unlocked', async () => {
const network = new NetworkController()
network.setProviderType('mainnet')
const preferences = new PreferencesController()
const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
controller.isOpen = true
controller.isUnlocked = false

@ -1,11 +1,14 @@
const assert = require('assert')
const ObservableStore = require('obs-store')
const PreferencesController = require('../../../../app/scripts/controllers/preferences')
describe('preferences controller', function () {
let preferencesController
let network
beforeEach(() => {
preferencesController = new PreferencesController()
network = {providerStore: new ObservableStore({ type: 'mainnet' })}
preferencesController = new PreferencesController({ network })
})
describe('setAddresses', function () {
@ -28,6 +31,20 @@ describe('preferences controller', function () {
})
})
it('should create account tokens for each account in the store', function () {
preferencesController.setAddresses([
'0xda22le',
'0x7e57e2',
])
const accountTokens = preferencesController.store.getState().accountTokens
assert.deepEqual(accountTokens, {
'0xda22le': {},
'0x7e57e2': {},
})
})
it('should replace its list of addresses', function () {
preferencesController.setAddresses([
'0xda22le',
@ -64,6 +81,17 @@ describe('preferences controller', function () {
assert.equal(preferencesController.store.getState().identities['0xda22le'], undefined)
})
it('should remove an address from state and respective tokens', function () {
preferencesController.setAddresses([
'0xda22le',
'0x7e57e2',
])
preferencesController.removeAddress('0xda22le')
assert.equal(preferencesController.store.getState().accountTokens['0xda22le'], undefined)
})
it('should switch accounts if the selected address is removed', function () {
preferencesController.setAddresses([
'0xda22le',
@ -158,6 +186,42 @@ describe('preferences controller', function () {
await preferencesController.addToken(address, symbol, decimals)
assert.equal(preferencesController.getTokens().length, 1, 'one token added for 2nd address')
})
it('should add token per account', async function () {
const addressFirst = '0xabcdef1234567'
const addressSecond = '0xabcdef1234568'
const symbolFirst = 'ABBR'
const symbolSecond = 'ABBB'
const decimals = 5
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.addToken(addressFirst, symbolFirst, decimals)
const tokensFirstAddress = preferencesController.getTokens()
await preferencesController.setSelectedAddress('0xda22le')
await preferencesController.addToken(addressSecond, symbolSecond, decimals)
const tokensSeconAddress = preferencesController.getTokens()
assert.notEqual(tokensFirstAddress, tokensSeconAddress, 'add different tokens for two account and tokens are equal')
})
it('should add token per network', async function () {
const addressFirst = '0xabcdef1234567'
const addressSecond = '0xabcdef1234568'
const symbolFirst = 'ABBR'
const symbolSecond = 'ABBB'
const decimals = 5
network.providerStore.updateState({ type: 'mainnet' })
await preferencesController.addToken(addressFirst, symbolFirst, decimals)
const tokensFirstAddress = preferencesController.getTokens()
network.providerStore.updateState({ type: 'rinkeby' })
await preferencesController.addToken(addressSecond, symbolSecond, decimals)
const tokensSeconAddress = preferencesController.getTokens()
assert.notEqual(tokensFirstAddress, tokensSeconAddress, 'add different tokens for two networks and tokens are equal')
})
})
describe('removeToken', function () {
@ -182,6 +246,98 @@ describe('preferences controller', function () {
const [token1] = tokens
assert.deepEqual(token1, {address: '0xb', symbol: 'B', decimals: 5})
})
it('should remove a token from its state on corresponding address', async function () {
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
await preferencesController.setSelectedAddress('0x7e57e3')
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
const initialTokensSecond = preferencesController.getTokens()
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.removeToken('0xa')
const tokensFirst = preferencesController.getTokens()
assert.equal(tokensFirst.length, 1, 'one token removed in account')
const [token1] = tokensFirst
assert.deepEqual(token1, {address: '0xb', symbol: 'B', decimals: 5})
await preferencesController.setSelectedAddress('0x7e57e3')
const tokensSecond = preferencesController.getTokens()
assert.deepEqual(tokensSecond, initialTokensSecond, 'token deleted for account')
})
it('should remove a token from its state on corresponding network', async function () {
network.providerStore.updateState({ type: 'mainnet' })
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
network.providerStore.updateState({ type: 'rinkeby' })
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
const initialTokensSecond = preferencesController.getTokens()
network.providerStore.updateState({ type: 'mainnet' })
await preferencesController.removeToken('0xa')
const tokensFirst = preferencesController.getTokens()
assert.equal(tokensFirst.length, 1, 'one token removed in network')
const [token1] = tokensFirst
assert.deepEqual(token1, {address: '0xb', symbol: 'B', decimals: 5})
network.providerStore.updateState({ type: 'rinkeby' })
const tokensSecond = preferencesController.getTokens()
assert.deepEqual(tokensSecond, initialTokensSecond, 'token deleted for network')
})
})
describe('on setSelectedAddress', function () {
it('should update tokens from its state on corresponding address', async function () {
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
await preferencesController.setSelectedAddress('0x7e57e3')
await preferencesController.addToken('0xa', 'C', 4)
await preferencesController.addToken('0xb', 'D', 5)
await preferencesController.setSelectedAddress('0x7e57e2')
const initialTokensFirst = preferencesController.getTokens()
await preferencesController.setSelectedAddress('0x7e57e3')
const initialTokensSecond = preferencesController.getTokens()
assert.notDeepEqual(initialTokensFirst, initialTokensSecond, 'tokens not equal for different accounts and tokens')
await preferencesController.setSelectedAddress('0x7e57e2')
const tokensFirst = preferencesController.getTokens()
await preferencesController.setSelectedAddress('0x7e57e3')
const tokensSecond = preferencesController.getTokens()
assert.deepEqual(tokensFirst, initialTokensFirst, 'tokens equal for same account')
assert.deepEqual(tokensSecond, initialTokensSecond, 'tokens equal for same account')
})
})
describe('on updateStateNetworkType', function () {
it('should remove a token from its state on corresponding network', async function () {
network.providerStore.updateState({ type: 'mainnet' })
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
const initialTokensFirst = preferencesController.getTokens()
network.providerStore.updateState({ type: 'rinkeby' })
await preferencesController.addToken('0xa', 'C', 4)
await preferencesController.addToken('0xb', 'D', 5)
const initialTokensSecond = preferencesController.getTokens()
assert.notDeepEqual(initialTokensFirst, initialTokensSecond, 'tokens not equal for different networks and tokens')
network.providerStore.updateState({ type: 'mainnet' })
const tokensFirst = preferencesController.getTokens()
network.providerStore.updateState({ type: 'rinkeby' })
const tokensSecond = preferencesController.getTokens()
assert.deepEqual(tokensFirst, initialTokensFirst, 'tokens equal for same network')
assert.deepEqual(tokensSecond, initialTokensSecond, 'tokens equal for same network')
})
})
})

@ -0,0 +1,46 @@
const assert = require('assert')
const migration28 = require('../../../app/scripts/migrations/028')
const oldStorage = {
'meta': {},
'data': {
'PreferencesController': {
'tokens': [{address: '0xa', symbol: 'A', decimals: 4}, {address: '0xb', symbol: 'B', decimals: 4}],
'identities': {
'0x6d14': {},
'0x3695': {},
},
},
},
}
describe('migration #28', () => {
it('should add corresponding tokens to accountTokens', (done) => {
migration28.migrate(oldStorage)
.then((newStorage) => {
const newTokens = newStorage.data.PreferencesController.tokens
const newAccountTokens = newStorage.data.PreferencesController.accountTokens
const testTokens = [{address: '0xa', symbol: 'A', decimals: 4}, {address: '0xb', symbol: 'B', decimals: 4}]
assert.equal(newTokens.length, 0, 'tokens is expected to have the length of 0')
assert.equal(newAccountTokens['0x6d14']['mainnet'].length, 2, 'tokens for address is expected to have the length of 2')
assert.equal(newAccountTokens['0x3695']['mainnet'].length, 2, 'tokens for address is expected to have the length of 2')
assert.equal(Object.keys(newAccountTokens).length, 2, 'account tokens should be created for all identities')
assert.deepEqual(newAccountTokens['0x6d14']['mainnet'], testTokens, 'tokens for address should be the same than before')
assert.deepEqual(newAccountTokens['0x3695']['mainnet'], testTokens, 'tokens for address should be the same than before')
done()
})
.catch(done)
})
it('should successfully migrate first time state', (done) => {
migration28.migrate({
meta: {},
data: require('../../../app/scripts/first-time-state'),
})
.then((migratedData) => {
assert.equal(migratedData.meta.version, migration28.version)
done()
}).catch(done)
})
})

@ -143,6 +143,8 @@ var actions = {
exportAccountComplete,
SET_ACCOUNT_LABEL: 'SET_ACCOUNT_LABEL',
setAccountLabel,
updateNetworkNonce,
SET_NETWORK_NONCE: 'SET_NETWORK_NONCE',
// tx conf screen
COMPLETED_TX: 'COMPLETED_TX',
TRANSACTION_ERROR: 'TRANSACTION_ERROR',
@ -1483,11 +1485,12 @@ function showAccountDetail (address) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
log.debug(`background.setSelectedAddress`)
background.setSelectedAddress(address, (err) => {
background.setSelectedAddress(address, (err, tokens) => {
dispatch(actions.hideLoadingIndication())
if (err) {
return dispatch(actions.displayWarning(err.message))
}
dispatch(updateTokens(tokens))
dispatch({
type: actions.SHOW_ACCOUNT_DETAIL,
value: address,
@ -2115,6 +2118,24 @@ function updateFeatureFlags (updatedFeatureFlags) {
}
}
function setNetworkNonce (networkNonce) {
return {
type: actions.SET_NETWORK_NONCE,
value: networkNonce,
}
}
function updateNetworkNonce (address) {
return (dispatch) => {
return new Promise((resolve, reject) => {
global.ethQuery.getTransactionCount(address, (err, data) => {
dispatch(setNetworkNonce(data))
resolve(data)
})
})
}
}
function setMouseUserState (isMouseUser) {
return {
type: actions.SET_MOUSE_USER_STATE,

@ -124,7 +124,7 @@ export default class ConfirmTransactionBase extends Component {
if (simulationFails) {
return {
valid: false,
valid: true,
errorKey: TRANSACTION_ERROR_KEY,
}
}

@ -35,6 +35,7 @@ function mapStateToProps (state) {
currentCurrency: getCurrentCurrency(state),
contractExchangeRates: state.metamask.contractExchangeRates,
selectedAddressTxList: state.metamask.selectedAddressTxList,
networkNonce: state.appState.networkNonce,
}
}
@ -209,6 +210,7 @@ TxListItem.prototype.showRetryButton = function () {
selectedAddressTxList,
transactionId,
txParams,
networkNonce,
} = this.props
if (!txParams) {
return false
@ -222,11 +224,7 @@ TxListItem.prototype.showRetryButton = function () {
const currentTxIsLatestWithNonce = lastSubmittedTxWithCurrentNonce &&
lastSubmittedTxWithCurrentNonce.id === transactionId
if (currentSubmittedTxs.length > 0) {
const earliestSubmitted = currentSubmittedTxs.reduce((tx1, tx2) => {
if (tx1.submittedTime < tx2.submittedTime) return tx1
return tx2
})
currentTxSharesEarliestNonce = currentNonce === earliestSubmitted.txParams.nonce
currentTxSharesEarliestNonce = currentNonce === networkNonce
}
return currentTxSharesEarliestNonce && currentTxIsLatestWithNonce && Date.now() - transactionSubmittedTime > 30000

@ -8,7 +8,7 @@ const selectors = require('../selectors')
const TxListItem = require('./tx-list-item')
const ShiftListItem = require('./shift-list-item')
const { formatDate } = require('../util')
const { showConfTxPage } = require('../actions')
const { showConfTxPage, updateNetworkNonce } = require('../actions')
const classnames = require('classnames')
const { tokenInfoGetter } = require('../token-util')
const { withRouter } = require('react-router-dom')
@ -28,12 +28,14 @@ function mapStateToProps (state) {
return {
txsToRender: selectors.transactionsSelector(state),
conversionRate: selectors.conversionRateSelector(state),
selectedAddress: selectors.getSelectedAddress(state),
}
}
function mapDispatchToProps (dispatch) {
return {
showConfTxPage: ({ id }) => dispatch(showConfTxPage({ id })),
updateNetworkNonce: (address) => dispatch(updateNetworkNonce(address)),
}
}
@ -44,6 +46,20 @@ function TxList () {
TxList.prototype.componentWillMount = function () {
this.tokenInfoGetter = tokenInfoGetter()
this.props.updateNetworkNonce(this.props.selectedAddress)
}
TxList.prototype.componentDidUpdate = function (prevProps) {
const oldTxsToRender = prevProps.txsToRender
const {
txsToRender: newTxsToRender,
selectedAddress,
updateNetworkNonce,
} = this.props
if (newTxsToRender.length > oldTxsToRender.length) {
updateNetworkNonce(selectedAddress)
}
}
TxList.prototype.render = function () {

@ -1,3 +1,3 @@
export const INSUFFICIENT_FUNDS_ERROR_KEY = 'insufficientFunds'
export const GAS_LIMIT_TOO_LOW_ERROR_KEY = 'gasLimitTooLow'
export const TRANSACTION_ERROR = 'transactionError'
export const TRANSACTION_ERROR_KEY = 'transactionError'

@ -130,7 +130,7 @@ class InitializeMenuScreen extends Component {
textDecoration: 'underline',
marginTop: '32px',
},
}, 'Use classic interface'),
}, this.context.t('classicInterface')),
]),
])

@ -65,6 +65,7 @@ function reduceApp (state, action) {
buyView: {},
isMouseUser: false,
gasIsLoading: false,
networkNonce: null,
}, state.appState)
switch (action.type) {
@ -701,6 +702,11 @@ function reduceApp (state, action) {
gasIsLoading: false,
})
case actions.SET_NETWORK_NONCE:
return extend(appState, {
networkNonce: action.value,
})
default:
return appState
}

@ -147,14 +147,20 @@ export const tokenAmountAndToAddressSelector = createSelector(
export const approveTokenAmountAndToAddressSelector = createSelector(
tokenDataParamsSelector,
params => {
tokenDecimalsSelector,
(params, tokenDecimals) => {
let toAddress = ''
let tokenAmount = 0
if (params && params.length) {
toAddress = params.find(param => param.name === TOKEN_PARAM_SPENDER).value
const value = Number(params.find(param => param.name === TOKEN_PARAM_VALUE).value)
tokenAmount = roundExponential(value)
if (tokenDecimals) {
tokenAmount = calcTokenAmount(value, tokenDecimals)
}
tokenAmount = roundExponential(tokenAmount)
}
return {

Loading…
Cancel
Save