fix merge conflicts

feature/default_network_editable
brunobar79 7 years ago
commit 1494cc5e6c
  1. 11
      CHANGELOG.md
  2. 1
      LICENSE
  3. 9
      app/_locales/en/messages.json
  4. 2
      app/manifest.json
  5. 13
      app/scripts/account-import-strategies/index.js
  6. 14
      app/scripts/background.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. 6
      app/scripts/inpage.js
  11. 61
      app/scripts/lib/auto-reload.js
  12. 24
      app/scripts/lib/createStreamSink.js
  13. 20
      app/scripts/lib/get-first-preferred-lang-code.js
  14. 6
      app/scripts/lib/notification-manager.js
  15. 11
      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. 6
      old-ui/app/components/typed-message-renderer.js
  29. 13720
      package-lock.json
  30. 4
      package.json
  31. 61
      test/e2e/beta/contract-test/contract.js
  32. 8
      test/e2e/beta/contract-test/index.html
  33. 175
      test/e2e/beta/from-import-beta-ui.spec.js
  34. 14
      test/e2e/beta/helpers.js
  35. 504
      test/e2e/beta/metamask-beta-ui.spec.js
  36. 4
      test/e2e/beta/run-all.sh
  37. 36
      test/e2e/metamask.spec.js
  38. 4
      test/integration/lib/send-new-ui.js
  39. 62
      test/unit/app/account-import-strategies.spec.js
  40. 50
      test/unit/app/controllers/notice-controller-test.js
  41. 17
      test/unit/test-utils.js
  42. 21
      ui/app/actions.js
  43. 4
      ui/app/app.js
  44. 48
      ui/app/components/customize-gas-modal/index.js
  45. 53
      ui/app/components/dropdowns/token-menu-dropdown.js
  46. 77
      ui/app/components/ens-input.js
  47. 1
      ui/app/components/identicon.js
  48. 12
      ui/app/components/input-number.js
  49. 8
      ui/app/components/pages/home.js
  50. 12
      ui/app/components/pages/notice.js
  51. 2
      ui/app/components/pending-tx/confirm-send-ether.js
  52. 2
      ui/app/components/pending-tx/confirm-send-token.js
  53. 9
      ui/app/components/send/currency-display.js
  54. 53
      ui/app/components/send/gas-fee-display-v2.js
  55. 4
      ui/app/components/send_/account-list-item/account-list-item.container.js
  56. 2
      ui/app/components/send_/account-list-item/tests/account-list-item-container.test.js
  57. 17
      ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js
  58. 4
      ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js
  59. 9
      ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js
  60. 2
      ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-container.test.js
  61. 2
      ui/app/components/send_/send-content/send-content.component.js
  62. 61
      ui/app/components/send_/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js
  63. 1
      ui/app/components/send_/send-content/send-gas-row/gas-fee-display/index.js
  64. 55
      ui/app/components/send_/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js
  65. 2
      ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js
  66. 4
      ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js
  67. 2
      ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-component.test.js
  68. 2
      ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-container.test.js
  69. 6
      ui/app/components/send_/send-content/send-to-row/send-to-row.component.js
  70. 6
      ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js
  71. 12
      ui/app/components/send_/send-content/send-to-row/tests/send-to-row-component.test.js
  72. 6
      ui/app/components/send_/send-content/send-to-row/tests/send-to-row-utils.test.js
  73. 8
      ui/app/components/send_/send.component.js
  74. 2
      ui/app/components/send_/send.constants.js
  75. 2
      ui/app/components/send_/send.container.js
  76. 5
      ui/app/components/send_/send.selectors.js
  77. 49
      ui/app/components/send_/send.utils.js
  78. 14
      ui/app/components/send_/tests/send-component.test.js
  79. 2
      ui/app/components/send_/tests/send-container.test.js
  80. 10
      ui/app/components/send_/tests/send-selectors.test.js
  81. 48
      ui/app/components/send_/tests/send-utils.test.js
  82. 2
      ui/app/components/shapeshift-form.js
  83. 12
      ui/app/components/signature-request.js
  84. 2
      ui/app/components/token-balance.js
  85. 11
      ui/app/conversion-util.js
  86. 13
      ui/app/css/itcss/components/currency-display.scss
  87. 15
      ui/app/css/itcss/components/hero-balance.scss
  88. 31
      ui/app/css/itcss/components/modal.scss
  89. 8
      ui/app/css/itcss/components/newui-sections.scss
  90. 6
      ui/app/css/itcss/components/request-signature.scss
  91. 6
      ui/app/css/itcss/components/token-list.scss
  92. 11
      ui/app/reducers/app.js
  93. 4
      ui/app/reducers/metamask.js
  94. 5
      ui/app/selectors.js
  95. 5
      ui/app/util.js

@ -2,8 +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
- Stop reloading browser page on Ethereum network change
- [#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

@ -18,3 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -152,6 +152,9 @@
"copy": { "copy": {
"message": "Copy" "message": "Copy"
}, },
"copyContractAddress": {
"message": "Copy Contract Address"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Copy to clipboard" "message": "Copy to clipboard"
}, },
@ -268,6 +271,9 @@
"encryptNewDen": { "encryptNewDen": {
"message": "Encrypt your new DEN" "message": "Encrypt your new DEN"
}, },
"ensNameNotFound": {
"message": "ENS name not found"
},
"enterPassword": { "enterPassword": {
"message": "Enter password" "message": "Enter password"
}, },
@ -964,6 +970,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
} }
// //

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

