Merge branch 'develop' into i4409-i4410-ens-input-enhancements

feature/default_network_editable
Dan 7 years ago
commit 06307ef8ae
  1. 10
      CHANGELOG.md
  2. 6
      app/_locales/en/messages.json
  3. 2
      app/manifest.json
  4. 13
      app/scripts/account-import-strategies/index.js
  5. 14
      app/scripts/background.js
  6. 9
      app/scripts/controllers/transactions/index.js
  7. 2
      app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js
  8. 14
      app/scripts/controllers/transactions/lib/recipient-blacklist-config.json
  9. 17
      app/scripts/controllers/transactions/lib/recipient-blacklist.js
  10. 54
      app/scripts/controllers/transactions/nonce-tracker.js
  11. 4
      app/scripts/controllers/transactions/pending-tx-tracker.js
  12. 23
      app/scripts/inpage.js
  13. 24
      app/scripts/lib/createStreamSink.js
  14. 20
      app/scripts/lib/get-first-preferred-lang-code.js
  15. 31
      app/scripts/metamask-controller.js
  16. 33
      app/scripts/notice-controller.js
  17. 2
      development/states/conf-tx.json
  18. 2
      development/states/first-time.json
  19. 2
      development/states/notice.json
  20. 14
      mascara/src/app/first-time/notice-screen.js
  21. 6
      notices/archive/notice_4.md
  22. 27
      notices/notice-delete.js
  23. 33
      notices/notice-generator.js
  24. 1
      notices/notice-nonce.json
  25. 34
      notices/notices.js
  26. 1
      notices/notices.json
  27. 6
      old-ui/app/app.js
  28. 12990
      package-lock.json
  29. 3
      package.json
  30. 29
      test/e2e/beta/from-import-beta-ui.spec.js
  31. 25
      test/e2e/beta/metamask-beta-ui.spec.js
  32. 2
      test/e2e/func.js
  33. 25
      test/e2e/metamask.spec.js
  34. 4
      test/integration/lib/send-new-ui.js
  35. 62
      test/unit/app/account-import-strategies.spec.js
  36. 50
      test/unit/app/controllers/notice-controller-test.js
  37. 17
      test/unit/test-utils.js
  38. 4
      ui/app/app.js
  39. 53
      ui/app/components/dropdowns/token-menu-dropdown.js
  40. 8
      ui/app/components/pages/home.js
  41. 12
      ui/app/components/pages/notice.js
  42. 7
      ui/app/components/send/currency-display.js
  43. 16
      ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js
  44. 9
      ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js
  45. 2
      ui/app/components/send_/send-content/send-content.component.js
  46. 8
      ui/app/components/send_/send.component.js
  47. 2
      ui/app/components/send_/send.container.js
  48. 46
      ui/app/components/send_/send.utils.js
  49. 14
      ui/app/components/send_/tests/send-component.test.js
  50. 2
      ui/app/components/send_/tests/send-container.test.js
  51. 42
      ui/app/components/send_/tests/send-utils.test.js
  52. 9
      ui/app/components/signature-request.js
  53. 11
      ui/app/conversion-util.js
  54. 6
      ui/app/css/itcss/components/request-signature.scss
  55. 6
      ui/app/css/itcss/components/token-list.scss
  56. 4
      ui/app/reducers/metamask.js