@ -3,6 +3,7 @@ cleanContextForImports()
require('web3/dist/web3.min.js') require('web3/dist/web3.min.js')
const log = require('loglevel') const log = require('loglevel')
const LocalMessageDuplexStream = require('post-message-stream') const LocalMessageDuplexStream = require('post-message-stream')
const setupDappAutoReload = require('./lib/auto-reload.js')
const MetamaskInpageProvider = require('./lib/inpage-provider.js') const MetamaskInpageProvider = require('./lib/inpage-provider.js')
restoreContextAfterImports() restoreContextAfterImports()
@ -38,7 +39,11 @@ web3.setProvider = function () {
} }
log.debug('MetaMask - injected web3') log.debug('MetaMask - injected web3')
setupDappAutoReload(web3, inpageProvider.publicConfigStore)
// export global web3, with usage-detection and deprecation warning // export global web3, with usage-detection and deprecation warning
/* TODO: Uncomment this area once auto-reload.js has been deprecated:
let hasBeenWarned = false let hasBeenWarned = false
global.web3 = new Proxy(web3, { global.web3 = new Proxy(web3, {
get: (_web3, key) => { get: (_web3, key) => {
@ -55,6 +60,7 @@ global.web3 = new Proxy(web3, {
_web3[key] = value _web3[key] = value
}, },
}) })
*/
// set web3 defaultAccount // set web3 defaultAccount
inpageProvider.publicConfigStore.subscribe(function (state) { inpageProvider.publicConfigStore.subscribe(function (state) {

@ -0,0 +1,61 @@
module.exports = setupDappAutoReload
function setupDappAutoReload (web3, observable) {
// export web3 as a global, checking for usage
let hasBeenWarned = false
let reloadInProgress = false
let lastTimeUsed
let lastSeenNetwork
global.web3 = new Proxy(web3, {
get: (_web3, key) => {
// show warning once on web3 access
if (!hasBeenWarned && key !== 'currentProvider') {
console.warn('MetaMask: web3 will be deprecated in the near future in favor of the ethereumProvider \nhttps://github.com/MetaMask/faq/blob/master/detecting_metamask.md#web3-deprecation')
hasBeenWarned = true
}
// get the time of use
lastTimeUsed = Date.now()
// return value normally
return _web3[key]
},
set: (_web3, key, value) => {
// set value normally
_web3[key] = value
},
})
observable.subscribe(function (state) {
// if reload in progress, no need to check reload logic
if (reloadInProgress) return
const currentNetwork = state.networkVersion
// set the initial network
if (!lastSeenNetwork) {
lastSeenNetwork = currentNetwork
return
}
// skip reload logic if web3 not used
if (!lastTimeUsed) return
// if network did not change, exit
if (currentNetwork === lastSeenNetwork) return
// initiate page reload
reloadInProgress = true
const timeSinceUse = Date.now() - lastTimeUsed
// if web3 was recently used then delay the reloading of the page
if (timeSinceUse > 500) {
triggerReset()
} else {
setTimeout(triggerReset, 500)
}
})
}
// reload the page
function triggerReset () {
global.location.reload()
}

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

@ -32,6 +32,8 @@ class NotificationManager {
type: 'popup', type: 'popup',
width, width,
height, height,
}).then((currentPopup) => {
this._popupId = currentPopup.id
}) })
} }
}) })
@ -84,7 +86,7 @@ class NotificationManager {
} }
/** /**
* Given an array of windows, returns the first that has a 'popup' type, or null if no such window exists. * Given an array of windows, returns the 'popup' that has been opened by MetaMask, or null if no such window exists.
* *
* @private * @private
* @param {array} windows An array of objects containing data about the open MetaMask extension windows. * @param {array} windows An array of objects containing data about the open MetaMask extension windows.
@ -93,7 +95,7 @@ class NotificationManager {
_getPopupIn (windows) { _getPopupIn (windows) {
return windows ? windows.find((win) => { return windows ? windows.find((win) => {
// Returns notification popup // Returns notification popup
return (win && win.type === 'popup') return (win && win.type === 'popup' && win.id === this._popupId)
}) : null }) : null
} }

@ -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')
const TrezorKeyring = require('eth-trezor-keyring') const TrezorKeyring = require('eth-trezor-keyring')
@ -66,12 +65,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
@ -93,7 +86,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
@ -192,9 +184,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,

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

@ -34,9 +34,13 @@ TypedMessageRenderer.prototype.render = function () {
function renderTypedData (values) { function renderTypedData (values) {
return values.map(function (value) { return values.map(function (value) {
let v = value.value
if (typeof v === 'boolean') {
v = v.toString()
}
return h('div', {}, [ return h('div', {}, [
h('strong', {style: {display: 'block', fontWeight: 'bold'}}, String(value.name) + ':'), h('strong', {style: {display: 'block', fontWeight: 'bold'}}, String(value.name) + ':'),
h('div', {}, value.value), h('div', {}, v),
]) ])
}) })
} }

13720
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -46,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": {
@ -186,6 +184,7 @@
"semaphore": "^1.0.5", "semaphore": "^1.0.5",
"semver": "^5.4.1", "semver": "^5.4.1",
"shallow-copy": "0.0.1", "shallow-copy": "0.0.1",
"superstatic": "^5.0.2",
"sw-controller": "^1.0.3", "sw-controller": "^1.0.3",
"sw-stream": "^2.0.2", "sw-stream": "^2.0.2",
"textarea-caret": "^3.0.1", "textarea-caret": "^3.0.1",
@ -254,6 +253,7 @@
"gulp-util": "^3.0.7", "gulp-util": "^3.0.7",
"gulp-watch": "^5.0.0", "gulp-watch": "^5.0.0",
"gulp-zip": "^4.0.0", "gulp-zip": "^4.0.0",
"http-server": "^0.11.1",
"image-size": "^0.6.2", "image-size": "^0.6.2",
"isomorphic-fetch": "^2.2.1", "isomorphic-fetch": "^2.2.1",
"jsdoc": "^3.5.5", "jsdoc": "^3.5.5",

@ -0,0 +1,61 @@
/*
The `piggybankContract` is compiled from:
pragma solidity ^0.4.0;
contract PiggyBank {
uint private balance;
address public owner;
function PiggyBank() public {
owner = msg.sender;
balance = 0;
}
function deposit() public payable returns (uint) {
balance += msg.value;
return balance;
}
function withdraw(uint withdrawAmount) public returns (uint remainingBal) {
require(msg.sender == owner);
balance -= withdrawAmount;
msg.sender.transfer(withdrawAmount);
return balance;
}
}
*/
var piggybankContract = web3.eth.contract([{"constant":false,"inputs":[{"name":"withdrawAmount","type":"uint256"}],"name":"withdraw","outputs":[{"name":"remainingBal","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"deposit","outputs":[{"name":"","type":"uint256"}],"payable":true,"stateMutability":"payable","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"}]);
deployButton.addEventListener('click', function (event) {
var piggybank = piggybankContract.new(
{
from: web3.eth.accounts[0],
data: '0x608060405234801561001057600080fd5b5033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506000808190555061023b806100686000396000f300608060405260043610610057576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e1a7d4d1461005c5780638da5cb5b1461009d578063d0e30db0146100f4575b600080fd5b34801561006857600080fd5b5061008760048036038101908080359060200190929190505050610112565b6040518082815260200191505060405180910390f35b3480156100a957600080fd5b506100b26101d0565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100fc6101f6565b6040518082815260200191505060405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614151561017057600080fd5b8160008082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc839081150290604051600060405180830381858888f193505050501580156101c5573d6000803e3d6000fd5b506000549050919050565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60003460008082825401925050819055506000549050905600a165627a7a72305820f237db3ec816a52589d82512117bc85bc08d3537683ffeff9059108caf3e5d400029',
gas: '4700000'
}, function (e, contract){
console.log(e, contract);
if (typeof contract.address !== 'undefined') {
console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash);
console.log(`contract`, contract);
depositButton.addEventListener('click', function (event) {
contract.deposit({ from: web3.eth.accounts[0], value: '0x29a2241af62c0000' }, function (result) {
console.log(result)
})
})
withdrawButton.addEventListener('click', function (event) {
contract.withdraw('0xde0b6b3a7640000', { from: web3.eth.accounts[0] }, function (result) {
console.log(result)
})
})
}
})
})

@ -0,0 +1,8 @@
<html>
<body>
<button id="deployButton">Deploy Contract</button>
<button id="depositButton">Deposit</button>
<button id="withdrawButton">Withdraw</button>
</body>
<script src="contract.js"></script>
</html>

@ -26,6 +26,7 @@ describe('Using MetaMask with an existing account', function () {
const testSeedPhrase = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent' const testSeedPhrase = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent'
const testAddress = '0xE18035BF8712672935FDB4e5e431b1a0183d2DFC' const testAddress = '0xE18035BF8712672935FDB4e5e431b1a0183d2DFC'
const testPrivateKey2 = '14abe6f4aab7f9f626fe981c864d0adeb5685f289ac9270c27b8fd790b4235d6'
const regularDelayMs = 1000 const regularDelayMs = 1000
const largeDelayMs = regularDelayMs * 2 const largeDelayMs = regularDelayMs * 2
const waitingNewPageDelayMs = regularDelayMs * 10 const waitingNewPageDelayMs = regularDelayMs * 10
@ -134,27 +135,39 @@ 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)
}) })
}) })
describe('Show account information', () => { describe('Show account information', () => {
it('shows the correct account address', async () => { it('shows the correct account address', async () => {
const detailsButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Details')]`)) await driver.findElement(By.css('.wallet-view__details-button')).click()
detailsButton.click()
await driver.findElement(By.css('.qr-wrapper')).isDisplayed() await driver.findElement(By.css('.qr-wrapper')).isDisplayed()
await delay(regularDelayMs) await delay(regularDelayMs)
@ -250,8 +263,10 @@ describe('Using MetaMask with an existing account', function () {
await configureGas.click() await configureGas.click()
await delay(regularDelayMs) await delay(regularDelayMs)
const gasModal = await driver.findElement(By.css('span .modal'))
const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`)) const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`))
await save.click() await save.click()
await driver.wait(until.stalenessOf(gasModal))
await delay(regularDelayMs) await delay(regularDelayMs)
// Continue to next screen // Continue to next screen
@ -276,142 +291,36 @@ describe('Using MetaMask with an existing account', function () {
}) })
}) })
describe('Send ETH from Faucet', () => { describe('Imports an account with private key', () => {
it('starts a send transaction inside Faucet', async () => { it('choose Create Account from the account menu', async () => {
await driver.executeScript('window.open("https://faucet.metamask.io")') await driver.findElement(By.css('.account-menu__icon')).click()
await delay(waitingNewPageDelayMs)
const [extension, faucet] = await driver.getAllWindowHandles()
await driver.switchTo().window(faucet)
await delay(regularDelayMs)
const send1eth = await findElement(driver, By.xpath(`//button[contains(text(), '10 ether')]`), 14000)
await send1eth.click()
await delay(regularDelayMs)
await driver.switchTo().window(extension)
await loadExtension(driver, extensionId)
await delay(regularDelayMs)
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`), 14000)
await confirmButton.click()
await delay(regularDelayMs)
await driver.switchTo().window(faucet)
await delay(regularDelayMs)
await driver.close()
await delay(regularDelayMs)
await driver.switchTo().window(extension)
await delay(regularDelayMs)
await loadExtension(driver, extensionId)
await delay(regularDelayMs)
})
})
describe('Add existing token using search', () => {
it('clicks on the Add Token button', async () => {
const addToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Token')]`))
await addToken.click()
await delay(regularDelayMs)
})
it('picks an existing token', async () => {
const tokenSearch = await findElement(driver, By.css('#search-tokens'))
await tokenSearch.sendKeys('BAT')
await delay(regularDelayMs)
const token = await findElement(driver, By.xpath("//span[contains(text(), 'BAT')]"))
await token.click()
await delay(regularDelayMs)
const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), 'Next')]`))
await nextScreen.click()
await delay(regularDelayMs) await delay(regularDelayMs)
const addTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Tokens')]`)) const [importAccount] = await findElements(driver, By.xpath(`//div[contains(text(), 'Import Account')]`))
await addTokens.click() await importAccount.click()
await delay(largeDelayMs)
})
it('renders the balance for the new token', async () => {
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
const tokenAmount = await balance.getText()
assert.equal(tokenAmount, '0BAT')
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
})
describe('Add a custom token from TokenFactory', () => {
it('creates a new token', async () => {
await driver.executeScript('window.open("https://tokenfactory.surge.sh/#/factory")')
await delay(waitingNewPageDelayMs)
const [extension, tokenFactory] = await driver.getAllWindowHandles()
await driver.switchTo().window(tokenFactory)
const [
totalSupply,
tokenName,
tokenDecimal,
tokenSymbol,
] = await findElements(driver, By.css('.form-control'))
await totalSupply.sendKeys('100')
await tokenName.sendKeys('Test')
await tokenDecimal.sendKeys('0')
await tokenSymbol.sendKeys('TST')
const createToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Create Token')]`))
await createToken.click()
await delay(regularDelayMs)
await driver.switchTo().window(extension)
await loadExtension(driver, extensionId)
await delay(regularDelayMs)
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirmButton.click()
await delay(regularDelayMs)
await driver.switchTo().window(tokenFactory) it('enter private key', async () => {
const privateKeyInput = await findElement(driver, By.css('#private-key-box'))
await privateKeyInput.sendKeys(testPrivateKey2)
await delay(regularDelayMs) await delay(regularDelayMs)
const tokenContactAddress = await driver.findElement(By.css('div > div > div:nth-child(2) > span:nth-child(3)')) const importButtons = await findElements(driver, By.xpath(`//button[contains(text(), 'Import')]`))
tokenAddress = await tokenContactAddress.getText() await importButtons[0].click()
await driver.close()
await driver.switchTo().window(extension)
await loadExtension(driver, extensionId)
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
it('clicks on the Add Token button', async () => { it('should show the correct account name', async () => {
const addToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Token')]`)) const [accountName] = await findElements(driver, By.css('.account-name'))
await addToken.click() assert.equal(await accountName.getText(), 'Account 3')
await delay(regularDelayMs)
})
it('picks the new Test token', async () => {
const addCustomToken = await findElement(driver, By.xpath("//div[contains(text(), 'Custom Token')]"))
await addCustomToken.click()
await delay(regularDelayMs)
const newTokenAddress = await findElement(driver, By.css('#custom-address'))
await newTokenAddress.sendKeys(tokenAddress)
await delay(regularDelayMs)
const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), 'Next')]`))
await nextScreen.click()
await delay(regularDelayMs)
const addTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Tokens')]`))
await addTokens.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
it('renders the balance for the new token', async () => { it('should show the imported label', async () => {
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount')) const [importedLabel] = await findElements(driver, By.css('.wallet-view__keyring-label'))
await driver.wait(until.elementTextIs(balance, '100TST')) assert.equal(await importedLabel.getText(), 'IMPORTED')
const tokenAmount = await balance.getText()
assert.equal(tokenAmount, '100TST')
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
}) })
}) })

@ -2,6 +2,7 @@ const fs = require('fs')
const mkdirp = require('mkdirp') const mkdirp = require('mkdirp')
const pify = require('pify') const pify = require('pify')
const {until} = require('selenium-webdriver') const {until} = require('selenium-webdriver')
const { delay } = require('../func')
module.exports = { module.exports = {
checkBrowserForConsoleErrors, checkBrowserForConsoleErrors,
@ -9,6 +10,7 @@ module.exports = {
verboseReportOnFailure, verboseReportOnFailure,
findElement, findElement,
findElements, findElements,
openNewPage,
} }
async function loadExtension (driver, extensionId) { async function loadExtension (driver, extensionId) {
@ -64,3 +66,15 @@ async function findElement (driver, by, timeout = 10000) {
async function findElements (driver, by, timeout = 10000) { async function findElements (driver, by, timeout = 10000) {
return driver.wait(until.elementsLocated(by), timeout) return driver.wait(until.elementsLocated(by), timeout)
} }
async function openNewPage (driver, url) {
await driver.executeScript('window.open()')
await delay(1000)
const handles = await driver.getAllWindowHandles()
const secondHandle = handles[1]
await driver.switchTo().window(secondHandle)
await driver.get(url)
await delay(1000)
}

@ -16,6 +16,7 @@ const {
checkBrowserForConsoleErrors, checkBrowserForConsoleErrors,
loadExtension, loadExtension,
verboseReportOnFailure, verboseReportOnFailure,
openNewPage,
} = require('./helpers') } = require('./helpers')
describe('MetaMask', function () { describe('MetaMask', function () {
@ -62,7 +63,7 @@ describe('MetaMask', function () {
} }
} }
if (this.currentTest.state === 'failed') { if (this.currentTest.state === 'failed') {
await verboseReportOnFailure(this.currentTest) await verboseReportOnFailure(driver, this.currentTest)
} }
}) })
@ -71,14 +72,20 @@ describe('MetaMask', function () {
}) })
describe('New UI setup', async function () { describe('New UI setup', async function () {
let networkSelector
it('switches to first tab', async function () { it('switches to first tab', async function () {
const [firstTab] = await driver.getAllWindowHandles() const [firstTab] = await driver.getAllWindowHandles()
await driver.switchTo().window(firstTab) await driver.switchTo().window(firstTab)
await delay(regularDelayMs) await delay(regularDelayMs)
try {
networkSelector = await findElement(driver, By.css('#network_component'))
} catch (e) {
await loadExtension(driver, extensionId)
}
await delay(regularDelayMs)
}) })
it('use the local network', async function () { it('use the local network', async function () {
const networkSelector = await findElement(driver, By.css('#network_component'))
await networkSelector.click() await networkSelector.click()
await delay(regularDelayMs) await delay(regularDelayMs)
@ -128,27 +135,43 @@ 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'))
driver.wait(until.elementIsEnabled(acceptTos))
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 () => {
const revealSeedPhrase = await findElement(driver, By.css('.backup-phrase__secret-blocker')) const byRevealButton = By.css('.backup-phrase__secret-blocker .backup-phrase__reveal-button')
await revealSeedPhrase.click() await driver.wait(until.elementLocated(byRevealButton, 10000))
const revealSeedPhraseButton = await findElement(driver, byRevealButton, 10000)
await revealSeedPhraseButton.click()
await delay(regularDelayMs) await delay(regularDelayMs)
seedPhrase = await driver.findElement(By.css('.backup-phrase__secret-words')).getText() seedPhrase = await driver.findElement(By.css('.backup-phrase__secret-words')).getText()
@ -160,56 +183,76 @@ describe('MetaMask', function () {
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
it('can retype the seed phrase', async () => { async function retypeSeedPhrase (words) {
const words = seedPhrase.split(' ') try {
const word0 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[0]}')]`), 10000)
const word0 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[0]}')]`)) await word0.click()
await word0.click() await delay(tinyDelayMs)
await delay(tinyDelayMs)
const word1 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[1]}')]`)) 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]}')]`)) await word1.click()
await word2.click() await delay(tinyDelayMs)
await delay(tinyDelayMs)
const word3 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[3]}')]`)) const word2 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[2]}')]`), 10000)
await word3.click()
await delay(tinyDelayMs)
const word4 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[4]}')]`)) await word2.click()
await word4.click() await delay(tinyDelayMs)
await delay(tinyDelayMs)
const word5 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[5]}')]`)) const word3 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[3]}')]`), 10000)
await word5.click()
await delay(tinyDelayMs)
const word6 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[6]}')]`)) await word3.click()
await word6.click() await delay(tinyDelayMs)
await delay(tinyDelayMs)
const word7 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[7]}')]`)) const word4 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[4]}')]`), 10000)
await word7.click()
await delay(tinyDelayMs)
const word8 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[8]}')]`)) await word4.click()
await word8.click() await delay(tinyDelayMs)
await delay(tinyDelayMs)
const word9 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[9]}')]`)) const word5 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[5]}')]`), 10000)
await word9.click()
await delay(tinyDelayMs)
const word10 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[10]}')]`)) await word5.click()
await word10.click() await delay(tinyDelayMs)
await delay(tinyDelayMs)
const word11 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[11]}')]`)) const word6 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[6]}')]`), 10000)
await word11.click()
await delay(tinyDelayMs) 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)
}
}
it('can retype the seed phrase', async () => {
const words = seedPhrase.split(' ')
await retypeSeedPhrase(words)
const confirm = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) const confirm = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirm.click() await confirm.click()
@ -217,7 +260,8 @@ describe('MetaMask', function () {
}) })
it('clicks through the deposit modal', async () => { it('clicks through the deposit modal', async () => {
const buyModal = await driver.findElement(By.css('span .modal')) const byBuyModal = By.css('span .modal')
const buyModal = await driver.wait(until.elementLocated(byBuyModal))
const closeModal = await findElement(driver, By.css('.page-container__header-close')) const closeModal = await findElement(driver, By.css('.page-container__header-close'))
await closeModal.click() await closeModal.click()
await driver.wait(until.stalenessOf(buyModal)) await driver.wait(until.stalenessOf(buyModal))
@ -231,8 +275,12 @@ describe('MetaMask', function () {
await driver.findElement(By.css('.qr-wrapper')).isDisplayed() await driver.findElement(By.css('.qr-wrapper')).isDisplayed()
await delay(regularDelayMs) await delay(regularDelayMs)
let accountModal = await driver.findElement(By.css('span .modal'))
await driver.executeScript("document.querySelector('.account-modal-close').click()") await driver.executeScript("document.querySelector('.account-modal-close').click()")
await delay(regularDelayMs * 4)
await driver.wait(until.stalenessOf(accountModal))
await delay(regularDelayMs)
}) })
}) })
@ -332,8 +380,11 @@ describe('MetaMask', function () {
await configureGas.click() await configureGas.click()
await delay(regularDelayMs) await delay(regularDelayMs)
const gasModal = await driver.findElement(By.css('span .modal'))
const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`)) const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`))
await save.click() await save.click()
await driver.wait(until.stalenessOf(gasModal))
await delay(regularDelayMs) await delay(regularDelayMs)
// Continue to next screen // Continue to next screen
@ -352,19 +403,20 @@ describe('MetaMask', function () {
const transactions = await findElements(driver, By.css('.tx-list-item')) const transactions = await findElements(driver, By.css('.tx-list-item'))
assert.equal(transactions.length, 1) assert.equal(transactions.length, 1)
const txValues = await findElements(driver, By.css('.tx-list-value')) const txValues = await findElement(driver, By.css('.tx-list-value'))
assert.equal(txValues.length, 1) await driver.wait(until.elementTextMatches(txValues, /1\sETH/), 10000)
assert.equal(await txValues[0].getText(), '1 ETH')
}) })
}) })
describe('Send ETH from Faucet', () => { describe('Send ETH from Faucet', () => {
it('starts a send transaction inside Faucet', async () => { it('starts a send transaction inside Faucet', async () => {
await driver.executeScript('window.open("https://faucet.metamask.io")') await openNewPage(driver, 'https://faucet.metamask.io')
await delay(waitingNewPageDelayMs)
const [extension, faucet] = await driver.getAllWindowHandles() const [extension, faucet] = await driver.getAllWindowHandles()
await driver.switchTo().window(faucet) await driver.switchTo().window(faucet)
const faucetPageTitle = await findElement(driver, By.css('.container-fluid'))
await driver.wait(until.elementTextMatches(faucetPageTitle, /MetaMask/))
await delay(regularDelayMs) await delay(regularDelayMs)
const send1eth = await findElement(driver, By.xpath(`//button[contains(text(), '10 ether')]`), 14000) const send1eth = await findElement(driver, By.xpath(`//button[contains(text(), '10 ether')]`), 14000)
@ -390,47 +442,133 @@ describe('MetaMask', function () {
}) })
}) })
describe('Add existing token using search', () => { describe('Deploy contract and call contract methods', () => {
it('clicks on the Add Token button', async () => { let extension
const addToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Token')]`)) let contractTestPage
await addToken.click() it('confirms a deploy contract transaction', async () => {
await openNewPage(driver, 'http://127.0.0.1:8080/');
[extension, contractTestPage] = await driver.getAllWindowHandles()
await delay(regularDelayMs)
const deployContractButton = await findElement(driver, By.css('#deployButton'))
await deployContractButton.click()
await delay(regularDelayMs)
await driver.switchTo().window(extension)
await delay(regularDelayMs) await delay(regularDelayMs)
const txListItem = await findElement(driver, By.css('.tx-list-item'))
await txListItem.click()
await delay(regularDelayMs)
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirmButton.click()
await delay(regularDelayMs)
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
const txAccounts = await findElements(driver, By.css('.tx-list-account'))
assert.equal(await txAccounts[0].getText(), 'Contract Deployment')
}) })
it('can pick a token from the existing options', async () => { it('calls and confirms a contract method where ETH is sent', async () => {
const tokenSearch = await findElement(driver, By.css('#search-tokens')) await driver.switchTo().window(contractTestPage)
await tokenSearch.sendKeys('BAT')
await delay(regularDelayMs) await delay(regularDelayMs)
const token = await findElement(driver, By.xpath("//span[contains(text(), 'BAT')]")) const depositButton = await findElement(driver, By.css('#depositButton'))
await token.click() await depositButton.click()
await delay(regularDelayMs) await delay(regularDelayMs)
const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), 'Next')]`)) await driver.switchTo().window(extension)
await nextScreen.click()
await delay(regularDelayMs) await delay(regularDelayMs)
const addTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Tokens')]`)) const txListItem = await findElement(driver, By.css('.tx-list-item'))
await addTokens.click() await txListItem.click()
await delay(largeDelayMs) await delay(regularDelayMs)
// Set the gas limit
const configureGas = await findElement(driver, By.css('.sliders-icon-container'))
await configureGas.click()
await delay(regularDelayMs)
let gasModal = await driver.findElement(By.css('span .modal'))
await driver.wait(until.elementLocated(By.css('.send-v2__customize-gas__title')))
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.customize-gas-input'))
await gasPriceInput.clear()
await gasPriceInput.sendKeys('10')
await gasLimitInput.clear()
await gasLimitInput.sendKeys('60001')
const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`))
await save.click()
await delay(regularDelayMs)
await driver.wait(until.stalenessOf(gasModal))
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirmButton.click()
await delay(regularDelayMs)
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
const txValues = await findElement(driver, By.css('.tx-list-value'))
await driver.wait(until.elementTextMatches(txValues, /3\sETH/), 10000)
const txAccounts = await findElements(driver, By.css('.tx-list-account'))
const firstTxAddress = await txAccounts[0].getText()
assert(firstTxAddress.match(/^0x\w{8}\.{3}\w{4}$/))
}) })
it('renders the balance for the chosen token', async () => { it('calls and confirms a contract method where ETH is received', async () => {
await driver.switchTo().window(contractTestPage)
await delay(regularDelayMs)
const withdrawButton = await findElement(driver, By.css('#withdrawButton'))
await withdrawButton.click()
await delay(regularDelayMs)
await driver.switchTo().window(extension)
await delay(regularDelayMs)
const txListItem = await findElement(driver, By.css('.tx-list-item'))
await txListItem.click()
await delay(regularDelayMs)
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirmButton.click()
await delay(regularDelayMs)
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
const txValues = await findElement(driver, By.css('.tx-list-value'))
await driver.wait(until.elementTextMatches(txValues, /0\sETH/), 10000)
await driver.switchTo().window(contractTestPage)
await driver.close()
await driver.switchTo().window(extension)
})
it('renders the correct ETH balance', async () => {
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount')) const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
await driver.wait(until.elementTextIs(balance, '0BAT')) await driver.wait(until.elementTextMatches(balance, /^86.*ETH.*$/), 10000)
const tokenAmount = await balance.getText() const tokenAmount = await balance.getText()
assert.equal(tokenAmount, '0BAT') assert.ok(/^86.*ETH.*$/.test(tokenAmount))
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
}) })
describe('Add a custom token from TokenFactory', () => { describe('Add a custom token from TokenFactory', () => {
it('creates a new token', async () => { it('creates a new token', async () => {
await driver.executeScript('window.open("https://tokenfactory.surge.sh/#/factory")') openNewPage(driver, 'https://tokenfactory.surge.sh/#/factory')
await delay(waitingNewPageDelayMs)
await delay(regularDelayMs * 10)
const [extension, tokenFactory] = await driver.getAllWindowHandles() const [extension, tokenFactory] = await driver.getAllWindowHandles()
await driver.switchTo().window(tokenFactory)
const [ const [
totalSupply, totalSupply,
tokenName, tokenName,
@ -457,12 +595,16 @@ describe('MetaMask', function () {
await driver.switchTo().window(tokenFactory) await driver.switchTo().window(tokenFactory)
await delay(regularDelayMs) await delay(regularDelayMs)
const tokenContactAddress = await driver.findElement(By.css('div > div > div:nth-child(2) > span:nth-child(3)')) const tokenContactAddress = await driver.findElement(By.css('div > div > div:nth-child(2) > span:nth-child(3)'))
tokenAddress = await tokenContactAddress.getText() tokenAddress = await tokenContactAddress.getText()
await driver.close() await driver.close()
await driver.switchTo().window(extension) await driver.switchTo().window(extension)
await loadExtension(driver, extensionId) await loadExtension(driver, extensionId)
await driver.switchTo().window(extension)
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
it('clicks on the Add Token button', async () => { it('clicks on the Add Token button', async () => {
@ -491,9 +633,217 @@ describe('MetaMask', function () {
it('renders the balance for the new token', async () => { it('renders the balance for the new token', async () => {
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount')) const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
await driver.wait(until.elementTextIs(balance, '100TST')) await driver.wait(until.elementTextMatches(balance, /^100\s*TST\s*$/))
const tokenAmount = await balance.getText() const tokenAmount = await balance.getText()
assert.equal(tokenAmount, '100TST') assert.ok(/^100\s*TST\s*$/.test(tokenAmount))
await delay(regularDelayMs)
})
})
describe('Send token from inside MetaMask', () => {
let gasModal
it('starts to send a transaction', async function () {
const sendButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Send')]`))
await sendButton.click()
await delay(regularDelayMs)
const inputAddress = await findElement(driver, By.css('input[placeholder="Recipient Address"]'))
const inputAmount = await findElement(driver, By.css('.currency-display__input'))
await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
await inputAmount.sendKeys('50')
// Set the gas limit
const configureGas = await findElement(driver, By.css('.send-v2__gas-fee-display button'))
await configureGas.click()
await delay(regularDelayMs)
gasModal = await driver.findElement(By.css('span .modal'))
})
it('customizes gas', async () => {
await driver.wait(until.elementLocated(By.css('.send-v2__customize-gas__title')))
const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`))
await save.click()
await delay(regularDelayMs)
})
it('transitions to the confirm screen', async () => {
await driver.wait(until.stalenessOf(gasModal))
// Continue to next screen
const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), 'Next')]`))
await nextScreen.click()
await delay(regularDelayMs)
})
it('submits the transaction', async function () {
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirmButton.click()
await delay(regularDelayMs)
})
it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.tx-list-item'))
assert.equal(transactions.length, 1)
const txValues = await findElements(driver, By.css('.tx-list-value'))
assert.equal(txValues.length, 1)
// test cancelled on firefox until https://github.com/mozilla/geckodriver/issues/906 is resolved,
// or possibly until we use latest version of firefox in the tests
if (process.env.SELENIUM_BROWSER !== 'firefox') {
await driver.wait(until.elementTextMatches(txValues[0], /50\sTST/), 10000)
}
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
const tx = await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed|Failed/), 10000)
assert.equal(await tx.getText(), 'Confirmed')
})
})
describe('Send a custom token from TokenFactory', () => {
let gasModal
it('sends an already created token', async () => {
openNewPage(driver, `https://tokenfactory.surge.sh/#/token/${tokenAddress}`)
const [extension, tokenFactory] = await driver.getAllWindowHandles()
const [
transferToAddress,
transferToAmount,
] = await findElements(driver, By.css('.form-control'))
await transferToAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
await transferToAmount.sendKeys('26')
const transferAmountButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Transfer Amount')]`))
await transferAmountButton.click()
await delay(regularDelayMs)
const [,, popup] = await driver.getAllWindowHandles()
await driver.switchTo().window(popup)
await driver.close()
await driver.switchTo().window(extension)
await delay(regularDelayMs)
const [txListItem] = await findElements(driver, By.css('.tx-list-item'))
await txListItem.click()
await delay(regularDelayMs)
// Set the gas limit
const configureGas = await driver.wait(until.elementLocated(By.css('.send-v2__gas-fee-display button')))
await configureGas.click()
await delay(regularDelayMs)
gasModal = await driver.findElement(By.css('span .modal'))
})
it('customizes gas', async () => {
await driver.wait(until.elementLocated(By.css('.send-v2__customize-gas__title')))
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.customize-gas-input'))
await gasPriceInput.clear()
await delay(tinyDelayMs)
await gasPriceInput.sendKeys('10')
await delay(tinyDelayMs)
await gasLimitInput.clear()
await delay(tinyDelayMs)
await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'a'))
await gasLimitInput.sendKeys('60000')
await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'e'))
// Needed for different behaviour of input in different versions of firefox
const gasLimitInputValue = await gasLimitInput.getAttribute('value')
if (gasLimitInputValue === '600001') {
await gasLimitInput.sendKeys(Key.BACK_SPACE)
}
const save = await findElement(driver, By.css('.send-v2__customize-gas__save'))
await save.click()
await driver.wait(until.stalenessOf(gasModal))
const gasFeeInput = await findElement(driver, By.css('.currency-display__input'))
assert.equal(await gasFeeInput.getAttribute('value'), 0.0006)
})
it('submits the transaction', async function () {
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirmButton.click()
await delay(regularDelayMs)
})
it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.tx-list-item'))
assert.equal(transactions.length, 2)
const txValues = await findElements(driver, By.css('.tx-list-value'))
await driver.wait(until.elementTextMatches(txValues[0], /26\sTST/))
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
const walletBalance = await findElement(driver, By.css('.wallet-balance'))
await walletBalance.click()
const tokenListItems = await findElements(driver, By.css('.token-list-item'))
await tokenListItems[0].click()
// test cancelled on firefox until https://github.com/mozilla/geckodriver/issues/906 is resolved,
// or possibly until we use latest version of firefox in the tests
if (process.env.SELENIUM_BROWSER !== 'firefox') {
const tokenBalanceAmount = await findElement(driver, By.css('.token-balance__amount'))
assert.equal(await tokenBalanceAmount.getText(), '24')
}
})
})
describe('Hide token', () => {
it('hides the token when clicked', async () => {
const [hideTokenEllipsis] = await findElements(driver, By.css('.token-list-item__ellipsis'))
await hideTokenEllipsis.click()
const byTokenMenuDropdownOption = By.css('.menu__item--clickable')
const tokenMenuDropdownOption = await driver.wait(until.elementLocated(byTokenMenuDropdownOption))
await tokenMenuDropdownOption.click()
const confirmHideModal = await findElement(driver, By.css('span .modal'))
const byHideTokenConfirmationButton = By.css('.hide-token-confirmation__button')
const hideTokenConfirmationButton = await driver.wait(until.elementLocated(byHideTokenConfirmationButton))
await hideTokenConfirmationButton.click()
await driver.wait(until.stalenessOf(confirmHideModal))
})
})
describe('Add existing token using search', () => {
it('clicks on the Add Token button', async () => {
const addToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Token')]`))
await addToken.click()
await delay(regularDelayMs)
})
it('can pick a token from the existing options', async () => {
const tokenSearch = await findElement(driver, By.css('#search-tokens'))
await tokenSearch.sendKeys('BAT')
await delay(regularDelayMs)
const token = await findElement(driver, By.xpath("//span[contains(text(), 'BAT')]"))
await token.click()
await delay(regularDelayMs)
const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), 'Next')]`))
await nextScreen.click()
await delay(regularDelayMs)
const addTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Tokens')]`))
await addTokens.click()
await delay(largeDelayMs)
})
it('renders the balance for the chosen token', async () => {
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
await driver.wait(until.elementTextMatches(balance, /0\sBAT/))
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
}) })

@ -6,5 +6,5 @@ set -o pipefail
export PATH="$PATH:./node_modules/.bin" export PATH="$PATH:./node_modules/.bin"
shell-parallel -s 'npm run ganache:start' -x 'sleep 5 && mocha test/e2e/beta/metamask-beta-ui.spec' 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' -x 'sleep 5 && mocha test/e2e/beta/from-import-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'

@ -4,7 +4,7 @@ const path = require('path')
const assert = require('assert') const assert = require('assert')
const pify = require('pify') const pify = require('pify')
const webdriver = require('selenium-webdriver') const webdriver = require('selenium-webdriver')
const { By, Key } = webdriver const { By, Key, until } = webdriver
const { delay, buildChromeWebDriver, buildFirefoxWebdriver, installWebExt, getExtensionIdChrome, getExtensionIdFirefox } = require('./func') const { delay, buildChromeWebDriver, buildFirefoxWebdriver, installWebExt, getExtensionIdChrome, getExtensionIdFirefox } = require('./func')
describe('Metamask popup page', function () { describe('Metamask popup page', function () {
@ -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'))
@ -218,7 +229,11 @@ describe('Metamask popup page', function () {
it('confirms transaction', async function () { it('confirms transaction', async function () {
await delay(300) await delay(300)
await driver.findElement(By.css('#pending-tx-form > div.flex-row.flex-space-around.conf-buttons > input')).click() const bySubmitButton = By.css('#pending-tx-form > div.flex-row.flex-space-around.conf-buttons > input')
const submitButton = await driver.wait(until.elementLocated(bySubmitButton))
submitButton.click()
await delay(500) await delay(500)
}) })
@ -258,7 +273,8 @@ describe('Metamask popup page', function () {
it('confirms transaction in MetaMask popup', async function () { it('confirms transaction in MetaMask popup', async function () {
const windowHandles = await driver.getAllWindowHandles() const windowHandles = await driver.getAllWindowHandles()
await driver.switchTo().window(windowHandles[windowHandles.length - 1]) await driver.switchTo().window(windowHandles[windowHandles.length - 1])
const metamaskSubmit = await driver.findElement(By.css('#pending-tx-form > div.flex-row.flex-space-around.conf-buttons > input')) const byMetamaskSubmit = By.css('#pending-tx-form > div.flex-row.flex-space-around.conf-buttons > input')
const metamaskSubmit = await driver.wait(until.elementLocated(byMetamaskSubmit))
await metamaskSubmit.click() await metamaskSubmit.click()
await delay(1000) await delay(1000)
}) })

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

@ -177,6 +177,8 @@ var actions = {
CLEAR_SEND: 'CLEAR_SEND', CLEAR_SEND: 'CLEAR_SEND',
OPEN_FROM_DROPDOWN: 'OPEN_FROM_DROPDOWN', OPEN_FROM_DROPDOWN: 'OPEN_FROM_DROPDOWN',
CLOSE_FROM_DROPDOWN: 'CLOSE_FROM_DROPDOWN', CLOSE_FROM_DROPDOWN: 'CLOSE_FROM_DROPDOWN',
GAS_LOADING_STARTED: 'GAS_LOADING_STARTED',
GAS_LOADING_FINISHED: 'GAS_LOADING_FINISHED',
setGasLimit, setGasLimit,
setGasPrice, setGasPrice,
updateGasData, updateGasData,
@ -192,6 +194,8 @@ var actions = {
updateSendErrors, updateSendErrors,
clearSend, clearSend,
setSelectedAddress, setSelectedAddress,
gasLoadingStarted,
gasLoadingFinished,
// app messages // app messages
confirmSeedWords: confirmSeedWords, confirmSeedWords: confirmSeedWords,
showAccountDetail: showAccountDetail, showAccountDetail: showAccountDetail,
@ -782,8 +786,9 @@ function updateGasData ({
to, to,
value, value,
}) { }) {
const estimatedGasPrice = estimateGasPriceFromRecentBlocks(recentBlocks)
return (dispatch) => { return (dispatch) => {
dispatch(actions.gasLoadingStarted())
const estimatedGasPrice = estimateGasPriceFromRecentBlocks(recentBlocks)
return Promise.all([ return Promise.all([
Promise.resolve(estimatedGasPrice), Promise.resolve(estimatedGasPrice),
estimateGas({ estimateGas({
@ -804,14 +809,28 @@ function updateGasData ({
.then((gasEstimate) => { .then((gasEstimate) => {
dispatch(actions.setGasTotal(gasEstimate)) dispatch(actions.setGasTotal(gasEstimate))
dispatch(updateSendErrors({ gasLoadingError: null })) dispatch(updateSendErrors({ gasLoadingError: null }))
dispatch(actions.gasLoadingFinished())
}) })
.catch(err => { .catch(err => {
log.error(err) log.error(err)
dispatch(updateSendErrors({ gasLoadingError: 'gasLoadingError' })) dispatch(updateSendErrors({ gasLoadingError: 'gasLoadingError' }))
dispatch(actions.gasLoadingFinished())
}) })
} }
} }
function gasLoadingStarted () {
return {
type: actions.GAS_LOADING_STARTED,
}
}
function gasLoadingFinished () {
return {
type: actions.GAS_LOADING_FINISHED,
}
}
function updateSendTokenBalance ({ function updateSendTokenBalance ({
selectedToken, selectedToken,
tokenContract, tokenContract,

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

@ -33,6 +33,7 @@ const {
const { const {
getGasPrice, getGasPrice,
getGasLimit, getGasLimit,
getGasIsLoading,
getForceGasMin, getForceGasMin,
conversionRateSelector, conversionRateSelector,
getSendAmount, getSendAmount,
@ -51,6 +52,7 @@ function mapStateToProps (state) {
return { return {
gasPrice: getGasPrice(state), gasPrice: getGasPrice(state),
gasLimit: getGasLimit(state), gasLimit: getGasLimit(state),
gasIsLoading: getGasIsLoading(state),
forceGasMin: getForceGasMin(state), forceGasMin: getForceGasMin(state),
conversionRate, conversionRate,
amount: getSendAmount(state), amount: getSendAmount(state),
@ -73,7 +75,7 @@ function mapDispatchToProps (dispatch) {
} }
} }
function getOriginalState (props) { function getFreshState (props) {
const gasPrice = props.gasPrice || MIN_GAS_PRICE_DEC const gasPrice = props.gasPrice || MIN_GAS_PRICE_DEC
const gasLimit = props.gasLimit || MIN_GAS_LIMIT_DEC const gasLimit = props.gasLimit || MIN_GAS_LIMIT_DEC
@ -97,7 +99,11 @@ inherits(CustomizeGasModal, Component)
function CustomizeGasModal (props) { function CustomizeGasModal (props) {
Component.call(this) Component.call(this)
this.state = getOriginalState(props) const originalState = getFreshState(props)
this.state = {
...originalState,
originalState,
}
} }
CustomizeGasModal.contextTypes = { CustomizeGasModal.contextTypes = {
@ -106,6 +112,36 @@ CustomizeGasModal.contextTypes = {
module.exports = connect(mapStateToProps, mapDispatchToProps)(CustomizeGasModal) module.exports = connect(mapStateToProps, mapDispatchToProps)(CustomizeGasModal)
CustomizeGasModal.prototype.componentWillReceiveProps = function (nextProps) {
const currentState = getFreshState(this.props)
const {
gasPrice: currentGasPrice,
gasLimit: currentGasLimit,
} = currentState
const newState = getFreshState(nextProps)
const {
gasPrice: newGasPrice,
gasLimit: newGasLimit,
gasTotal: newGasTotal,
} = newState
const gasPriceChanged = currentGasPrice !== newGasPrice
const gasLimitChanged = currentGasLimit !== newGasLimit
if (gasPriceChanged) {
this.setState({
gasPrice: newGasPrice,
gasTotal: newGasTotal,
priceSigZeros: '',
priceSigDec: '',
})
}
if (gasLimitChanged) {
this.setState({ gasLimit: newGasLimit, gasTotal: newGasTotal })
}
if (gasLimitChanged || gasPriceChanged) {
this.validate({ gasLimit: newGasLimit, gasTotal: newGasTotal })
}
}
CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) { CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) {
const { const {
@ -137,7 +173,7 @@ CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) {
} }
CustomizeGasModal.prototype.revert = function () { CustomizeGasModal.prototype.revert = function () {
this.setState(getOriginalState(this.props)) this.setState(this.state.originalState)
} }
CustomizeGasModal.prototype.validate = function ({ gasTotal, gasLimit }) { CustomizeGasModal.prototype.validate = function ({ gasTotal, gasLimit }) {
@ -233,7 +269,7 @@ CustomizeGasModal.prototype.convertAndSetGasPrice = function (newGasPrice) {
} }
CustomizeGasModal.prototype.render = function () { CustomizeGasModal.prototype.render = function () {
const { hideModal, forceGasMin } = this.props const { hideModal, forceGasMin, gasIsLoading } = this.props
const { gasPrice, gasLimit, gasTotal, error, priceSigZeros, priceSigDec } = this.state const { gasPrice, gasLimit, gasTotal, error, priceSigZeros, priceSigDec } = this.state
let convertedGasPrice = conversionUtil(gasPrice, { let convertedGasPrice = conversionUtil(gasPrice, {
@ -266,7 +302,7 @@ CustomizeGasModal.prototype.render = function () {
toNumericBase: 'dec', toNumericBase: 'dec',
}) })
return h('div.send-v2__customize-gas', {}, [ return !gasIsLoading && h('div.send-v2__customize-gas', {}, [
h('div.send-v2__customize-gas__content', { h('div.send-v2__customize-gas__content', {
}, [ }, [
h('div.send-v2__customize-gas__header', {}, [ h('div.send-v2__customize-gas__header', {}, [
@ -288,6 +324,7 @@ CustomizeGasModal.prototype.render = function () {
onChange: value => this.convertAndSetGasPrice(value), onChange: value => this.convertAndSetGasPrice(value),
title: this.context.t('gasPrice'), title: this.context.t('gasPrice'),
copy: this.context.t('gasPriceCalculation'), copy: this.context.t('gasPriceCalculation'),
gasIsLoading,
}), }),
h(GasModalCard, { h(GasModalCard, {
@ -297,6 +334,7 @@ CustomizeGasModal.prototype.render = function () {
onChange: value => this.convertAndSetGasLimit(value), onChange: value => this.convertAndSetGasLimit(value),
title: this.context.t('gasLimit'), title: this.context.t('gasLimit'),
copy: this.context.t('gasLimitCalculation'), copy: this.context.t('gasLimitCalculation'),
gasIsLoading,
}), }),
]), ]),

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

@ -12,6 +12,7 @@ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
const connect = require('react-redux').connect const connect = require('react-redux').connect
const ToAutoComplete = require('./send/to-autocomplete') const ToAutoComplete = require('./send/to-autocomplete')
const log = require('loglevel') const log = require('loglevel')
const { isValidENSAddress } = require('../util')
EnsInput.contextTypes = { EnsInput.contextTypes = {
t: PropTypes.func, t: PropTypes.func,
@ -25,31 +26,34 @@ function EnsInput () {
Component.call(this) Component.call(this)
} }
EnsInput.prototype.render = function () { EnsInput.prototype.onChange = function (recipient) {
const props = this.props const network = this.props.network
const opts = extend(props, { const networkHasEnsSupport = getNetworkEnsSupport(network)
list: 'addresses',
onChange: (recipient) => {
const network = this.props.network
const networkHasEnsSupport = getNetworkEnsSupport(network)
props.onChange(recipient) this.props.onChange({ toAddress: recipient })
if (!networkHasEnsSupport) return if (!networkHasEnsSupport) return
if (recipient.match(ensRE) === null) { if (recipient.match(ensRE) === null) {
return this.setState({ return this.setState({
loadingEns: false, loadingEns: false,
ensResolution: null, ensResolution: null,
ensFailure: null, ensFailure: null,
}) toError: null,
} })
}
this.setState({ this.setState({
loadingEns: true, loadingEns: true,
}) })
this.checkName(recipient) this.checkName(recipient)
}, }
EnsInput.prototype.render = function () {
const props = this.props
const opts = extend(props, {
list: 'addresses',
onChange: this.onChange.bind(this),
}) })
return h('div', { return h('div', {
style: { width: '100%', position: 'relative' }, style: { width: '100%', position: 'relative' },
@ -85,17 +89,27 @@ EnsInput.prototype.lookupEnsName = function (recipient) {
nickname: recipient.trim(), nickname: recipient.trim(),
hoverText: address + '\n' + this.context.t('clickCopy'), hoverText: address + '\n' + this.context.t('clickCopy'),
ensFailure: false, ensFailure: false,
toError: null,
}) })
} }
}) })
.catch((reason) => { .catch((reason) => {
log.error(reason) const setStateObj = {
return this.setState({
loadingEns: false, loadingEns: false,
ensResolution: ZERO_ADDRESS, ensResolution: recipient,
ensFailure: true, ensFailure: true,
hoverText: reason.message, toError: null,
}) }
if (isValidENSAddress(recipient) && reason.message === 'ENS name not defined.') {
setStateObj.hoverText = this.context.t('ensNameNotFound')
setStateObj.toError = 'ensNameNotFound'
setStateObj.ensFailure = false
} else {
log.error(reason)
setStateObj.hoverText = reason.message
}
return this.setState(setStateObj)
}) })
} }
@ -105,9 +119,14 @@ EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) {
// If an address is sent without a nickname, meaning not from ENS or from // If an address is sent without a nickname, meaning not from ENS or from
// the user's own accounts, a default of a one-space string is used. // the user's own accounts, a default of a one-space string is used.
const nickname = state.nickname || ' ' const nickname = state.nickname || ' '
if (prevProps.network !== this.props.network) {
const provider = global.ethereumProvider
this.ens = new ENS({ provider, network: this.props.network })
this.onChange(ensResolution)
}
if (prevState && ensResolution && this.props.onChange && if (prevState && ensResolution && this.props.onChange &&
ensResolution !== prevState.ensResolution) { ensResolution !== prevState.ensResolution) {
this.props.onChange(ensResolution, nickname) this.props.onChange({ toAddress: ensResolution, nickname, toError: state.toError })
} }
} }
@ -124,7 +143,9 @@ EnsInput.prototype.ensIcon = function (recipient) {
} }
EnsInput.prototype.ensIconContents = function (recipient) { EnsInput.prototype.ensIconContents = function (recipient) {
const { loadingEns, ensFailure, ensResolution } = this.state || { ensResolution: ZERO_ADDRESS} const { loadingEns, ensFailure, ensResolution, toError } = this.state || { ensResolution: ZERO_ADDRESS }
if (toError) return
if (loadingEns) { if (loadingEns) {
return h('img', { return h('img', {

@ -36,6 +36,7 @@ IdenticonComponent.prototype.render = function () {
key: 'identicon-' + address, key: 'identicon-' + address,
style: { style: {
display: 'flex', display: 'flex',
flexShrink: 0,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
height: diameter, height: diameter,

@ -22,12 +22,16 @@ function isValidInput (text) {
return re.test(text) return re.test(text)
} }
function removeLeadingZeroes (str) {
return str.replace(/^0*(?=\d)/, '')
}
InputNumber.prototype.setValue = function (newValue) { InputNumber.prototype.setValue = function (newValue) {
newValue = removeLeadingZeroes(newValue)
if (newValue && !isValidInput(newValue)) return if (newValue && !isValidInput(newValue)) return
const { fixed, min = -1, max = Infinity, onChange } = this.props const { fixed, min = -1, max = Infinity, onChange } = this.props
newValue = fixed ? newValue.toFixed(4) : newValue newValue = fixed ? newValue.toFixed(4) : newValue
const newValueGreaterThanMin = conversionGTE( const newValueGreaterThanMin = conversionGTE(
{ value: newValue || '0', fromNumericBase: 'dec' }, { value: newValue || '0', fromNumericBase: 'dec' },
{ value: min, fromNumericBase: 'hex' }, { value: min, fromNumericBase: 'hex' },
@ -47,7 +51,7 @@ InputNumber.prototype.setValue = function (newValue) {
} }
InputNumber.prototype.render = function () { InputNumber.prototype.render = function () {
const { unitLabel, step = 1, placeholder, value = 0 } = this.props const { unitLabel, step = 1, placeholder, value } = this.props
return h('div.customize-gas-input-wrapper', {}, [ return h('div.customize-gas-input-wrapper', {}, [
h('input', { h('input', {
@ -63,11 +67,11 @@ InputNumber.prototype.render = function () {
h('span.gas-tooltip-input-detail', {}, [unitLabel]), h('span.gas-tooltip-input-detail', {}, [unitLabel]),
h('div.gas-tooltip-input-arrows', {}, [ h('div.gas-tooltip-input-arrows', {}, [
h('i.fa.fa-angle-up', { h('i.fa.fa-angle-up', {
onClick: () => this.setValue(addCurrencies(value, step)), onClick: () => this.setValue(addCurrencies(value, step, { toNumericBase: 'dec' })),
}), }),
h('i.fa.fa-angle-down', { h('i.fa.fa-angle-down', {
style: { cursor: 'pointer' }, style: { cursor: 'pointer' },
onClick: () => this.setValue(subtractCurrencies(value, step)), onClick: () => this.setValue(subtractCurrencies(value, step, { toNumericBase: 'dec' })),
}), }),
]), ]),
]) ])

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

@ -20,7 +20,7 @@ const {
calcGasTotal, calcGasTotal,
isBalanceSufficient, isBalanceSufficient,
} = require('../send_/send.utils') } = require('../send_/send.utils')
const GasFeeDisplay = require('../send/gas-fee-display-v2') const GasFeeDisplay = require('../send_/send-content/send-gas-row/gas-fee-display/gas-fee-display.component').default
const SenderToRecipient = require('../sender-to-recipient') const SenderToRecipient = require('../sender-to-recipient')
const NetworkDisplay = require('../network-display') const NetworkDisplay = require('../network-display')
const currencyFormatter = require('currency-formatter') const currencyFormatter = require('currency-formatter')

@ -11,7 +11,7 @@ abiDecoder.addABI(tokenAbi)
const actions = require('../../actions') const actions = require('../../actions')
const clone = require('clone') const clone = require('clone')
const Identicon = require('../identicon') const Identicon = require('../identicon')
const GasFeeDisplay = require('../send/gas-fee-display-v2.js') const GasFeeDisplay = require('../send_/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js').default
const NetworkDisplay = require('../network-display') const NetworkDisplay = require('../network-display')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN const BN = ethUtil.BN

@ -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))
} }
@ -113,6 +118,7 @@ CurrencyDisplay.prototype.render = function () {
readOnly = false, readOnly = false,
inError = false, inError = false,
onBlur, onBlur,
step,
} = this.props } = this.props
const { valueToRender } = this.state const { valueToRender } = this.state
@ -147,6 +153,7 @@ CurrencyDisplay.prototype.render = function () {
width: this.getInputWidth(valueToRender, readOnly), width: this.getInputWidth(valueToRender, readOnly),
}, },
min: 0, min: 0,
step,
}), }),
h('span.currency-display__currency-symbol', primaryCurrency), h('span.currency-display__currency-symbol', primaryCurrency),

@ -1,53 +0,0 @@
const Component = require('react').Component
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const inherits = require('util').inherits
const CurrencyDisplay = require('./currency-display')
const connect = require('react-redux').connect
GasFeeDisplay.contextTypes = {
t: PropTypes.func,
}
module.exports = connect()(GasFeeDisplay)
inherits(GasFeeDisplay, Component)
function GasFeeDisplay () {
Component.call(this)
}
GasFeeDisplay.prototype.render = function () {
const {
conversionRate,
gasTotal,
onClick,
primaryCurrency = 'ETH',
convertedCurrency,
gasLoadingError,
} = this.props
return h('div.send-v2__gas-fee-display', [
gasTotal
? h(CurrencyDisplay, {
primaryCurrency,
convertedCurrency,
value: gasTotal,
conversionRate,
convertedPrefix: '$',
readOnly: true,
})
: gasLoadingError
? h('div.currency-display.currency-display--message', this.context.t('setGasPrice'))
: h('div.currency-display', this.context.t('loading')),
h('button.sliders-icon-container', {
onClick,
disabled: !gasTotal && !gasLoadingError,
}, [
h('i.fa.fa-sliders.sliders-icon'),
]),
])
}

@ -1,7 +1,7 @@
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { import {
getConversionRate, getConversionRate,
getConvertedCurrency, getCurrentCurrency,
} from '../send.selectors.js' } from '../send.selectors.js'
import AccountListItem from './account-list-item.component' import AccountListItem from './account-list-item.component'
@ -10,6 +10,6 @@ export default connect(mapStateToProps)(AccountListItem)
function mapStateToProps (state) { function mapStateToProps (state) {
return { return {
conversionRate: getConversionRate(state), conversionRate: getConversionRate(state),
currentCurrency: getConvertedCurrency(state), currentCurrency: getCurrentCurrency(state),
} }
} }

@ -12,7 +12,7 @@ proxyquire('../account-list-item.container.js', {
}, },
'../send.selectors.js': { '../send.selectors.js': {
getConversionRate: (s) => `mockConversionRate:${s}`, getConversionRate: (s) => `mockConversionRate:${s}`,
getConvertedCurrency: (s) => `mockCurrentCurrency:${s}`, getCurrentCurrency: (s) => `mockCurrentCurrency:${s}`,
}, },
}) })

@ -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,16 @@ 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}
step="any"
/> />
</SendRowWrapper> </SendRowWrapper>
) )

@ -2,7 +2,7 @@ import { connect } from 'react-redux'
import { import {
getAmountConversionRate, getAmountConversionRate,
getConversionRate, getConversionRate,
getConvertedCurrency, getCurrentCurrency,
getGasTotal, getGasTotal,
getPrimaryCurrency, getPrimaryCurrency,
getSelectedToken, getSelectedToken,
@ -31,7 +31,7 @@ function mapStateToProps (state) {
amountConversionRate: getAmountConversionRate(state), amountConversionRate: getAmountConversionRate(state),
balance: getSendFromBalance(state), balance: getSendFromBalance(state),
conversionRate: getConversionRate(state), conversionRate: getConversionRate(state),
convertedCurrency: getConvertedCurrency(state), convertedCurrency: getCurrentCurrency(state),
gasTotal: getGasTotal(state), gasTotal: getGasTotal(state),
inError: sendAmountIsInError(state), inError: sendAmountIsInError(state),
primaryCurrency: getPrimaryCurrency(state), primaryCurrency: getPrimaryCurrency(state),

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

@ -24,7 +24,7 @@ proxyquire('../send-amount-row.container.js', {
'../../send.selectors': { '../../send.selectors': {
getAmountConversionRate: (s) => `mockAmountConversionRate:${s}`, getAmountConversionRate: (s) => `mockAmountConversionRate:${s}`,
getConversionRate: (s) => `mockConversionRate:${s}`, getConversionRate: (s) => `mockConversionRate:${s}`,
getConvertedCurrency: (s) => `mockConvertedCurrency:${s}`, getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`,
getGasTotal: (s) => `mockGasTotal:${s}`, getGasTotal: (s) => `mockGasTotal:${s}`,
getPrimaryCurrency: (s) => `mockPrimaryCurrency:${s}`, getPrimaryCurrency: (s) => `mockPrimaryCurrency:${s}`,
getSelectedToken: (s) => `mockSelectedToken:${s}`, getSelectedToken: (s) => `mockSelectedToken:${s}`,

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

@ -0,0 +1,61 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import CurrencyDisplay from '../../../../send/currency-display'
export default class GasFeeDisplay extends Component {
static propTypes = {
conversionRate: PropTypes.number,
primaryCurrency: PropTypes.string,
convertedCurrency: PropTypes.string,
gasLoadingError: PropTypes.bool,
gasTotal: PropTypes.string,
onClick: PropTypes.func,
};
render() {
const {
conversionRate,
gasTotal,
onClick,
primaryCurrency = 'ETH',
convertedCurrency,
gasLoadingError,
} = this.props
return (
<div className="send-v2__gas-fee-display">
{gasTotal
? <CurrencyDisplay
primaryCurrency={primaryCurrency}
convertedCurrency={convertedCurrency}
value={gasTotal}
conversionRate={conversionRate}
gasLoadingError={gasLoadingError}
convertedPrefix={'$'}
readOnly
/>
: gasLoadingError
? <div className="currency-display.currency-display--message">
{this.context.t('setGasPrice')}
</div>
: <div className="currency-display">
{this.context.t('loading')}
</div>
}
<button
className="sliders-icon-container"
onClick={onClick}
disabled={!gasTotal && !gasLoadingError}
>
<i className="fa fa-sliders sliders-icon" />
</button>
</div>
)
}
}
GasFeeDisplay.contextTypes = {
t: PropTypes.func,
}

@ -0,0 +1 @@
export { default } from './gas-fee-display.component'

@ -0,0 +1,55 @@
import React from 'react'
import assert from 'assert'
import {shallow} from 'enzyme'
import GasFeeDisplay from '../gas-fee-display.component'
import CurrencyDisplay from '../../../../../send/currency-display'
import sinon from 'sinon'
const propsMethodSpies = {
showCustomizeGasModal: sinon.spy(),
}
describe('SendGasRow Component', function() {
let wrapper
beforeEach(() => {
wrapper = shallow(<GasFeeDisplay
conversionRate={20}
gasTotal={'mockGasTotal'}
onClick={propsMethodSpies.showCustomizeGasModal}
primaryCurrency={'mockPrimaryCurrency'}
convertedCurrency={'mockConvertedCurrency'}
/>, {context: {t: str => str + '_t'}})
})
afterEach(() => {
propsMethodSpies.showCustomizeGasModal.resetHistory()
})
describe('render', () => {
it('should render a CurrencyDisplay component', () => {
assert.equal(wrapper.find(CurrencyDisplay).length, 1)
})
it('should render the CurrencyDisplay with the correct props', () => {
const {
conversionRate,
convertedCurrency,
value,
} = wrapper.find(CurrencyDisplay).props()
assert.equal(conversionRate, 20)
assert.equal(convertedCurrency, 'mockConvertedCurrency')
assert.equal(value, 'mockGasTotal')
})
it('should render the Button with the correct props', () => {
const {
onClick,
} = wrapper.find('button').props()
assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 0)
onClick()
assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 1)
})
})
})

@ -1,7 +1,7 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import SendRowWrapper from '../send-row-wrapper/' import SendRowWrapper from '../send-row-wrapper/'
import GasFeeDisplay from '../../../send/gas-fee-display-v2' import GasFeeDisplay from './gas-fee-display/gas-fee-display.component'
export default class SendGasRow extends Component { export default class SendGasRow extends Component {

@ -1,7 +1,7 @@
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { import {
getConversionRate, getConversionRate,
getConvertedCurrency, getCurrentCurrency,
getGasTotal, getGasTotal,
} from '../../send.selectors.js' } from '../../send.selectors.js'
import { sendGasIsInError } from './send-gas-row.selectors.js' import { sendGasIsInError } from './send-gas-row.selectors.js'
@ -13,7 +13,7 @@ export default connect(mapStateToProps, mapDispatchToProps)(SendGasRow)
function mapStateToProps (state) { function mapStateToProps (state) {
return { return {
conversionRate: getConversionRate(state), conversionRate: getConversionRate(state),
convertedCurrency: getConvertedCurrency(state), convertedCurrency: getCurrentCurrency(state),
gasTotal: getGasTotal(state), gasTotal: getGasTotal(state),
gasLoadingError: sendGasIsInError(state), gasLoadingError: sendGasIsInError(state),
} }

@ -5,7 +5,7 @@ import sinon from 'sinon'
import SendGasRow from '../send-gas-row.component.js' import SendGasRow from '../send-gas-row.component.js'
import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component' import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component'
import GasFeeDisplay from '../../../../send/gas-fee-display-v2' import GasFeeDisplay from '../gas-fee-display/gas-fee-display.component'
const propsMethodSpies = { const propsMethodSpies = {
showCustomizeGasModal: sinon.spy(), showCustomizeGasModal: sinon.spy(),

@ -19,7 +19,7 @@ proxyquire('../send-gas-row.container.js', {
}, },
'../../send.selectors.js': { '../../send.selectors.js': {
getConversionRate: (s) => `mockConversionRate:${s}`, getConversionRate: (s) => `mockConversionRate:${s}`,
getConvertedCurrency: (s) => `mockConvertedCurrency:${s}`, getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`,
getGasTotal: (s) => `mockGasTotal:${s}`, getGasTotal: (s) => `mockGasTotal:${s}`,
}, },
'./send-gas-row.selectors.js': { sendGasIsInError: (s) => `mockGasLoadingError:${s}` }, './send-gas-row.selectors.js': { sendGasIsInError: (s) => `mockGasLoadingError:${s}` },

@ -19,9 +19,9 @@ export default class SendToRow extends Component {
updateSendToError: PropTypes.func, updateSendToError: PropTypes.func,
}; };
handleToChange (to, nickname = '') { handleToChange (to, nickname = '', toError) {
const { updateSendTo, updateSendToError, updateGas } = this.props const { updateSendTo, updateSendToError, updateGas } = this.props
const toErrorObject = getToErrorObject(to) const toErrorObject = getToErrorObject(to, toError)
updateSendTo(to, nickname) updateSendTo(to, nickname)
updateSendToError(toErrorObject) updateSendToError(toErrorObject)
if (toErrorObject.to === null) { if (toErrorObject.to === null) {
@ -53,7 +53,7 @@ export default class SendToRow extends Component {
inError={inError} inError={inError}
name={'address'} name={'address'}
network={network} network={network}
onChange={(newTo, newNickname) => this.handleToChange(newTo, newNickname)} onChange={({ toAddress, nickname, toError }) => this.handleToChange(toAddress, nickname, toError)}
openDropdown={() => openToDropdown()} openDropdown={() => openToDropdown()}
placeholder={this.context.t('recipientAddress')} placeholder={this.context.t('recipientAddress')}
to={to} to={to}

@ -4,12 +4,10 @@ const {
} = require('../../send.constants') } = require('../../send.constants')
const { isValidAddress } = require('../../../../util') const { isValidAddress } = require('../../../../util')
function getToErrorObject (to) { function getToErrorObject (to, toError = null) {
let toError = null
if (!to) { if (!to) {
toError = REQUIRED_ERROR toError = REQUIRED_ERROR
} else if (!isValidAddress(to)) { } else if (!isValidAddress(to) && !toError) {
toError = INVALID_RECIPIENT_ADDRESS_ERROR toError = INVALID_RECIPIENT_ADDRESS_ERROR
} }

@ -6,8 +6,8 @@ import proxyquire from 'proxyquire'
const SendToRow = proxyquire('../send-to-row.component.js', { const SendToRow = proxyquire('../send-to-row.component.js', {
'./send-to-row.utils.js': { './send-to-row.utils.js': {
getToErrorObject: (to) => ({ getToErrorObject: (to, toError) => ({
to: to === false ? null : `mockToErrorObject:${to}`, to: to === false ? null : `mockToErrorObject:${to}${toError}`,
}), }),
}, },
}).default }).default
@ -67,11 +67,11 @@ describe('SendToRow Component', function () {
it('should call updateSendToError', () => { it('should call updateSendToError', () => {
assert.equal(propsMethodSpies.updateSendToError.callCount, 0) assert.equal(propsMethodSpies.updateSendToError.callCount, 0)
instance.handleToChange('mockTo2') instance.handleToChange('mockTo2', '', 'mockToError')
assert.equal(propsMethodSpies.updateSendToError.callCount, 1) assert.equal(propsMethodSpies.updateSendToError.callCount, 1)
assert.deepEqual( assert.deepEqual(
propsMethodSpies.updateSendToError.getCall(0).args, propsMethodSpies.updateSendToError.getCall(0).args,
[{ to: 'mockToErrorObject:mockTo2' }] [{ to: 'mockToErrorObject:mockTo2mockToError' }]
) )
}) })
@ -138,11 +138,11 @@ describe('SendToRow Component', function () {
openDropdown() openDropdown()
assert.equal(propsMethodSpies.openToDropdown.callCount, 1) assert.equal(propsMethodSpies.openToDropdown.callCount, 1)
assert.equal(SendToRow.prototype.handleToChange.callCount, 0) assert.equal(SendToRow.prototype.handleToChange.callCount, 0)
onChange('mockNewTo', 'mockNewNickname') onChange({ toAddress: 'mockNewTo', nickname: 'mockNewNickname', toError: 'mockToError' })
assert.equal(SendToRow.prototype.handleToChange.callCount, 1) assert.equal(SendToRow.prototype.handleToChange.callCount, 1)
assert.deepEqual( assert.deepEqual(
SendToRow.prototype.handleToChange.getCall(0).args, SendToRow.prototype.handleToChange.getCall(0).args,
['mockNewTo', 'mockNewNickname'] ['mockNewTo', 'mockNewNickname', 'mockToError']
) )
}) })
}) })

@ -40,6 +40,12 @@ describe('send-to-row utils', () => {
to: null, to: null,
}) })
}) })
it('should return the passed error if to is truthy but invalid if to is truthy and valid', () => {
assert.deepEqual(getToErrorObject('invalid #$ 345878', 'someExplicitError'), {
to: 'someExplicitError',
})
})
}) })
}) })

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

@ -36,6 +36,7 @@ const ONE_GWEI_IN_WEI_HEX = ethUtil.addHexPrefix(conversionUtil('0x1', {
})) }))
const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send. const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send.
const BASE_TOKEN_GAS_COST = '0x186a0' // Hex for 100000, a base estimate for token transfers.
module.exports = { module.exports = {
INSUFFICIENT_FUNDS_ERROR, INSUFFICIENT_FUNDS_ERROR,
@ -52,4 +53,5 @@ module.exports = {
REQUIRED_ERROR, REQUIRED_ERROR,
SIMPLE_GAS_COST, SIMPLE_GAS_COST,
TOKEN_TRANSFER_FUNCTION_SIGNATURE, TOKEN_TRANSFER_FUNCTION_SIGNATURE,
BASE_TOKEN_GAS_COST,
} }

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

@ -14,7 +14,6 @@ const selectors = {
getAmountConversionRate, getAmountConversionRate,
getBlockGasLimit, getBlockGasLimit,
getConversionRate, getConversionRate,
getConvertedCurrency,
getCurrentAccountWithSendEtherInfo, getCurrentAccountWithSendEtherInfo,
getCurrentCurrency, getCurrentCurrency,
getCurrentNetwork, getCurrentNetwork,
@ -98,10 +97,6 @@ function getConversionRate (state) {
return state.metamask.conversionRate return state.metamask.conversionRate
} }
function getConvertedCurrency (state) {
return state.metamask.currentCurrency
}
function getCurrentAccountWithSendEtherInfo (state) { function getCurrentAccountWithSendEtherInfo (state) {
const currentAddress = getSelectedAddress(state) const currentAddress = getSelectedAddress(state)
const accounts = accountsWithSendEtherInfoSelector(state) const accounts = accountsWithSendEtherInfoSelector(state)

@ -4,11 +4,13 @@ const {
conversionGTE, conversionGTE,
multiplyCurrencies, multiplyCurrencies,
conversionGreaterThan, conversionGreaterThan,
conversionLessThan,
} = require('../../conversion-util') } = require('../../conversion-util')
const { const {
calcTokenAmount, calcTokenAmount,
} = require('../../token-util') } = require('../../token-util')
const { const {
BASE_TOKEN_GAS_COST,
INSUFFICIENT_FUNDS_ERROR, INSUFFICIENT_FUNDS_ERROR,
INSUFFICIENT_TOKENS_ERROR, INSUFFICIENT_TOKENS_ERROR,
NEGATIVE_ETH_ERROR, NEGATIVE_ETH_ERROR,
@ -20,6 +22,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 +30,7 @@ module.exports = {
estimateGasPriceFromRecentBlocks, estimateGasPriceFromRecentBlocks,
generateTokenTransferData, generateTokenTransferData,
getAmountErrorObject, getAmountErrorObject,
getToAddressForGasUpdate,
isBalanceSufficient, isBalanceSufficient,
isTokenBalanceSufficient, isTokenBalanceSufficient,
} }
@ -175,12 +179,13 @@ 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
} }
} else if (selectedToken && !to) {
return BASE_TOKEN_GAS_COST
} }
paramsForGasEstimate.to = selectedToken ? selectedToken.address : to paramsForGasEstimate.to = selectedToken ? selectedToken.address : to
@ -201,16 +206,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 +272,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',

@ -8,7 +8,6 @@ const {
getBlockGasLimit, getBlockGasLimit,
getAmountConversionRate, getAmountConversionRate,
getConversionRate, getConversionRate,
getConvertedCurrency,
getCurrentAccountWithSendEtherInfo, getCurrentAccountWithSendEtherInfo,
getCurrentCurrency, getCurrentCurrency,
getCurrentNetwork, getCurrentNetwork,
@ -154,15 +153,6 @@ describe('send selectors', () => {
}) })
}) })
describe('getConvertedCurrency()', () => {
it('should return the currently selected currency', () => {
assert.equal(
getConvertedCurrency(mockState),
'USD'
)
})
})
describe('getCurrentAccountWithSendEtherInfo()', () => { describe('getCurrentAccountWithSendEtherInfo()', () => {
it('should return the currently selected account with identity info', () => { it('should return the currently selected account with identity info', () => {
assert.deepEqual( assert.deepEqual(

@ -2,6 +2,7 @@ import assert from 'assert'
import sinon from 'sinon' import sinon from 'sinon'
import proxyquire from 'proxyquire' import proxyquire from 'proxyquire'
import { import {
BASE_TOKEN_GAS_COST,
ONE_GWEI_IN_WEI_HEX, ONE_GWEI_IN_WEI_HEX,
SIMPLE_GAS_COST, SIMPLE_GAS_COST,
} from '../send.constants' } from '../send.constants'
@ -18,10 +19,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 +33,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 +49,7 @@ const {
estimateGasPriceFromRecentBlocks, estimateGasPriceFromRecentBlocks,
generateTokenTransferData, generateTokenTransferData,
getAmountErrorObject, getAmountErrorObject,
getToAddressForGasUpdate,
calcTokenBalance, calcTokenBalance,
isBalanceSufficient, isBalanceSufficient,
isTokenBalanceSufficient, isTokenBalanceSufficient,
@ -255,7 +261,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 +285,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 +316,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,12 +325,23 @@ 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: '' } }))
assert.notEqual(result, SIMPLE_GAS_COST) assert.notEqual(result, SIMPLE_GAS_COST)
}) })
it(`should return ${BASE_TOKEN_GAS_COST} if passed a selectedToken but no to address`, async () => {
const result = await estimateGas(Object.assign({}, baseMockParams, { to: null, selectedToken: { address: '' } }))
assert.equal(result, BASE_TOKEN_GAS_COST)
})
it(`should return the adjusted blockGasLimit if it fails with a 'Transaction execution error.'`, async () => { it(`should return the adjusted blockGasLimit if it fails with a 'Transaction execution error.'`, async () => {
const result = await estimateGas(Object.assign({}, baseMockParams, { const result = await estimateGas(Object.assign({}, baseMockParams, {
to: 'isContract willFailBecauseOf:Transaction execution error.', to: 'isContract willFailBecauseOf:Transaction execution error.',
@ -401,4 +428,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')
})
})
}) })

@ -181,7 +181,7 @@ ShapeshiftForm.prototype.render = function () {
return h('div.shapeshift-form-wrapper', [ return h('div.shapeshift-form-wrapper', [
showQrCode showQrCode
? this.renderQrCode() ? this.renderQrCode()
: h('div.shapeshift-form', [ : h('div.modal-shapeshift-form', [
h('div.shapeshift-form__selectors', [ h('div.shapeshift-form__selectors', [
h('div.shapeshift-form__selector', [ h('div.shapeshift-form__selector', [

@ -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', {}, [
@ -197,6 +204,9 @@ SignatureRequest.prototype.renderBody = function () {
h('div.request-signature__rows', [ h('div.request-signature__rows', [
...rows.map(({ name, value }) => { ...rows.map(({ name, value }) => {
if (typeof value === 'boolean') {
value = value.toString()
}
return h('div.request-signature__row', [ return h('div.request-signature__row', [
h('div.request-signature__row-title', [`${name}:`]), h('div.request-signature__row-title', [`${name}:`]),
h('div.request-signature__row-value', value), h('div.request-signature__row-value', value),

@ -34,7 +34,7 @@ TokenBalance.prototype.render = function () {
return isLoading return isLoading
? h('span', '') ? h('span', '')
: h('span.token-balance', [ : h('span.token-balance', [
h('span.token-balance__amount', string), h('span.hide-text-overflow.token-balance__amount', string),
!balanceOnly && h('span.token-balance__symbol', symbol), !balanceOnly && h('span.token-balance__symbol', symbol),
]) ])
} }

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

@ -1,6 +1,5 @@
.currency-display { .currency-display {
height: 54px; height: 54px;
width: 100%ß;
border: 1px solid $alto; border: 1px solid $alto;
border-radius: 4px; border-radius: 4px;
background-color: $white; background-color: $white;
@ -21,7 +20,7 @@
line-height: 22px; line-height: 22px;
border: none; border: none;
outline: 0 !important; outline: 0 !important;
max-width: 100%; max-width: 22ch;
} }
&__primary-currency { &__primary-currency {
@ -47,14 +46,22 @@
&__input-wrapper { &__input-wrapper {
position: relative; position: relative;
display: flex; display: flex;
flex: 1;
max-width: 100%;
input[type="number"] {
-moz-appearance: textfield;
}
input[type="number"]::-webkit-inner-spin-button { input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none;
display: none; display: none;
} }
input[type="number"]:hover::-webkit-inner-spin-button { input[type="number"]:hover::-webkit-inner-spin-button {
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none;
display: none; display: none;
} }
} }
@ -67,11 +74,13 @@
.react-numeric-input { .react-numeric-input {
input[type="number"]::-webkit-inner-spin-button { input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none;
display: none; display: none;
} }
input[type="number"]:hover::-webkit-inner-spin-button { input[type="number"]:hover::-webkit-inner-spin-button {
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none;
display: none; display: none;
} }
} }

@ -27,25 +27,37 @@
@media screen and (max-width: $break-small) { @media screen and (max-width: $break-small) {
flex-direction: column; flex-direction: column;
flex: 0 0 auto; flex: 0 0 auto;
max-width: 100%;
} }
@media screen and (min-width: $break-large) { @media screen and (min-width: $break-large) {
flex-direction: row; flex-direction: row;
flex-grow: 3; flex-grow: 3;
min-width: 0;
} }
} }
.balance-display { .balance-display {
.token-amount { .token-amount {
color: $black; color: $black;
max-width: 100%;
.token-balance {
display: flex;
}
} }
@media screen and (max-width: $break-small) { @media screen and (max-width: $break-small) {
max-width: 100%;
text-align: center; text-align: center;
.token-amount { .token-amount {
font-size: 1.75rem; font-size: 1.75rem;
margin-top: 1rem; margin-top: 1rem;
.token-balance {
flex-direction: column;
}
} }
.fiat-amount { .fiat-amount {
@ -56,9 +68,10 @@
} }
@media screen and (min-width: $break-large) { @media screen and (min-width: $break-large) {
margin-left: .8em; margin: 0 .8em;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start; align-items: flex-start;
min-width: 0;
.token-amount { .token-amount {
font-size: 1.5rem; font-size: 1.5rem;

@ -642,10 +642,31 @@
display: flex; display: flex;
flex-flow: column nowrap; flex-flow: column nowrap;
flex: 1; flex: 1;
align-items: center;
@media screen and (max-width: 575px) { @media screen and (max-width: 575px) {
height: 0; height: 0;
} }
.shapeshift-form-wrapper {
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
flex: 1 0 auto;
.shapeshift-form, .modal-shapeshift-form {
border-radius: 8px;
background-color: rgba(0, 0, 0, .05);
padding: 17px 15px;
margin-bottom: 10px;
&__caret {
width: auto;
flex: 1;
}
}
}
} }
&__logo { &__logo {
@ -773,17 +794,15 @@
margin-top: 28px; margin-top: 28px;
flex: 1 0 auto; flex: 1 0 auto;
.shapeshift-form { .shapeshift-form, .modal-shapeshift-form {
width: auto; border-radius: 8px;
background-color: rgba(0, 0, 0, .05);
padding: 17px 15px;
&__caret { &__caret {
width: auto; width: auto;
flex: 1; flex: 1;
} }
@media screen and (max-width: 575px) {
width: auto;
}
} }
} }

@ -26,14 +26,16 @@ $wallet-view-bg: $alabaster;
//Account and transaction details //Account and transaction details
.account-and-transaction-details { .account-and-transaction-details {
display: flex; display: flex;
flex: 1 0 auto; flex: 1 1 auto;
min-width: 0;
} }
// tx view // tx view
.tx-view { .tx-view {
flex: 63.5 0 66.5%; flex: 1 1 66.5%;
background: $tx-view-bg; background: $tx-view-bg;
min-width: 0;
// No title on mobile // No title on mobile
@media screen and (max-width: 575px) { @media screen and (max-width: 575px) {
@ -286,7 +288,7 @@ $wallet-view-bg: $alabaster;
} }
.token-balance__amount { .token-balance__amount {
padding-right: 6px; padding: 0 6px;
} }
// first time // first time

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

@ -62,6 +62,7 @@ function reduceApp (state, action) {
warning: null, warning: null,
buyView: {}, buyView: {},
isMouseUser: false, isMouseUser: false,
gasIsLoading: false,
}, state.appState) }, state.appState)
switch (action.type) { switch (action.type) {
@ -675,6 +676,16 @@ function reduceApp (state, action) {
isMouseUser: action.value, isMouseUser: action.value,
}) })
case actions.GAS_LOADING_STARTED:
return extend(appState, {
gasIsLoading: true,
})
case actions.GAS_LOADING_FINISHED:
return extend(appState, {
gasIsLoading: false,
})
default: default:
return appState return appState
} }

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

@ -16,6 +16,7 @@ const selectors = {
transactionsSelector, transactionsSelector,
accountsWithSendEtherInfoSelector, accountsWithSendEtherInfoSelector,
getCurrentAccountWithSendEtherInfo, getCurrentAccountWithSendEtherInfo,
getGasIsLoading,
getGasPrice, getGasPrice,
getGasLimit, getGasLimit,
getForceGasMin, getForceGasMin,
@ -117,6 +118,10 @@ function transactionsSelector (state) {
.sort((a, b) => b.time - a.time) .sort((a, b) => b.time - a.time)
} }
function getGasIsLoading (state) {
return state.appState.gasIsLoading
}
function getGasPrice (state) { function getGasPrice (state) {
return state.metamask.send.gasPrice return state.metamask.send.gasPrice
} }

@ -36,6 +36,7 @@ module.exports = {
miniAddressSummary: miniAddressSummary, miniAddressSummary: miniAddressSummary,
isAllOneCase: isAllOneCase, isAllOneCase: isAllOneCase,
isValidAddress: isValidAddress, isValidAddress: isValidAddress,
isValidENSAddress,
numericBalance: numericBalance, numericBalance: numericBalance,
parseBalance: parseBalance, parseBalance: parseBalance,
formatBalance: formatBalance, formatBalance: formatBalance,
@ -87,6 +88,10 @@ function isValidAddress (address) {
return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed) return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed)
} }
function isValidENSAddress (address) {
return address.match(/^.{7,}\.(eth|test)$/)
}
function isInvalidChecksumAddress (address) { function isInvalidChecksumAddress (address) {
var prefixed = ethUtil.addHexPrefix(address) var prefixed = ethUtil.addHexPrefix(address)
if (address === '0x0000000000000000000000000000000000000000') return false if (address === '0x0000000000000000000000000000000000000000') return false

Loading…
Cancel
Save