@ -2,7 +2,15 @@
## Current Master ## Current Master
- Fix bug where account reset did not work with custom RPC providers. ## 4.8.0 Thur Jun 14 2018
- [#4513](https://github.com/MetaMask/metamask-extension/pull/4513): Attempting to import an empty private key will now show a clear error.
- [#4570](https://github.com/MetaMask/metamask-extension/pull/4570): Fix bug where metamask data would stop being written to disk after prolonged use.
- [#4523](https://github.com/MetaMask/metamask-extension/pull/4523): Fix bug where account reset did not work with custom RPC providers.
- [#4524](https://github.com/MetaMask/metamask-extension/pull/4524): Fix for Brave i18n getAcceptLanguages.
- [#4557](https://github.com/MetaMask/metamask-extension/pull/4557): Fix bug where nonce mutex was never released.
- [#4566](https://github.com/MetaMask/metamask-extension/pull/4566): Add phishing notice.
- [#4591](https://github.com/MetaMask/metamask-extension/pull/4591): Allow Copying Token Addresses and link to Token on Etherscan.
## 4.7.4 Tue Jun 05 2018 ## 4.7.4 Tue Jun 05 2018

@ -146,6 +146,9 @@
"copy": { "copy": {
"message": "Copy" "message": "Copy"
}, },
"copyContractAddress": {
"message": "Copy Contract Address"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Copy to clipboard" "message": "Copy to clipboard"
}, },
@ -958,6 +961,9 @@
"viewAccount": { "viewAccount": {
"message": "View Account" "message": "View Account"
}, },
"viewOnEtherscan": {
"message": "View on Etherscan"
},
"visitWebSite": { "visitWebSite": {
"message": "Visit our web site" "message": "Visit our web site"
}, },

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

@ -16,7 +16,18 @@ const accountImporter = {
strategies: { strategies: {
'Private Key': (privateKey) => { '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 return stripped
}, },
'JSON File': (input, password) => { 'JSON File': (input, password) => {

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

@ -165,7 +165,7 @@ class TransactionController extends EventEmitter {
// add default tx params // add default tx params
txMeta = await this.addTxGasDefaults(txMeta) txMeta = await this.addTxGasDefaults(txMeta)
} catch (error) { } catch (error) {
console.log(error) log.warn(error)
this.txStateManager.setTxStatusFailed(txMeta.id, error) this.txStateManager.setTxStatusFailed(txMeta.id, error)
throw error throw error
} }
@ -264,7 +264,12 @@ class TransactionController extends EventEmitter {
// must set transaction to submitted/failed before releasing lock // must set transaction to submitted/failed before releasing lock
nonceLock.releaseLock() nonceLock.releaseLock()
} catch (err) { } 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 // must set transaction to submitted/failed before releasing lock
if (nonceLock) nonceLock.releaseLock() if (nonceLock) nonceLock.releaseLock()
// continue with error chain // continue with error chain

@ -1,4 +1,4 @@
const Config = require('./recipient-blacklist-config.json') const Config = require('./recipient-blacklist.js')
/** @module*/ /** @module*/
module.exports = { module.exports = {

@ -1,14 +0,0 @@
{
"blacklist": [
"0x627306090abab3a6e1400e9345bc60c78a8bef57",
"0xf17f52151ebef6c7334fad080c5704d77216b732",
"0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef",
"0x821aea9a577a9b44299b9c15c88cf3087f3b5544",
"0x0d1d4e623d10f9fba5db95830f7d3839406c6af2",
"0x2932b7a2355d6fecc4b5c0b6bd44cc31df247a2e",
"0x2191ef87e392377ec08e7c08eb105ef5448eced5",
"0x0f4f2ac550a1b4e2280d04c21cea7ebd822934b5",
"0x6330a553fc93768f612722bb8c2ec78ac90b3bbc",
"0x5aeda56215b167893e80b4fe645ba6d5bab767de"
]
}

@ -0,0 +1,17 @@
module.exports = {
'blacklist': [
// IDEX phisher
'0x9bcb0A9d99d815Bb87ee3191b1399b1Bcc46dc77',
// Ganache default seed phrases
'0x627306090abab3a6e1400e9345bc60c78a8bef57',
'0xf17f52151ebef6c7334fad080c5704d77216b732',
'0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef',
'0x821aea9a577a9b44299b9c15c88cf3087f3b5544',
'0x0d1d4e623d10f9fba5db95830f7d3839406c6af2',
'0x2932b7a2355d6fecc4b5c0b6bd44cc31df247a2e',
'0x2191ef87e392377ec08e7c08eb105ef5448eced5',
'0x0f4f2ac550a1b4e2280d04c21cea7ebd822934b5',
'0x6330a553fc93768f612722bb8c2ec78ac90b3bbc',
'0x5aeda56215b167893e80b4fe645ba6d5bab767de',
],
}

@ -49,29 +49,35 @@ class NonceTracker {
await this._globalMutexFree() await this._globalMutexFree()
// await lock free, then take lock // await lock free, then take lock
const releaseLock = await this._takeMutex(address) const releaseLock = await this._takeMutex(address)
// evaluate multiple nextNonce strategies try {
const nonceDetails = {} // evaluate multiple nextNonce strategies
const networkNonceResult = await this._getNetworkNextNonce(address) const nonceDetails = {}
const highestLocallyConfirmed = this._getHighestLocallyConfirmed(address) const networkNonceResult = await this._getNetworkNextNonce(address)
const nextNetworkNonce = networkNonceResult.nonce const highestLocallyConfirmed = this._getHighestLocallyConfirmed(address)
const highestSuggested = Math.max(nextNetworkNonce, highestLocallyConfirmed) 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, nonceDetails.params = {
highestSuggested, highestLocallyConfirmed,
nextNetworkNonce, 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 () { async _getCurrentBlock () {
@ -85,8 +91,8 @@ class NonceTracker {
async _globalMutexFree () { async _globalMutexFree () {
const globalMutex = this._lookupMutex('global') const globalMutex = this._lookupMutex('global')
const release = await globalMutex.acquire() const releaseLock = await globalMutex.acquire()
release() releaseLock()
} }
async _takeMutex (lockId) { async _takeMutex (lockId) {

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

@ -38,9 +38,30 @@ web3.setProvider = function () {
log.debug('MetaMask - overrode web3.setProvider') log.debug('MetaMask - overrode web3.setProvider')
} }
log.debug('MetaMask - injected web3') log.debug('MetaMask - injected web3')
// export global web3, with usage-detection
setupDappAutoReload(web3, inpageProvider.publicConfigStore) setupDappAutoReload(web3, inpageProvider.publicConfigStore)
// export global web3, with usage-detection and deprecation warning
/* TODO: Uncomment this area once auto-reload.js has been deprecated:
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 // set web3 defaultAccount
inpageProvider.publicConfigStore.subscribe(function (state) { inpageProvider.publicConfigStore.subscribe(function (state) {
web3.eth.defaultAccount = state.selectedAddress web3.eth.defaultAccount = state.selectedAddress

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

@ -2,8 +2,7 @@ const extension = require('extensionizer')
const promisify = require('pify') const promisify = require('pify')
const allLocales = require('../../_locales/index.json') const allLocales = require('../../_locales/index.json')
const isSupported = extension.i18n && extension.i18n.getAcceptLanguages const getPreferredLocales = extension.i18n ? promisify(
const getPreferredLocales = isSupported ? promisify(
extension.i18n.getAcceptLanguages, extension.i18n.getAcceptLanguages,
{ errorFirst: false } { errorFirst: false }
) : async () => [] ) : async () => []
@ -18,7 +17,21 @@ const existingLocaleCodes = allLocales.map(locale => locale.code.toLowerCase().r
* *
*/ */
async function getFirstPreferredLangCode () { async function getFirstPreferredLangCode () {
const userPreferredLocaleCodes = await getPreferredLocales() let userPreferredLocaleCodes
try {
userPreferredLocaleCodes = await getPreferredLocales()
} catch (e) {
// Brave currently throws when calling getAcceptLanguages, so this handles that.
userPreferredLocaleCodes = []
}
// safeguard for Brave Browser until they implement chrome.i18n.getAcceptLanguages
// https://github.com/MetaMask/metamask-extension/issues/4270
if (!userPreferredLocaleCodes){
userPreferredLocaleCodes = []
}
const firstPreferredLangCode = userPreferredLocaleCodes const firstPreferredLangCode = userPreferredLocaleCodes
.map(code => code.toLowerCase()) .map(code => code.toLowerCase())
.find(code => existingLocaleCodes.includes(code)) .find(code => existingLocaleCodes.includes(code))
@ -26,3 +39,4 @@ async function getFirstPreferredLangCode () {
} }
module.exports = getFirstPreferredLangCode module.exports = getFirstPreferredLangCode

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

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

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

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

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

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

@ -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](mailto: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).

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

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

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

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

12990
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -9,6 +9,7 @@
"dist": "gulp dist", "dist": "gulp dist",
"doc": "jsdoc -c development/tools/.jsdoc.json", "doc": "jsdoc -c development/tools/.jsdoc.json",
"test": "npm run test:unit && npm run test:integration && npm run lint", "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: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: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", "test:integration": "npm run test:integration:build && npm run test:flat && npm run test:mascara",
@ -45,8 +46,6 @@
"disc": "gulp disc --debug", "disc": "gulp disc --debug",
"announce": "node development/announcer.js", "announce": "node development/announcer.js",
"version:bump": "node development/run-version-bump.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" "storybook": "start-storybook -p 6006 -c .storybook"
}, },
"browserify": { "browserify": {

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

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

@ -21,7 +21,7 @@ function delay (time) {
} }
function buildChromeWebDriver (extPath) { 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() return new webdriver.Builder()
.withCapabilities({ .withCapabilities({
chromeOptions: { chromeOptions: {

@ -71,13 +71,6 @@ describe('Metamask popup page', function () {
it('matches MetaMask title', async () => { it('matches MetaMask title', async () => {
const title = await driver.getTitle() const title = await driver.getTitle()
assert.equal(title, 'MetaMask', 'title matches MetaMask') 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) await delay(300)
}) })
@ -100,6 +93,24 @@ describe('Metamask popup page', function () {
await button.click() 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 () => { it('accepts password with length of eight', async () => {
const passwordBox = await driver.findElement(By.id('password-box')) const passwordBox = await driver.findElement(By.id('password-box'))
const passwordBoxConfirm = await driver.findElement(By.id('password-box-confirm')) const passwordBoxConfirm = await driver.findElement(By.id('password-box-confirm'))

@ -117,12 +117,12 @@ async function runSendFlowTest(assert, done) {
const sendGasField = await queryAsync($, '.send-v2__gas-fee-display') const sendGasField = await queryAsync($, '.send-v2__gas-fee-display')
assert.equal( assert.equal(
sendGasField.find('.currency-display__input-wrapper > input').val(), sendGasField.find('.currency-display__input-wrapper > input').val(),
'0.000198264', '0.000021',
'send gas field should show estimated gas total' 'send gas field should show estimated gas total'
) )
assert.equal( assert.equal(
sendGasField.find('.currency-display__converted-value')[0].textContent, sendGasField.find('.currency-display__converted-value')[0].textContent,
'$0.24 USD', '$0.03 USD',
'send gas field should show estimated gas total converted to USD' 'send gas field should show estimated gas total converted to USD'
) )

@ -1,31 +1,59 @@
const assert = require('assert') const assert = require('assert')
const path = require('path') const path = require('path')
const accountImporter = require('../../../app/scripts/account-import-strategies/index')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const accountImporter = require('../../../app/scripts/account-import-strategies/index')
const { assertRejects } = require('../test-utils')
describe('Account Import Strategies', function () { describe('Account Import Strategies', function () {
const privkey = '0x4cfd3e90fc78b0f86bf7524722150bb8da9c60cd532564d7ff43f5716514f553' 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"}}' 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 () { describe('private key import', function () {
const importPrivKey = await accountImporter.importAccount('Private Key', [ privkey ]) it('imports a private key and strips 0x prefix', async function () {
assert.equal(importPrivKey, ethUtil.stripHexPrefix(privkey)) const importPrivKey = await accountImporter.importAccount('Private Key', [ privkey ])
}) assert.equal(importPrivKey, ethUtil.stripHexPrefix(privkey))
})
it('fails when password is incorrect for keystore', async function () { it('throws an error for empty string private key', async () => {
const wrongPassword = 'password2' assertRejects(async function() {
await accountImporter.importAccount('Private Key', [ '' ])
}, Error, 'no empty strings')
})
try { it('throws an error for undefined string private key', async () => {
await accountImporter.importAccount('JSON File', [ json, wrongPassword]) assertRejects(async function () {
} catch (error) { await accountImporter.importAccount('Private Key', [ undefined ])
assert.equal(error.message, 'Key derivation failed - possibly wrong passphrase') })
} })
})
it('imports json string and password to return a private key', async function () { it('throws an error for undefined string private key', async () => {
const fileContentsPassword = 'password1' assertRejects(async function () {
const importJson = await accountImporter.importAccount('JSON File', [ json, fileContentsPassword]) await accountImporter.importAccount('Private Key', [])
assert.equal(importJson, '0x5733876abe94146069ce8bcbabbde2677f2e35fa33e875e92041ed2ac87e5bc7') })
})
it('throws an error for invalid private key', async () => {
assertRejects(async function () {
await accountImporter.importAccount('Private Key', [ 'popcorn' ])
})
})
}) })
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')
}
})
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')
})
})
}) })

@ -14,18 +14,6 @@ describe('notice-controller', function () {
}) })
describe('notices', 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 () { describe('#setNoticesList', function () {
it('should set data appropriately', function (done) { 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 () { describe('#markNoticeRead', function () {
it('should mark a notice as read', function (done) { it('should mark a notice as read', function (done) {
var testList = [{ 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) { it('should retrieve the latest unread notice', function (done) {
var testList = [ var testList = [
{id: 0, read: true, title: 'Past Notice'}, {id: 0, read: true, title: 'Past Notice'},
@ -94,8 +52,8 @@ describe('notice-controller', function () {
{id: 2, read: false, title: 'Future Notice'}, {id: 2, read: false, title: 'Future Notice'},
] ]
noticeController.setNoticesList(testList) noticeController.setNoticesList(testList)
var latestUnread = noticeController.getLatestUnreadNotice() var latestUnread = noticeController.getNextUnreadNotice()
assert.equal(latestUnread.id, 2) assert.equal(latestUnread.id, 1)
done() done()
}) })
it('should return undefined if no unread notices exist.', function (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'}, {id: 2, read: true, title: 'Future Notice'},
] ]
noticeController.setNoticesList(testList) noticeController.setNoticesList(testList)
var latestUnread = noticeController.getLatestUnreadNotice() var latestUnread = noticeController.getNextUnreadNotice()
assert.ok(!latestUnread) assert.ok(!latestUnread)
done() done()
}) })

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

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

@ -4,14 +4,21 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits const inherits = require('util').inherits
const connect = require('react-redux').connect const connect = require('react-redux').connect
const actions = require('../../actions') const actions = require('../../actions')
const genAccountLink = require('etherscan-link').createAccountLink
const copyToClipboard = require('copy-to-clipboard')
const { Menu, Item, CloseArea } = require('./components/menu')
TokenMenuDropdown.contextTypes = { TokenMenuDropdown.contextTypes = {
t: PropTypes.func, t: PropTypes.func,
} }
module.exports = connect(null, mapDispatchToProps)(TokenMenuDropdown) module.exports = connect(mapStateToProps, mapDispatchToProps)(TokenMenuDropdown)
function mapStateToProps (state) {
return {
network: state.metamask.network,
}
}
function mapDispatchToProps (dispatch) { function mapDispatchToProps (dispatch) {
return { return {
@ -37,22 +44,34 @@ TokenMenuDropdown.prototype.onClose = function (e) {
TokenMenuDropdown.prototype.render = function () { TokenMenuDropdown.prototype.render = function () {
const { showHideTokenConfirmationModal } = this.props const { showHideTokenConfirmationModal } = this.props
return h('div.token-menu-dropdown', {}, [ return h(Menu, { className: 'token-menu-dropdown', isShowing: true }, [
h('div.token-menu-dropdown__close-area', { h(CloseArea, {
onClick: this.onClose, onClick: this.onClose,
}), }),
h('div.token-menu-dropdown__container', {}, [ h(Item, {
h('div.token-menu-dropdown__options', {}, [ onClick: (e) => {
e.stopPropagation()
h('div.token-menu-dropdown__option', { showHideTokenConfirmationModal(this.props.token)
onClick: (e) => { this.props.onClose()
e.stopPropagation() },
showHideTokenConfirmationModal(this.props.token) text: this.context.t('hideToken'),
this.props.onClose() }),
}, h(Item, {
}, this.context.t('hideToken')), onClick: (e) => {
e.stopPropagation()
]), copyToClipboard(this.props.token.address)
]), this.props.onClose()
},
text: this.context.t('copyContractAddress'),
}),
h(Item, {
onClick: (e) => {
e.stopPropagation()
const url = genAccountLink(this.props.token.address, this.props.network)
global.platform.openWindow({ url })
this.props.onClose()
},
text: this.context.t('viewOnEtherscan'),
}),
]) ])
} }

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

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

@ -57,6 +57,7 @@ CurrencyDisplay.prototype.getValueToRender = function ({ selectedToken, conversi
return selectedToken return selectedToken
? conversionUtil(ethUtil.addHexPrefix(value), { ? conversionUtil(ethUtil.addHexPrefix(value), {
fromNumericBase: 'hex', fromNumericBase: 'hex',
toNumericBase: 'dec',
toCurrency: symbol, toCurrency: symbol,
conversionRate: multiplier, conversionRate: multiplier,
invertConversionRate: true, invertConversionRate: true,
@ -91,8 +92,12 @@ CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValu
: convertedValue : convertedValue
} }
function removeLeadingZeroes (str) {
return str.replace(/^0*(?=\d)/, '')
}
CurrencyDisplay.prototype.handleChange = function (newVal) { CurrencyDisplay.prototype.handleChange = function (newVal) {
this.setState({ valueToRender: newVal }) this.setState({ valueToRender: removeLeadingZeroes(newVal) })
this.props.onChange(this.getAmount(newVal)) this.props.onChange(this.getAmount(newVal))
} }

@ -23,6 +23,7 @@ export default class SendAmountRow extends Component {
tokenBalance: PropTypes.string, tokenBalance: PropTypes.string,
updateSendAmount: PropTypes.func, updateSendAmount: PropTypes.func,
updateSendAmountError: PropTypes.func, updateSendAmountError: PropTypes.func,
updateGas: PropTypes.func,
} }
validateAmount (amount) { validateAmount (amount) {
@ -56,6 +57,14 @@ export default class SendAmountRow extends Component {
updateSendAmount(amount) updateSendAmount(amount)
} }
updateGas (amount) {
const { selectedToken, updateGas } = this.props
if (selectedToken) {
updateGas({ amount })
}
}
render () { render () {
const { const {
amount, amount,
@ -77,12 +86,15 @@ export default class SendAmountRow extends Component {
<CurrencyDisplay <CurrencyDisplay
conversionRate={amountConversionRate} conversionRate={amountConversionRate}
convertedCurrency={convertedCurrency} convertedCurrency={convertedCurrency}
onBlur={newAmount => this.updateAmount(newAmount)} onBlur={newAmount => {
this.updateGas(newAmount)
this.updateAmount(newAmount)
}}
onChange={newAmount => this.validateAmount(newAmount)} onChange={newAmount => this.validateAmount(newAmount)}
inError={inError} inError={inError}
primaryCurrency={primaryCurrency || 'ETH'} primaryCurrency={primaryCurrency || 'ETH'}
selectedToken={selectedToken} selectedToken={selectedToken}
value={amount || '0x0'} value={amount}
/> />
</SendRowWrapper> </SendRowWrapper>
) )

@ -12,10 +12,12 @@ const propsMethodSpies = {
setMaxModeTo: sinon.spy(), setMaxModeTo: sinon.spy(),
updateSendAmount: sinon.spy(), updateSendAmount: sinon.spy(),
updateSendAmountError: sinon.spy(), updateSendAmountError: sinon.spy(),
updateGas: sinon.spy(),
} }
sinon.spy(SendAmountRow.prototype, 'updateAmount') sinon.spy(SendAmountRow.prototype, 'updateAmount')
sinon.spy(SendAmountRow.prototype, 'validateAmount') sinon.spy(SendAmountRow.prototype, 'validateAmount')
sinon.spy(SendAmountRow.prototype, 'updateGas')
describe('SendAmountRow Component', function () { describe('SendAmountRow Component', function () {
let wrapper let wrapper
@ -36,6 +38,7 @@ describe('SendAmountRow Component', function () {
tokenBalance={'mockTokenBalance'} tokenBalance={'mockTokenBalance'}
updateSendAmount={propsMethodSpies.updateSendAmount} updateSendAmount={propsMethodSpies.updateSendAmount}
updateSendAmountError={propsMethodSpies.updateSendAmountError} updateSendAmountError={propsMethodSpies.updateSendAmountError}
updateGas={propsMethodSpies.updateGas}
/>, { context: { t: str => str + '_t' } }) />, { context: { t: str => str + '_t' } })
instance = wrapper.instance() instance = wrapper.instance()
}) })
@ -139,8 +142,14 @@ describe('SendAmountRow Component', function () {
assert.equal(primaryCurrency, 'mockPrimaryCurrency') assert.equal(primaryCurrency, 'mockPrimaryCurrency')
assert.deepEqual(selectedToken, { address: 'mockTokenAddress' }) assert.deepEqual(selectedToken, { address: 'mockTokenAddress' })
assert.equal(value, 'mockAmount') assert.equal(value, 'mockAmount')
assert.equal(SendAmountRow.prototype.updateGas.callCount, 0)
assert.equal(SendAmountRow.prototype.updateAmount.callCount, 0) assert.equal(SendAmountRow.prototype.updateAmount.callCount, 0)
onBlur('mockNewAmount') onBlur('mockNewAmount')
assert.equal(SendAmountRow.prototype.updateGas.callCount, 1)
assert.deepEqual(
SendAmountRow.prototype.updateGas.getCall(0).args,
['mockNewAmount']
)
assert.equal(SendAmountRow.prototype.updateAmount.callCount, 1) assert.equal(SendAmountRow.prototype.updateAmount.callCount, 1)
assert.deepEqual( assert.deepEqual(
SendAmountRow.prototype.updateAmount.getCall(0).args, SendAmountRow.prototype.updateAmount.getCall(0).args,

@ -18,7 +18,7 @@ export default class SendContent extends Component {
<div className="send-v2__form"> <div className="send-v2__form">
<SendFromRow /> <SendFromRow />
<SendToRow updateGas={(updateData) => this.props.updateGas(updateData)} /> <SendToRow updateGas={(updateData) => this.props.updateGas(updateData)} />
<SendAmountRow /> <SendAmountRow updateGas={(updateData) => this.props.updateGas(updateData)} />
<SendGasRow /> <SendGasRow />
</div> </div>
</PageContainerContent> </PageContainerContent>

@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
import PersistentForm from '../../../lib/persistent-form' import PersistentForm from '../../../lib/persistent-form'
import { import {
getAmountErrorObject, getAmountErrorObject,
getToAddressForGasUpdate,
doesAmountErrorRequireUpdate, doesAmountErrorRequireUpdate,
} from './send.utils' } from './send.utils'
@ -38,7 +39,7 @@ export default class SendTransactionScreen extends PersistentForm {
updateSendTokenBalance: PropTypes.func, updateSendTokenBalance: PropTypes.func,
}; };
updateGas ({ to } = {}) { updateGas ({ to: updatedToAddress, amount: value } = {}) {
const { const {
amount, amount,
blockGasLimit, blockGasLimit,
@ -48,6 +49,7 @@ export default class SendTransactionScreen extends PersistentForm {
recentBlocks, recentBlocks,
selectedAddress, selectedAddress,
selectedToken = {}, selectedToken = {},
to: currentToAddress,
updateAndSetGasTotal, updateAndSetGasTotal,
} = this.props } = this.props
@ -59,8 +61,8 @@ export default class SendTransactionScreen extends PersistentForm {
recentBlocks, recentBlocks,
selectedAddress, selectedAddress,
selectedToken, selectedToken,
to: to && to.toLowerCase(), to: getToAddressForGasUpdate(updatedToAddress, currentToAddress),
value: amount, value: value || amount,
}) })
} }

@ -19,6 +19,7 @@ import {
getSendAmount, getSendAmount,
getSendEditingTransactionId, getSendEditingTransactionId,
getSendFromObject, getSendFromObject,
getSendTo,
getTokenBalance, getTokenBalance,
} from './send.selectors' } from './send.selectors'
import { import {
@ -54,6 +55,7 @@ function mapStateToProps (state) {
recentBlocks: getRecentBlocks(state), recentBlocks: getRecentBlocks(state),
selectedAddress: getSelectedAddress(state), selectedAddress: getSelectedAddress(state),
selectedToken: getSelectedToken(state), selectedToken: getSelectedToken(state),
to: getSendTo(state),
tokenBalance: getTokenBalance(state), tokenBalance: getTokenBalance(state),
tokenContract: getSelectedTokenContract(state), tokenContract: getSelectedTokenContract(state),
tokenToFiatRate: getSelectedTokenToFiatRate(state), tokenToFiatRate: getSelectedTokenToFiatRate(state),

@ -4,6 +4,7 @@ const {
conversionGTE, conversionGTE,
multiplyCurrencies, multiplyCurrencies,
conversionGreaterThan, conversionGreaterThan,
conversionLessThan,
} = require('../../conversion-util') } = require('../../conversion-util')
const { const {
calcTokenAmount, calcTokenAmount,
@ -20,6 +21,7 @@ const abi = require('ethereumjs-abi')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
module.exports = { module.exports = {
addGasBuffer,
calcGasTotal, calcGasTotal,
calcTokenBalance, calcTokenBalance,
doesAmountErrorRequireUpdate, doesAmountErrorRequireUpdate,
@ -27,6 +29,7 @@ module.exports = {
estimateGasPriceFromRecentBlocks, estimateGasPriceFromRecentBlocks,
generateTokenTransferData, generateTokenTransferData,
getAmountErrorObject, getAmountErrorObject,
getToAddressForGasUpdate,
isBalanceSufficient, isBalanceSufficient,
isTokenBalanceSufficient, isTokenBalanceSufficient,
} }
@ -175,9 +178,8 @@ async function estimateGas ({ selectedAddress, selectedToken, blockGasLimit, to,
} }
// if recipient has no code, gas is 21k max: // if recipient has no code, gas is 21k max:
const hasRecipient = Boolean(to) if (!selectedToken) {
if (hasRecipient && !selectedToken) { const code = Boolean(to) && await global.eth.getCode(to)
const code = await global.eth.getCode(to)
if (!code || code === '0x') { if (!code || code === '0x') {
return SIMPLE_GAS_COST return SIMPLE_GAS_COST
} }
@ -201,16 +203,46 @@ async function estimateGas ({ selectedAddress, selectedToken, blockGasLimit, to,
err.message.includes('gas required exceeds allowance or always failing transaction') err.message.includes('gas required exceeds allowance or always failing transaction')
) )
if (simulationFailed) { if (simulationFailed) {
return resolve(paramsForGasEstimate.gas) const estimateWithBuffer = addGasBuffer(paramsForGasEstimate.gas, blockGasLimit, 1.5)
return resolve(ethUtil.addHexPrefix(estimateWithBuffer))
} else { } else {
return reject(err) return reject(err)
} }
} }
return resolve(estimatedGas.toString(16)) const estimateWithBuffer = addGasBuffer(estimatedGas.toString(16), blockGasLimit, 1.5)
return resolve(ethUtil.addHexPrefix(estimateWithBuffer))
}) })
}) })
} }
function addGasBuffer (initialGasLimitHex, blockGasLimitHex, bufferMultiplier = 1.5) {
const upperGasLimit = multiplyCurrencies(blockGasLimitHex, 0.9, {
toNumericBase: 'hex',
multiplicandBase: 16,
multiplierBase: 10,
numberOfDecimals: '0',
})
const bufferedGasLimit = multiplyCurrencies(initialGasLimitHex, bufferMultiplier, {
toNumericBase: 'hex',
multiplicandBase: 16,
multiplierBase: 10,
numberOfDecimals: '0',
})
// if initialGasLimit is above blockGasLimit, dont modify it
if (conversionGreaterThan(
{ value: initialGasLimitHex, fromNumericBase: 'hex' },
{ value: upperGasLimit, fromNumericBase: 'hex' },
)) return initialGasLimitHex
// if bufferedGasLimit is below blockGasLimit, use bufferedGasLimit
if (conversionLessThan(
{ value: bufferedGasLimit, fromNumericBase: 'hex' },
{ value: upperGasLimit, fromNumericBase: 'hex' },
)) return bufferedGasLimit
// otherwise use blockGasLimit
return upperGasLimit
}
function generateTokenTransferData ({ toAddress = '0x0', amount = '0x0', selectedToken }) { function generateTokenTransferData ({ toAddress = '0x0', amount = '0x0', selectedToken }) {
if (!selectedToken) return if (!selectedToken) return
return TOKEN_TRANSFER_FUNCTION_SIGNATURE + Array.prototype.map.call( return TOKEN_TRANSFER_FUNCTION_SIGNATURE + Array.prototype.map.call(
@ -237,3 +269,7 @@ function estimateGasPriceFromRecentBlocks (recentBlocks) {
return lowestPrices[Math.floor(lowestPrices.length / 2)] return lowestPrices[Math.floor(lowestPrices.length / 2)]
} }
function getToAddressForGasUpdate (...addresses) {
return [...addresses, ''].find(str => str !== undefined && str !== null).toLowerCase()
}

@ -201,7 +201,7 @@ describe('Send Component', function () {
}) })
describe('updateGas', () => { describe('updateGas', () => {
it('should call updateAndSetGasTotal with the correct params', () => { it('should call updateAndSetGasTotal with the correct params if no to prop is passed', () => {
propsMethodSpies.updateAndSetGasTotal.resetHistory() propsMethodSpies.updateAndSetGasTotal.resetHistory()
wrapper.instance().updateGas() wrapper.instance().updateGas()
assert.equal(propsMethodSpies.updateAndSetGasTotal.callCount, 1) assert.equal(propsMethodSpies.updateAndSetGasTotal.callCount, 1)
@ -215,12 +215,22 @@ describe('Send Component', function () {
recentBlocks: ['mockBlock'], recentBlocks: ['mockBlock'],
selectedAddress: 'mockSelectedAddress', selectedAddress: 'mockSelectedAddress',
selectedToken: 'mockSelectedToken', selectedToken: 'mockSelectedToken',
to: undefined, to: '',
value: 'mockAmount', value: 'mockAmount',
} }
) )
}) })
it('should call updateAndSetGasTotal with the correct params if a to prop is passed', () => {
propsMethodSpies.updateAndSetGasTotal.resetHistory()
wrapper.setProps({ to: 'someAddress' })
wrapper.instance().updateGas()
assert.equal(
propsMethodSpies.updateAndSetGasTotal.getCall(0).args[0].to,
'someaddress',
)
})
it('should call updateAndSetGasTotal with to set to lowercase if passed', () => { it('should call updateAndSetGasTotal with to set to lowercase if passed', () => {
propsMethodSpies.updateAndSetGasTotal.resetHistory() propsMethodSpies.updateAndSetGasTotal.resetHistory()
wrapper.instance().updateGas({ to: '0xABC' }) wrapper.instance().updateGas({ to: '0xABC' })

@ -39,6 +39,7 @@ proxyquire('../send.container.js', {
getSelectedTokenContract: (s) => `mockTokenContract:${s}`, getSelectedTokenContract: (s) => `mockTokenContract:${s}`,
getSelectedTokenToFiatRate: (s) => `mockTokenToFiatRate:${s}`, getSelectedTokenToFiatRate: (s) => `mockTokenToFiatRate:${s}`,
getSendAmount: (s) => `mockAmount:${s}`, getSendAmount: (s) => `mockAmount:${s}`,
getSendTo: (s) => `mockTo:${s}`,
getSendEditingTransactionId: (s) => `mockEditingTransactionId:${s}`, getSendEditingTransactionId: (s) => `mockEditingTransactionId:${s}`,
getSendFromObject: (s) => `mockFrom:${s}`, getSendFromObject: (s) => `mockFrom:${s}`,
getTokenBalance: (s) => `mockTokenBalance:${s}`, getTokenBalance: (s) => `mockTokenBalance:${s}`,
@ -70,6 +71,7 @@ describe('send container', () => {
recentBlocks: 'mockRecentBlocks:mockState', recentBlocks: 'mockRecentBlocks:mockState',
selectedAddress: 'mockSelectedAddress:mockState', selectedAddress: 'mockSelectedAddress:mockState',
selectedToken: 'mockSelectedToken:mockState', selectedToken: 'mockSelectedToken:mockState',
to: 'mockTo:mockState',
tokenBalance: 'mockTokenBalance:mockState', tokenBalance: 'mockTokenBalance:mockState',
tokenContract: 'mockTokenContract:mockState', tokenContract: 'mockTokenContract:mockState',
tokenToFiatRate: 'mockTokenToFiatRate:mockState', tokenToFiatRate: 'mockTokenToFiatRate:mockState',

@ -18,10 +18,12 @@ const {
const stubs = { const stubs = {
addCurrencies: sinon.stub().callsFake((a, b, obj) => a + b), addCurrencies: sinon.stub().callsFake((a, b, obj) => a + b),
conversionUtil: sinon.stub().callsFake((val, obj) => parseInt(val, 16)), conversionUtil: sinon.stub().callsFake((val, obj) => parseInt(val, 16)),
conversionGTE: sinon.stub().callsFake((obj1, obj2) => obj1.value > obj2.value), conversionGTE: sinon.stub().callsFake((obj1, obj2) => obj1.value >= obj2.value),
multiplyCurrencies: sinon.stub().callsFake((a, b) => `${a}x${b}`), multiplyCurrencies: sinon.stub().callsFake((a, b) => `${a}x${b}`),
calcTokenAmount: sinon.stub().callsFake((a, d) => 'calc:' + a + d), calcTokenAmount: sinon.stub().callsFake((a, d) => 'calc:' + a + d),
rawEncode: sinon.stub().returns([16, 1100]), rawEncode: sinon.stub().returns([16, 1100]),
conversionGreaterThan: sinon.stub().callsFake((obj1, obj2) => obj1.value > obj2.value),
conversionLessThan: sinon.stub().callsFake((obj1, obj2) => obj1.value < obj2.value),
} }
const sendUtils = proxyquire('../send.utils.js', { const sendUtils = proxyquire('../send.utils.js', {
@ -30,6 +32,8 @@ const sendUtils = proxyquire('../send.utils.js', {
conversionUtil: stubs.conversionUtil, conversionUtil: stubs.conversionUtil,
conversionGTE: stubs.conversionGTE, conversionGTE: stubs.conversionGTE,
multiplyCurrencies: stubs.multiplyCurrencies, multiplyCurrencies: stubs.multiplyCurrencies,
conversionGreaterThan: stubs.conversionGreaterThan,
conversionLessThan: stubs.conversionLessThan,
}, },
'../../token-util': { calcTokenAmount: stubs.calcTokenAmount }, '../../token-util': { calcTokenAmount: stubs.calcTokenAmount },
'ethereumjs-abi': { 'ethereumjs-abi': {
@ -44,6 +48,7 @@ const {
estimateGasPriceFromRecentBlocks, estimateGasPriceFromRecentBlocks,
generateTokenTransferData, generateTokenTransferData,
getAmountErrorObject, getAmountErrorObject,
getToAddressForGasUpdate,
calcTokenBalance, calcTokenBalance,
isBalanceSufficient, isBalanceSufficient,
isTokenBalanceSufficient, isTokenBalanceSufficient,
@ -255,7 +260,7 @@ describe('send utils', () => {
estimateGasMethod: sinon.stub().callsFake( estimateGasMethod: sinon.stub().callsFake(
(data, cb) => cb( (data, cb) => cb(
data.to.match(/willFailBecauseOf:/) ? { message: data.to.match(/:(.+)$/)[1] } : null, data.to.match(/willFailBecauseOf:/) ? { message: data.to.match(/:(.+)$/)[1] } : null,
{ toString: (n) => `mockToString:${n}` } { toString: (n) => `0xabc${n}` }
) )
), ),
} }
@ -279,13 +284,23 @@ describe('send utils', () => {
}) })
it('should call ethQuery.estimateGas with the expected params', async () => { it('should call ethQuery.estimateGas with the expected params', async () => {
const result = await estimateGas(baseMockParams) const result = await sendUtils.estimateGas(baseMockParams)
assert.equal(baseMockParams.estimateGasMethod.callCount, 1) assert.equal(baseMockParams.estimateGasMethod.callCount, 1)
assert.deepEqual( assert.deepEqual(
baseMockParams.estimateGasMethod.getCall(0).args[0], baseMockParams.estimateGasMethod.getCall(0).args[0],
Object.assign({ gasPrice: undefined, value: undefined }, baseExpectedCall) Object.assign({ gasPrice: undefined, value: undefined }, baseExpectedCall)
) )
assert.equal(result, 'mockToString:16') assert.equal(result, '0xabc16')
})
it('should call ethQuery.estimateGas with the expected params when initialGasLimitHex is lower than the upperGasLimit', async () => {
const result = await estimateGas(Object.assign({}, baseMockParams, { blockGasLimit: '0xbcd' }))
assert.equal(baseMockParams.estimateGasMethod.callCount, 1)
assert.deepEqual(
baseMockParams.estimateGasMethod.getCall(0).args[0],
Object.assign({ gasPrice: undefined, value: undefined }, baseExpectedCall, { gas: '0xbcdx0.95' })
)
assert.equal(result, '0xabc16x1.5')
}) })
it('should call ethQuery.estimateGas with a value of 0x0 and the expected data and to if passed a selectedToken', async () => { it('should call ethQuery.estimateGas with a value of 0x0 and the expected data and to if passed a selectedToken', async () => {
@ -300,7 +315,7 @@ describe('send utils', () => {
to: 'mockAddress', to: 'mockAddress',
}) })
) )
assert.equal(result, 'mockToString:16') assert.equal(result, '0xabc16')
}) })
it(`should return ${SIMPLE_GAS_COST} if ethQuery.getCode does not return '0x'`, async () => { it(`should return ${SIMPLE_GAS_COST} if ethQuery.getCode does not return '0x'`, async () => {
@ -309,6 +324,12 @@ describe('send utils', () => {
assert.equal(result, SIMPLE_GAS_COST) assert.equal(result, SIMPLE_GAS_COST)
}) })
it(`should return ${SIMPLE_GAS_COST} if not passed a selectedToken or truthy to address`, async () => {
assert.equal(baseMockParams.estimateGasMethod.callCount, 0)
const result = await estimateGas(Object.assign({}, baseMockParams, { to: null }))
assert.equal(result, SIMPLE_GAS_COST)
})
it(`should not return ${SIMPLE_GAS_COST} if passed a selectedToken`, async () => { it(`should not return ${SIMPLE_GAS_COST} if passed a selectedToken`, async () => {
assert.equal(baseMockParams.estimateGasMethod.callCount, 0) assert.equal(baseMockParams.estimateGasMethod.callCount, 0)
const result = await estimateGas(Object.assign({}, baseMockParams, { to: '0x123', selectedToken: { address: '' } })) const result = await estimateGas(Object.assign({}, baseMockParams, { to: '0x123', selectedToken: { address: '' } }))
@ -401,4 +422,15 @@ describe('send utils', () => {
assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), '0x5') assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), '0x5')
}) })
}) })
describe('getToAddressForGasUpdate()', () => {
it('should return empty string if all params are undefined or null', () => {
assert.equal(getToAddressForGasUpdate(undefined, null), '')
})
it('should return the first string that is not defined or null in lower case', () => {
assert.equal(getToAddressForGasUpdate('A', null), 'a')
assert.equal(getToAddressForGasUpdate(undefined, 'B'), 'b')
})
})
}) })

@ -178,7 +178,14 @@ SignatureRequest.prototype.renderBody = function () {
rows = data rows = data
} else if (type === 'eth_sign') { } else if (type === 'eth_sign') {
rows = [{ name: this.context.t('message'), value: data }] 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', {}, [ return h('div.request-signature__body', {}, [

@ -190,6 +190,16 @@ const conversionGreaterThan = (
return firstValue.gt(secondValue) return firstValue.gt(secondValue)
} }
const conversionLessThan = (
{ ...firstProps },
{ ...secondProps },
) => {
const firstValue = converter({ ...firstProps })
const secondValue = converter({ ...secondProps })
return firstValue.lt(secondValue)
}
const conversionMax = ( const conversionMax = (
{ ...firstProps }, { ...firstProps },
{ ...secondProps }, { ...secondProps },
@ -229,6 +239,7 @@ module.exports = {
addCurrencies, addCurrencies,
multiplyCurrencies, multiplyCurrencies,
conversionGreaterThan, conversionGreaterThan,
conversionLessThan,
conversionGTE, conversionGTE,
conversionLTE, conversionLTE,
conversionMax, conversionMax,

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

@ -81,13 +81,9 @@ $wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and (
} }
.token-menu-dropdown { .token-menu-dropdown {
height: 55px;
width: 80%; width: 80%;
border-radius: 4px;
background-color: rgba(0, 0, 0, .82);
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .5);
position: absolute; position: absolute;
top: 60px; top: 52px;
right: 25px; right: 25px;
z-index: 2000; z-index: 2000;

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

Loading…
Cancel
Save