fix merge conflicts

feature/default_network_editable
brunobar79 6 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. 15
      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
- Fix bug where account reset did not work with custom RPC providers
- Stop reloading browser page on Ethereum network change
## 4.8.0 Thur Jun 14 2018
- [#4513](https://github.com/MetaMask/metamask-extension/pull/4513): Attempting to import an empty private key will now show a clear error.
- [#4570](https://github.com/MetaMask/metamask-extension/pull/4570): Fix bug where metamask data would stop being written to disk after prolonged use.
- [#4523](https://github.com/MetaMask/metamask-extension/pull/4523): Fix bug where account reset did not work with custom RPC providers.
- [#4524](https://github.com/MetaMask/metamask-extension/pull/4524): Fix for Brave i18n getAcceptLanguages.
- [#4557](https://github.com/MetaMask/metamask-extension/pull/4557): Fix bug where nonce mutex was never released.
- [#4566](https://github.com/MetaMask/metamask-extension/pull/4566): Add phishing notice.
- [#4591](https://github.com/MetaMask/metamask-extension/pull/4591): Allow Copying Token Addresses and link to Token on Etherscan.
## 4.7.4 Tue Jun 05 2018

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

@ -152,6 +152,9 @@
"copy": {
"message": "Copy"
},
"copyContractAddress": {
"message": "Copy Contract Address"
},
"copyToClipboard": {
"message": "Copy to clipboard"
},
@ -268,6 +271,9 @@
"encryptNewDen": {
"message": "Encrypt your new DEN"
},
"ensNameNotFound": {
"message": "ENS name not found"
},
"enterPassword": {
"message": "Enter password"
},
@ -964,6 +970,9 @@
"viewAccount": {
"message": "View Account"
},
"viewOnEtherscan": {
"message": "View on Etherscan"
},
"visitWebSite": {
"message": "Visit our web site"
},

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

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

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

@ -1,4 +1,4 @@
const Config = require('./recipient-blacklist-config.json')
const Config = require('./recipient-blacklist.js')
/** @module*/
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')
const log = require('loglevel')
const LocalMessageDuplexStream = require('post-message-stream')
const setupDappAutoReload = require('./lib/auto-reload.js')
const MetamaskInpageProvider = require('./lib/inpage-provider.js')
restoreContextAfterImports()
@ -38,7 +39,11 @@ web3.setProvider = function () {
}
log.debug('MetaMask - injected web3')
setupDappAutoReload(web3, inpageProvider.publicConfigStore)
// export global web3, with usage-detection and deprecation warning
/* TODO: Uncomment this area once auto-reload.js has been deprecated:
let hasBeenWarned = false
global.web3 = new Proxy(web3, {
get: (_web3, key) => {
@ -55,6 +60,7 @@ global.web3 = new Proxy(web3, {
_web3[key] = value
},
})
*/
// set web3 defaultAccount
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 allLocales = require('../../_locales/index.json')
const isSupported = extension.i18n && extension.i18n.getAcceptLanguages
const getPreferredLocales = isSupported ? promisify(
const getPreferredLocales = extension.i18n ? promisify(
extension.i18n.getAcceptLanguages,
{ errorFirst: false }
) : async () => []
@ -18,7 +17,21 @@ const existingLocaleCodes = allLocales.map(locale => locale.code.toLowerCase().r
*
*/
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
.map(code => code.toLowerCase())
.find(code => existingLocaleCodes.includes(code))
@ -26,3 +39,4 @@ async function getFirstPreferredLangCode () {
}
module.exports = getFirstPreferredLangCode

@ -32,6 +32,8 @@ class NotificationManager {
type: 'popup',
width,
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
* @param {array} windows An array of objects containing data about the open MetaMask extension windows.
@ -93,7 +95,7 @@ class NotificationManager {
_getPopupIn (windows) {
return windows ? windows.find((win) => {
// Returns notification popup
return (win && win.type === 'popup')
return (win && win.type === 'popup' && win.id === this._popupId)
}) : null
}

@ -46,7 +46,6 @@ const GWEI_BN = new BN('1000000000')
const percentile = require('percentile')
const seedPhraseVerifier = require('./lib/seed-phrase-verifier')
const cleanErrorStack = require('./lib/cleanErrorStack')
const DiagnosticsReporter = require('./lib/diagnostics-reporter')
const log = require('loglevel')
const TrezorKeyring = require('eth-trezor-keyring')
@ -66,12 +65,6 @@ module.exports = class MetamaskController extends EventEmitter {
const initState = opts.initState || {}
this.recordFirstTimeInfo(initState)
// metamask diagnostics reporter
this.diagnostics = opts.diagnostics || new DiagnosticsReporter({
firstTimeInfo: initState.firstTimeInfo,
version,
})
// platform-specific api
this.platform = opts.platform
@ -93,7 +86,6 @@ module.exports = class MetamaskController extends EventEmitter {
this.preferencesController = new PreferencesController({
initState: initState.PreferencesController,
initLangCode: opts.initLangCode,
diagnostics: this.diagnostics,
})
// currency controller
@ -192,9 +184,6 @@ module.exports = class MetamaskController extends EventEmitter {
version,
firstVersion: initState.firstTimeInfo.version,
})
this.noticeController.updateNoticesList()
// to be uncommented when retrieving notices from a remote server.
// this.noticeController.startPolling()
this.shapeshiftController = new ShapeShiftController({
initState: initState.ShapeShiftController,

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

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

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

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

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

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

@ -34,9 +34,13 @@ TypedMessageRenderer.prototype.render = function () {
function renderTypedData (values) {
return values.map(function (value) {
let v = value.value
if (typeof v === 'boolean') {
v = v.toString()
}
return h('div', {}, [
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",
"announce": "node development/announcer.js",
"version:bump": "node development/run-version-bump.js",
"generateNotice": "node notices/notice-generator.js",
"deleteNotice": "node notices/notice-delete.js",
"storybook": "start-storybook -p 6006 -c .storybook"
},
"browserify": {
@ -186,6 +184,7 @@
"semaphore": "^1.0.5",
"semver": "^5.4.1",
"shallow-copy": "0.0.1",
"superstatic": "^5.0.2",
"sw-controller": "^1.0.3",
"sw-stream": "^2.0.2",
"textarea-caret": "^3.0.1",
@ -254,6 +253,7 @@
"gulp-util": "^3.0.7",
"gulp-watch": "^5.0.0",
"gulp-zip": "^4.0.0",
"http-server": "^0.11.1",
"image-size": "^0.6.2",
"isomorphic-fetch": "^2.2.1",
"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 testAddress = '0xE18035BF8712672935FDB4e5e431b1a0183d2DFC'
const testPrivateKey2 = '14abe6f4aab7f9f626fe981c864d0adeb5685f289ac9270c27b8fd790b4235d6'
const regularDelayMs = 1000
const largeDelayMs = regularDelayMs * 2
const waitingNewPageDelayMs = regularDelayMs * 10
@ -134,27 +135,39 @@ describe('Using MetaMask with an existing account', function () {
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 () => {
const [nextScreen] = await findElements(driver, By.css('.tou button'))
// privacy notice
const nextScreen = await findElement(driver, By.css('.tou button'))
await nextScreen.click()
await delay(regularDelayMs)
})
const canClickThrough = await driver.findElement(By.css('.tou button')).isEnabled()
assert.equal(canClickThrough, false, 'disabled continue button')
const element = await findElement(driver, By.linkText('Attributions'))
await driver.executeScript('arguments[0].scrollIntoView(true)', element)
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 acceptTos = await findElement(driver, By.xpath(`//button[contains(text(), 'Accept')]`))
await acceptTos.click()
const nextScreen = await findElement(driver, By.css('.tou button'))
await nextScreen.click()
await delay(regularDelayMs)
})
})
describe('Show account information', () => {
it('shows the correct account address', async () => {
const detailsButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Details')]`))
detailsButton.click()
await driver.findElement(By.css('.wallet-view__details-button')).click()
await driver.findElement(By.css('.qr-wrapper')).isDisplayed()
await delay(regularDelayMs)
@ -250,8 +263,10 @@ describe('Using MetaMask with an existing account', function () {
await configureGas.click()
await delay(regularDelayMs)
const gasModal = await driver.findElement(By.css('span .modal'))
const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`))
await save.click()
await driver.wait(until.stalenessOf(gasModal))
await delay(regularDelayMs)
// Continue to next screen
@ -276,142 +291,36 @@ describe('Using MetaMask with an existing account', function () {
})
})
describe('Send ETH from Faucet', () => {
it('starts a send transaction inside Faucet', async () => {
await driver.executeScript('window.open("https://faucet.metamask.io")')
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()
describe('Imports an account with private key', () => {
it('choose Create Account from the account menu', async () => {
await driver.findElement(By.css('.account-menu__icon')).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 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')
const [importAccount] = await findElements(driver, By.xpath(`//div[contains(text(), 'Import Account')]`))
await importAccount.click()
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()
it('enter private key', async () => {
const privateKeyInput = await findElement(driver, By.css('#private-key-box'))
await privateKeyInput.sendKeys(testPrivateKey2)
await delay(regularDelayMs)
await driver.switchTo().window(tokenFactory)
await delay(regularDelayMs)
const tokenContactAddress = await driver.findElement(By.css('div > div > div:nth-child(2) > span:nth-child(3)'))
tokenAddress = await tokenContactAddress.getText()
await driver.close()
await driver.switchTo().window(extension)
await loadExtension(driver, extensionId)
const importButtons = await findElements(driver, By.xpath(`//button[contains(text(), 'Import')]`))
await importButtons[0].click()
await delay(regularDelayMs)
})
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 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()
it('should show the correct account name', async () => {
const [accountName] = await findElements(driver, By.css('.account-name'))
assert.equal(await accountName.getText(), 'Account 3')
await delay(regularDelayMs)
})
it('renders the balance for the new token', async () => {
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
await driver.wait(until.elementTextIs(balance, '100TST'))
const tokenAmount = await balance.getText()
assert.equal(tokenAmount, '100TST')
it('should show the imported label', async () => {
const [importedLabel] = await findElements(driver, By.css('.wallet-view__keyring-label'))
assert.equal(await importedLabel.getText(), 'IMPORTED')
await delay(regularDelayMs)
})
})
})

@ -2,6 +2,7 @@ const fs = require('fs')
const mkdirp = require('mkdirp')
const pify = require('pify')
const {until} = require('selenium-webdriver')
const { delay } = require('../func')
module.exports = {
checkBrowserForConsoleErrors,
@ -9,6 +10,7 @@ module.exports = {
verboseReportOnFailure,
findElement,
findElements,
openNewPage,
}
async function loadExtension (driver, extensionId) {
@ -64,3 +66,15 @@ async function findElement (driver, by, timeout = 10000) {
async function findElements (driver, by, timeout = 10000) {
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,
loadExtension,
verboseReportOnFailure,
openNewPage,
} = require('./helpers')
describe('MetaMask', function () {
@ -62,7 +63,7 @@ describe('MetaMask', function () {
}
}
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 () {
let networkSelector
it('switches to first tab', async function () {
const [firstTab] = await driver.getAllWindowHandles()
await driver.switchTo().window(firstTab)
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 () {
const networkSelector = await findElement(driver, By.css('#network_component'))
await networkSelector.click()
await delay(regularDelayMs)
@ -128,27 +135,43 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
})
it('clicks through the privacy notice', async () => {
const nextScreen = await findElement(driver, By.css('.tou button'))
await nextScreen.click()
await delay(regularDelayMs)
it('clicks through the ToS', async () => {
// terms of use
const canClickThrough = await driver.findElement(By.css('.tou button')).isEnabled()
assert.equal(canClickThrough, false, 'disabled continue button')
const bottomOfTos = await findElement(driver, By.linkText('Attributions'))
await driver.executeScript('arguments[0].scrollIntoView(true)', bottomOfTos)
await delay(regularDelayMs)
const acceptTos = await findElement(driver, By.css('.tou button'))
driver.wait(until.elementIsEnabled(acceptTos))
await acceptTos.click()
await delay(regularDelayMs)
})
it('clicks through the privacy notice', async () => {
// privacy notice
const nextScreen = await findElement(driver, By.css('.tou button'))
await nextScreen.click()
await delay(regularDelayMs)
})
it('clicks through the phishing notice', async () => {
// phishing notice
const noticeElement = await driver.findElement(By.css('.markdown'))
await driver.executeScript('arguments[0].scrollTop = arguments[0].scrollHeight', noticeElement)
await delay(regularDelayMs)
const nextScreen = await findElement(driver, By.css('.tou button'))
await nextScreen.click()
await delay(regularDelayMs)
})
let seedPhrase
it('reveals the seed phrase', async () => {
const revealSeedPhrase = await findElement(driver, By.css('.backup-phrase__secret-blocker'))
await revealSeedPhrase.click()
const byRevealButton = By.css('.backup-phrase__secret-blocker .backup-phrase__reveal-button')
await driver.wait(until.elementLocated(byRevealButton, 10000))
const revealSeedPhraseButton = await findElement(driver, byRevealButton, 10000)
await revealSeedPhraseButton.click()
await delay(regularDelayMs)
seedPhrase = await driver.findElement(By.css('.backup-phrase__secret-words')).getText()
@ -160,56 +183,76 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
})
it('can retype the seed phrase', async () => {
const words = seedPhrase.split(' ')
async function retypeSeedPhrase (words) {
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 delay(tinyDelayMs)
await word0.click()
await delay(tinyDelayMs)
const word1 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[1]}')]`))
await word1.click()
await delay(tinyDelayMs)
const word1 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[1]}')]`), 10000)
const word2 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[2]}')]`))
await word2.click()
await delay(tinyDelayMs)
await word1.click()
await delay(tinyDelayMs)
const word3 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[3]}')]`))
await word3.click()
await delay(tinyDelayMs)
const word2 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[2]}')]`), 10000)
const word4 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[4]}')]`))
await word4.click()
await delay(tinyDelayMs)
await word2.click()
await delay(tinyDelayMs)
const word5 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[5]}')]`))
await word5.click()
await delay(tinyDelayMs)
const word3 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[3]}')]`), 10000)
const word6 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[6]}')]`))
await word6.click()
await delay(tinyDelayMs)
await word3.click()
await delay(tinyDelayMs)
const word7 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[7]}')]`))
await word7.click()
await delay(tinyDelayMs)
const word4 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[4]}')]`), 10000)
const word8 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[8]}')]`))
await word8.click()
await delay(tinyDelayMs)
await word4.click()
await delay(tinyDelayMs)
const word9 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[9]}')]`))
await word9.click()
await delay(tinyDelayMs)
const word5 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[5]}')]`), 10000)
const word10 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[10]}')]`))
await word10.click()
await delay(tinyDelayMs)
await word5.click()
await delay(tinyDelayMs)
const word11 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[11]}')]`))
await word11.click()
await delay(tinyDelayMs)
const word6 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[6]}')]`), 10000)
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')]`))
await confirm.click()
@ -217,7 +260,8 @@ describe('MetaMask', function () {
})
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'))
await closeModal.click()
await driver.wait(until.stalenessOf(buyModal))
@ -231,8 +275,12 @@ describe('MetaMask', function () {
await driver.findElement(By.css('.qr-wrapper')).isDisplayed()
await delay(regularDelayMs)
let accountModal = await driver.findElement(By.css('span .modal'))
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 delay(regularDelayMs)
const gasModal = await driver.findElement(By.css('span .modal'))
const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`))
await save.click()
await driver.wait(until.stalenessOf(gasModal))
await delay(regularDelayMs)
// Continue to next screen
@ -352,19 +403,20 @@ describe('MetaMask', 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)
assert.equal(await txValues[0].getText(), '1 ETH')
const txValues = await findElement(driver, By.css('.tx-list-value'))
await driver.wait(until.elementTextMatches(txValues, /1\sETH/), 10000)
})
})
describe('Send ETH from Faucet', () => {
it('starts a send transaction inside Faucet', async () => {
await driver.executeScript('window.open("https://faucet.metamask.io")')
await delay(waitingNewPageDelayMs)
await openNewPage(driver, 'https://faucet.metamask.io')
const [extension, faucet] = await driver.getAllWindowHandles()
await driver.switchTo().window(faucet)
const faucetPageTitle = await findElement(driver, By.css('.container-fluid'))
await driver.wait(until.elementTextMatches(faucetPageTitle, /MetaMask/))
await delay(regularDelayMs)
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', () => {
it('clicks on the Add Token button', async () => {
const addToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Token')]`))
await addToken.click()
describe('Deploy contract and call contract methods', () => {
let extension
let contractTestPage
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)
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 () => {
const tokenSearch = await findElement(driver, By.css('#search-tokens'))
await tokenSearch.sendKeys('BAT')
it('calls and confirms a contract method where ETH is sent', async () => {
await driver.switchTo().window(contractTestPage)
await delay(regularDelayMs)
const token = await findElement(driver, By.xpath("//span[contains(text(), 'BAT')]"))
await token.click()
const depositButton = await findElement(driver, By.css('#depositButton'))
await depositButton.click()
await delay(regularDelayMs)
const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), 'Next')]`))
await nextScreen.click()
await driver.switchTo().window(extension)
await delay(regularDelayMs)
const addTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Tokens')]`))
await addTokens.click()
await delay(largeDelayMs)
const txListItem = await findElement(driver, By.css('.tx-list-item'))
await txListItem.click()
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'))
await driver.wait(until.elementTextIs(balance, '0BAT'))
await driver.wait(until.elementTextMatches(balance, /^86.*ETH.*$/), 10000)
const tokenAmount = await balance.getText()
assert.equal(tokenAmount, '0BAT')
assert.ok(/^86.*ETH.*$/.test(tokenAmount))
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)
openNewPage(driver, 'https://tokenfactory.surge.sh/#/factory')
await delay(regularDelayMs * 10)
const [extension, tokenFactory] = await driver.getAllWindowHandles()
await driver.switchTo().window(tokenFactory)
const [
totalSupply,
tokenName,
@ -457,12 +595,16 @@ describe('MetaMask', function () {
await driver.switchTo().window(tokenFactory)
await delay(regularDelayMs)
const tokenContactAddress = await driver.findElement(By.css('div > div > div:nth-child(2) > span:nth-child(3)'))
tokenAddress = await tokenContactAddress.getText()
await driver.close()
await driver.switchTo().window(extension)
await loadExtension(driver, extensionId)
await driver.switchTo().window(extension)
await delay(regularDelayMs)
})
it('clicks on the Add Token button', async () => {
@ -491,9 +633,217 @@ describe('MetaMask', function () {
it('renders the balance for the new token', async () => {
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()
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)
})
})

@ -6,5 +6,5 @@ set -o pipefail
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 && mocha test/e2e/beta/from-import-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 -- -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 pify = require('pify')
const webdriver = require('selenium-webdriver')
const { By, Key } = webdriver
const { By, Key, until } = webdriver
const { delay, buildChromeWebDriver, buildFirefoxWebdriver, installWebExt, getExtensionIdChrome, getExtensionIdFirefox } = require('./func')
describe('Metamask popup page', function () {
@ -71,13 +71,6 @@ describe('Metamask popup page', function () {
it('matches MetaMask title', async () => {
const title = await driver.getTitle()
assert.equal(title, 'MetaMask', 'title matches MetaMask')
})
it('shows privacy notice', async () => {
await delay(300)
const privacy = await driver.findElement(By.css('.terms-header')).getText()
assert.equal(privacy, 'PRIVACY NOTICE', 'shows privacy notice')
await driver.findElement(By.css('button')).click()
await delay(300)
})
@ -100,6 +93,24 @@ describe('Metamask popup page', function () {
await button.click()
})
it('shows privacy notice', async () => {
const privacy = await driver.findElement(By.css('.terms-header')).getText()
assert.equal(privacy, 'PRIVACY NOTICE', 'shows privacy notice')
await driver.findElement(By.css('button')).click()
await delay(300)
})
it('shows phishing notice', async () => {
await delay(300)
const noticeHeader = await driver.findElement(By.css('.terms-header')).getText()
assert.equal(noticeHeader, 'PHISHING WARNING', 'shows phishing warning')
const element = await driver.findElement(By.css('.markdown'))
await driver.executeScript('arguments[0].scrollTop = arguments[0].scrollHeight', element)
await delay(300)
await driver.findElement(By.css('button')).click()
await delay(300)
})
it('accepts password with length of eight', async () => {
const passwordBox = await driver.findElement(By.id('password-box'))
const passwordBoxConfirm = await driver.findElement(By.id('password-box-confirm'))
@ -218,7 +229,11 @@ describe('Metamask popup page', function () {
it('confirms transaction', async function () {
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)
})
@ -258,7 +273,8 @@ describe('Metamask popup page', function () {
it('confirms transaction in MetaMask popup', async function () {
const windowHandles = await driver.getAllWindowHandles()
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 delay(1000)
})

@ -117,12 +117,12 @@ async function runSendFlowTest(assert, done) {
const sendGasField = await queryAsync($, '.send-v2__gas-fee-display')
assert.equal(
sendGasField.find('.currency-display__input-wrapper > input').val(),
'0.000198264',
'0.000021',
'send gas field should show estimated gas total'
)
assert.equal(
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'
)

@ -1,31 +1,59 @@
const assert = require('assert')
const path = require('path')
const accountImporter = require('../../../app/scripts/account-import-strategies/index')
const ethUtil = require('ethereumjs-util')
const accountImporter = require('../../../app/scripts/account-import-strategies/index')
const { assertRejects } = require('../test-utils')
describe('Account Import Strategies', function () {
const privkey = '0x4cfd3e90fc78b0f86bf7524722150bb8da9c60cd532564d7ff43f5716514f553'
const json = '{"version":3,"id":"dbb54385-0a99-437f-83c0-647de9f244c3","address":"a7f92ce3fba24196cf6f4bd2e1eb3db282ba998c","Crypto":{"ciphertext":"bde13d9ade5c82df80281ca363320ce254a8a3a06535bbf6ffdeaf0726b1312c","cipherparams":{"iv":"fbf93718a57f26051b292f072f2e5b41"},"cipher":"aes-128-ctr","kdf":"scrypt","kdfparams":{"dklen":32,"salt":"7ffe00488319dec48e4c49a120ca49c6afbde9272854c64d9541c83fc6acdffe","n":8192,"r":8,"p":1},"mac":"2adfd9c4bc1cdac4c85bddfb31d9e21a684e0e050247a70c5698facf6b7d4681"}}'
it('imports a private key and strips 0x prefix', async function () {
const importPrivKey = await accountImporter.importAccount('Private Key', [ privkey ])
assert.equal(importPrivKey, ethUtil.stripHexPrefix(privkey))
})
describe('private key import', function () {
it('imports a private key and strips 0x prefix', async function () {
const importPrivKey = await accountImporter.importAccount('Private Key', [ privkey ])
assert.equal(importPrivKey, ethUtil.stripHexPrefix(privkey))
})
it('fails when password is incorrect for keystore', async function () {
const wrongPassword = 'password2'
it('throws an error for empty string private key', async () => {
assertRejects(async function() {
await accountImporter.importAccount('Private Key', [ '' ])
}, Error, 'no empty strings')
})
try {
await accountImporter.importAccount('JSON File', [ json, wrongPassword])
} catch (error) {
assert.equal(error.message, 'Key derivation failed - possibly wrong passphrase')
}
})
it('throws an error for undefined string private key', async () => {
assertRejects(async function () {
await accountImporter.importAccount('Private Key', [ undefined ])
})
})
it('imports json string and password to return a private key', async function () {
const fileContentsPassword = 'password1'
const importJson = await accountImporter.importAccount('JSON File', [ json, fileContentsPassword])
assert.equal(importJson, '0x5733876abe94146069ce8bcbabbde2677f2e35fa33e875e92041ed2ac87e5bc7')
it('throws an error for undefined string private key', async () => {
assertRejects(async function () {
await accountImporter.importAccount('Private Key', [])
})
})
it('throws an error for invalid private key', async () => {
assertRejects(async function () {
await accountImporter.importAccount('Private Key', [ 'popcorn' ])
})
})
})
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('#getNoticesList', function () {
it('should return an empty array when new', function (done) {
// const testList = [{
// id: 0,
// read: false,
// title: 'Futuristic Notice',
// }]
var result = noticeController.getNoticesList()
assert.equal(result.length, 0)
done()
})
})
describe('#setNoticesList', function () {
it('should set data appropriately', function (done) {
@ -41,36 +29,6 @@ describe('notice-controller', function () {
})
})
describe('#updateNoticeslist', function () {
it('should integrate the latest changes from the source', function (done) {
var testList = [{
id: 55,
read: false,
title: 'Futuristic Notice',
}]
noticeController.setNoticesList(testList)
noticeController.updateNoticesList().then(() => {
var newList = noticeController.getNoticesList()
assert.ok(newList[0].id === 55)
assert.ok(newList[1])
done()
})
})
it('should not overwrite any existing fields', function (done) {
var testList = [{
id: 0,
read: false,
title: 'Futuristic Notice',
}]
noticeController.setNoticesList(testList)
var newList = noticeController.getNoticesList()
assert.equal(newList[0].id, 0)
assert.equal(newList[0].title, 'Futuristic Notice')
assert.equal(newList.length, 1)
done()
})
})
describe('#markNoticeRead', function () {
it('should mark a notice as read', function (done) {
var testList = [{
@ -86,7 +44,7 @@ describe('notice-controller', function () {
})
})
describe('#getLatestUnreadNotice', function () {
describe('#getNextUnreadNotice', function () {
it('should retrieve the latest unread notice', function (done) {
var testList = [
{id: 0, read: true, title: 'Past Notice'},
@ -94,8 +52,8 @@ describe('notice-controller', function () {
{id: 2, read: false, title: 'Future Notice'},
]
noticeController.setNoticesList(testList)
var latestUnread = noticeController.getLatestUnreadNotice()
assert.equal(latestUnread.id, 2)
var latestUnread = noticeController.getNextUnreadNotice()
assert.equal(latestUnread.id, 1)
done()
})
it('should return undefined if no unread notices exist.', function (done) {
@ -105,7 +63,7 @@ describe('notice-controller', function () {
{id: 2, read: true, title: 'Future Notice'},
]
noticeController.setNoticesList(testList)
var latestUnread = noticeController.getLatestUnreadNotice()
var latestUnread = noticeController.getNextUnreadNotice()
assert.ok(!latestUnread)
done()
})

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

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

@ -33,6 +33,7 @@ const {
const {
getGasPrice,
getGasLimit,
getGasIsLoading,
getForceGasMin,
conversionRateSelector,
getSendAmount,
@ -51,6 +52,7 @@ function mapStateToProps (state) {
return {
gasPrice: getGasPrice(state),
gasLimit: getGasLimit(state),
gasIsLoading: getGasIsLoading(state),
forceGasMin: getForceGasMin(state),
conversionRate,
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 gasLimit = props.gasLimit || MIN_GAS_LIMIT_DEC
@ -97,7 +99,11 @@ inherits(CustomizeGasModal, Component)
function CustomizeGasModal (props) {
Component.call(this)
this.state = getOriginalState(props)
const originalState = getFreshState(props)
this.state = {
...originalState,
originalState,
}
}
CustomizeGasModal.contextTypes = {
@ -106,6 +112,36 @@ CustomizeGasModal.contextTypes = {
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) {
const {
@ -137,7 +173,7 @@ CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) {
}
CustomizeGasModal.prototype.revert = function () {
this.setState(getOriginalState(this.props))
this.setState(this.state.originalState)
}
CustomizeGasModal.prototype.validate = function ({ gasTotal, gasLimit }) {
@ -233,7 +269,7 @@ CustomizeGasModal.prototype.convertAndSetGasPrice = function (newGasPrice) {
}
CustomizeGasModal.prototype.render = function () {
const { hideModal, forceGasMin } = this.props
const { hideModal, forceGasMin, gasIsLoading } = this.props
const { gasPrice, gasLimit, gasTotal, error, priceSigZeros, priceSigDec } = this.state
let convertedGasPrice = conversionUtil(gasPrice, {
@ -266,7 +302,7 @@ CustomizeGasModal.prototype.render = function () {
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__header', {}, [
@ -288,6 +324,7 @@ CustomizeGasModal.prototype.render = function () {
onChange: value => this.convertAndSetGasPrice(value),
title: this.context.t('gasPrice'),
copy: this.context.t('gasPriceCalculation'),
gasIsLoading,
}),
h(GasModalCard, {
@ -297,6 +334,7 @@ CustomizeGasModal.prototype.render = function () {
onChange: value => this.convertAndSetGasLimit(value),
title: this.context.t('gasLimit'),
copy: this.context.t('gasLimitCalculation'),
gasIsLoading,
}),
]),

@ -4,14 +4,21 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
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 = {
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) {
return {
@ -37,22 +44,34 @@ TokenMenuDropdown.prototype.onClose = function (e) {
TokenMenuDropdown.prototype.render = function () {
const { showHideTokenConfirmationModal } = this.props
return h('div.token-menu-dropdown', {}, [
h('div.token-menu-dropdown__close-area', {
return h(Menu, { className: 'token-menu-dropdown', isShowing: true }, [
h(CloseArea, {
onClick: this.onClose,
}),
h('div.token-menu-dropdown__container', {}, [
h('div.token-menu-dropdown__options', {}, [
h('div.token-menu-dropdown__option', {
onClick: (e) => {
e.stopPropagation()
showHideTokenConfirmationModal(this.props.token)
this.props.onClose()
},
}, this.context.t('hideToken')),
]),
]),
h(Item, {
onClick: (e) => {
e.stopPropagation()
showHideTokenConfirmationModal(this.props.token)
this.props.onClose()
},
text: this.context.t('hideToken'),
}),
h(Item, {
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 ToAutoComplete = require('./send/to-autocomplete')
const log = require('loglevel')
const { isValidENSAddress } = require('../util')
EnsInput.contextTypes = {
t: PropTypes.func,
@ -25,31 +26,34 @@ function EnsInput () {
Component.call(this)
}
EnsInput.prototype.render = function () {
const props = this.props
const opts = extend(props, {
list: 'addresses',
onChange: (recipient) => {
const network = this.props.network
const networkHasEnsSupport = getNetworkEnsSupport(network)
EnsInput.prototype.onChange = function (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) {
return this.setState({
loadingEns: false,
ensResolution: null,
ensFailure: null,
})
}
if (recipient.match(ensRE) === null) {
return this.setState({
loadingEns: false,
ensResolution: null,
ensFailure: null,
toError: null,
})
}
this.setState({
loadingEns: true,
})
this.checkName(recipient)
},
this.setState({
loadingEns: true,
})
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', {
style: { width: '100%', position: 'relative' },
@ -85,17 +89,27 @@ EnsInput.prototype.lookupEnsName = function (recipient) {
nickname: recipient.trim(),
hoverText: address + '\n' + this.context.t('clickCopy'),
ensFailure: false,
toError: null,
})
}
})
.catch((reason) => {
log.error(reason)
return this.setState({
const setStateObj = {
loadingEns: false,
ensResolution: ZERO_ADDRESS,
ensResolution: recipient,
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
// the user's own accounts, a default of a one-space string is used.
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 &&
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) {
const { loadingEns, ensFailure, ensResolution } = this.state || { ensResolution: ZERO_ADDRESS}
const { loadingEns, ensFailure, ensResolution, toError } = this.state || { ensResolution: ZERO_ADDRESS }
if (toError) return
if (loadingEns) {
return h('img', {

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

@ -22,12 +22,16 @@ function isValidInput (text) {
return re.test(text)
}
function removeLeadingZeroes (str) {
return str.replace(/^0*(?=\d)/, '')
}
InputNumber.prototype.setValue = function (newValue) {
newValue = removeLeadingZeroes(newValue)
if (newValue && !isValidInput(newValue)) return
const { fixed, min = -1, max = Infinity, onChange } = this.props
newValue = fixed ? newValue.toFixed(4) : newValue
const newValueGreaterThanMin = conversionGTE(
{ value: newValue || '0', fromNumericBase: 'dec' },
{ value: min, fromNumericBase: 'hex' },
@ -47,7 +51,7 @@ InputNumber.prototype.setValue = function (newValue) {
}
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', {}, [
h('input', {
@ -63,11 +67,11 @@ InputNumber.prototype.render = function () {
h('span.gas-tooltip-input-detail', {}, [unitLabel]),
h('div.gas-tooltip-input-arrows', {}, [
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', {
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) {
// log.debug('rendering notice screen for unread notices.')
// return h(NoticeScreen, {
// notice: props.lastUnreadNotice,
// notice: props.nextUnreadNotice,
// key: 'NoticeScreen',
// onConfirm: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)),
// onConfirm: () => props.dispatch(actions.markNoticeRead(props.nextUnreadNotice)),
// })
// } else if (props.lostAccounts && props.lostAccounts.length > 0) {
// log.debug('rendering notice screen for lost accounts view.')
@ -279,7 +279,7 @@ function mapStateToProps (state) {
noActiveNotices,
seedWords,
unapprovedTxs,
lastUnreadNotice,
nextUnreadNotice,
lostAccounts,
unapprovedMsgCount,
unapprovedPersonalMsgCount,
@ -313,7 +313,7 @@ function mapStateToProps (state) {
network: state.metamask.network,
provider: state.metamask.provider,
forgottenPassword: state.appState.forgottenPassword,
lastUnreadNotice,
nextUnreadNotice,
lostAccounts,
frequentRpcList: state.metamask.frequentRpcList || [],
currentCurrency: state.metamask.currentCurrency,

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

@ -20,7 +20,7 @@ const {
calcGasTotal,
isBalanceSufficient,
} = 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 NetworkDisplay = require('../network-display')
const currencyFormatter = require('currency-formatter')

@ -11,7 +11,7 @@ abiDecoder.addABI(tokenAbi)
const actions = require('../../actions')
const clone = require('clone')
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 ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN

@ -57,6 +57,7 @@ CurrencyDisplay.prototype.getValueToRender = function ({ selectedToken, conversi
return selectedToken
? conversionUtil(ethUtil.addHexPrefix(value), {
fromNumericBase: 'hex',
toNumericBase: 'dec',
toCurrency: symbol,
conversionRate: multiplier,
invertConversionRate: true,
@ -91,8 +92,12 @@ CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValu
: convertedValue
}
function removeLeadingZeroes (str) {
return str.replace(/^0*(?=\d)/, '')
}
CurrencyDisplay.prototype.handleChange = function (newVal) {
this.setState({ valueToRender: newVal })
this.setState({ valueToRender: removeLeadingZeroes(newVal) })
this.props.onChange(this.getAmount(newVal))
}
@ -113,6 +118,7 @@ CurrencyDisplay.prototype.render = function () {
readOnly = false,
inError = false,
onBlur,
step,
} = this.props
const { valueToRender } = this.state
@ -147,6 +153,7 @@ CurrencyDisplay.prototype.render = function () {
width: this.getInputWidth(valueToRender, readOnly),
},
min: 0,
step,
}),
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 {
getConversionRate,
getConvertedCurrency,
getCurrentCurrency,
} from '../send.selectors.js'
import AccountListItem from './account-list-item.component'
@ -10,6 +10,6 @@ export default connect(mapStateToProps)(AccountListItem)
function mapStateToProps (state) {
return {
conversionRate: getConversionRate(state),
currentCurrency: getConvertedCurrency(state),
currentCurrency: getCurrentCurrency(state),
}
}

@ -12,7 +12,7 @@ proxyquire('../account-list-item.container.js', {
},
'../send.selectors.js': {
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,
updateSendAmount: PropTypes.func,
updateSendAmountError: PropTypes.func,
updateGas: PropTypes.func,
}
validateAmount (amount) {
@ -56,6 +57,14 @@ export default class SendAmountRow extends Component {
updateSendAmount(amount)
}
updateGas (amount) {
const { selectedToken, updateGas } = this.props
if (selectedToken) {
updateGas({ amount })
}
}
render () {
const {
amount,
@ -77,12 +86,16 @@ export default class SendAmountRow extends Component {
<CurrencyDisplay
conversionRate={amountConversionRate}
convertedCurrency={convertedCurrency}
onBlur={newAmount => this.updateAmount(newAmount)}
onBlur={newAmount => {
this.updateGas(newAmount)
this.updateAmount(newAmount)
}}
onChange={newAmount => this.validateAmount(newAmount)}
inError={inError}
primaryCurrency={primaryCurrency || 'ETH'}
selectedToken={selectedToken}
value={amount || '0x0'}
value={amount}
step="any"
/>
</SendRowWrapper>
)

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

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

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

@ -18,7 +18,7 @@ export default class SendContent extends Component {
<div className="send-v2__form">
<SendFromRow />
<SendToRow updateGas={(updateData) => this.props.updateGas(updateData)} />
<SendAmountRow />
<SendAmountRow updateGas={(updateData) => this.props.updateGas(updateData)} />
<SendGasRow />
</div>
</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 PropTypes from 'prop-types'
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 {

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

@ -5,7 +5,7 @@ import sinon from 'sinon'
import SendGasRow from '../send-gas-row.component.js'
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 = {
showCustomizeGasModal: sinon.spy(),

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

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

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

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

@ -40,6 +40,12 @@ describe('send-to-row utils', () => {
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 {
getAmountErrorObject,
getToAddressForGasUpdate,
doesAmountErrorRequireUpdate,
} from './send.utils'
@ -38,7 +39,7 @@ export default class SendTransactionScreen extends PersistentForm {
updateSendTokenBalance: PropTypes.func,
};
updateGas ({ to } = {}) {
updateGas ({ to: updatedToAddress, amount: value } = {}) {
const {
amount,
blockGasLimit,
@ -48,6 +49,7 @@ export default class SendTransactionScreen extends PersistentForm {
recentBlocks,
selectedAddress,
selectedToken = {},
to: currentToAddress,
updateAndSetGasTotal,
} = this.props
@ -59,8 +61,8 @@ export default class SendTransactionScreen extends PersistentForm {
recentBlocks,
selectedAddress,
selectedToken,
to: to && to.toLowerCase(),
value: amount,
to: getToAddressForGasUpdate(updatedToAddress, currentToAddress),
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 BASE_TOKEN_GAS_COST = '0x186a0' // Hex for 100000, a base estimate for token transfers.
module.exports = {
INSUFFICIENT_FUNDS_ERROR,
@ -52,4 +53,5 @@ module.exports = {
REQUIRED_ERROR,
SIMPLE_GAS_COST,
TOKEN_TRANSFER_FUNCTION_SIGNATURE,
BASE_TOKEN_GAS_COST,
}

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

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

@ -4,11 +4,13 @@ const {
conversionGTE,
multiplyCurrencies,
conversionGreaterThan,
conversionLessThan,
} = require('../../conversion-util')
const {
calcTokenAmount,
} = require('../../token-util')
const {
BASE_TOKEN_GAS_COST,
INSUFFICIENT_FUNDS_ERROR,
INSUFFICIENT_TOKENS_ERROR,
NEGATIVE_ETH_ERROR,
@ -20,6 +22,7 @@ const abi = require('ethereumjs-abi')
const ethUtil = require('ethereumjs-util')
module.exports = {
addGasBuffer,
calcGasTotal,
calcTokenBalance,
doesAmountErrorRequireUpdate,
@ -27,6 +30,7 @@ module.exports = {
estimateGasPriceFromRecentBlocks,
generateTokenTransferData,
getAmountErrorObject,
getToAddressForGasUpdate,
isBalanceSufficient,
isTokenBalanceSufficient,
}
@ -175,12 +179,13 @@ async function estimateGas ({ selectedAddress, selectedToken, blockGasLimit, to,
}
// if recipient has no code, gas is 21k max:
const hasRecipient = Boolean(to)
if (hasRecipient && !selectedToken) {
const code = await global.eth.getCode(to)
if (!selectedToken) {
const code = Boolean(to) && await global.eth.getCode(to)
if (!code || code === '0x') {
return SIMPLE_GAS_COST
}
} else if (selectedToken && !to) {
return BASE_TOKEN_GAS_COST
}
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')
)
if (simulationFailed) {
return resolve(paramsForGasEstimate.gas)
const estimateWithBuffer = addGasBuffer(paramsForGasEstimate.gas, blockGasLimit, 1.5)
return resolve(ethUtil.addHexPrefix(estimateWithBuffer))
} else {
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 }) {
if (!selectedToken) return
return TOKEN_TRANSFER_FUNCTION_SIGNATURE + Array.prototype.map.call(
@ -237,3 +272,7 @@ function estimateGasPriceFromRecentBlocks (recentBlocks) {
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', () => {
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()
wrapper.instance().updateGas()
assert.equal(propsMethodSpies.updateAndSetGasTotal.callCount, 1)
@ -215,12 +215,22 @@ describe('Send Component', function () {
recentBlocks: ['mockBlock'],
selectedAddress: 'mockSelectedAddress',
selectedToken: 'mockSelectedToken',
to: undefined,
to: '',
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', () => {
propsMethodSpies.updateAndSetGasTotal.resetHistory()
wrapper.instance().updateGas({ to: '0xABC' })

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

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

@ -2,6 +2,7 @@ import assert from 'assert'
import sinon from 'sinon'
import proxyquire from 'proxyquire'
import {
BASE_TOKEN_GAS_COST,
ONE_GWEI_IN_WEI_HEX,
SIMPLE_GAS_COST,
} from '../send.constants'
@ -18,10 +19,12 @@ const {
const stubs = {
addCurrencies: sinon.stub().callsFake((a, b, obj) => a + b),
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}`),
calcTokenAmount: sinon.stub().callsFake((a, d) => 'calc:' + a + d),
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', {
@ -30,6 +33,8 @@ const sendUtils = proxyquire('../send.utils.js', {
conversionUtil: stubs.conversionUtil,
conversionGTE: stubs.conversionGTE,
multiplyCurrencies: stubs.multiplyCurrencies,
conversionGreaterThan: stubs.conversionGreaterThan,
conversionLessThan: stubs.conversionLessThan,
},
'../../token-util': { calcTokenAmount: stubs.calcTokenAmount },
'ethereumjs-abi': {
@ -44,6 +49,7 @@ const {
estimateGasPriceFromRecentBlocks,
generateTokenTransferData,
getAmountErrorObject,
getToAddressForGasUpdate,
calcTokenBalance,
isBalanceSufficient,
isTokenBalanceSufficient,
@ -255,7 +261,7 @@ describe('send utils', () => {
estimateGasMethod: sinon.stub().callsFake(
(data, cb) => cb(
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 () => {
const result = await estimateGas(baseMockParams)
const result = await sendUtils.estimateGas(baseMockParams)
assert.equal(baseMockParams.estimateGasMethod.callCount, 1)
assert.deepEqual(
baseMockParams.estimateGasMethod.getCall(0).args[0],
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 () => {
@ -300,7 +316,7 @@ describe('send utils', () => {
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 () => {
@ -309,12 +325,23 @@ describe('send utils', () => {
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 () => {
assert.equal(baseMockParams.estimateGasMethod.callCount, 0)
const result = await estimateGas(Object.assign({}, baseMockParams, { to: '0x123', selectedToken: { address: '' } }))
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 () => {
const result = await estimateGas(Object.assign({}, baseMockParams, {
to: 'isContract willFailBecauseOf:Transaction execution error.',
@ -401,4 +428,15 @@ describe('send utils', () => {
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', [
showQrCode
? this.renderQrCode()
: h('div.shapeshift-form', [
: h('div.modal-shapeshift-form', [
h('div.shapeshift-form__selectors', [
h('div.shapeshift-form__selector', [

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

@ -34,7 +34,7 @@ TokenBalance.prototype.render = function () {
return isLoading
? h('span', '')
: 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),
])
}

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

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

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

@ -642,10 +642,31 @@
display: flex;
flex-flow: column nowrap;
flex: 1;
align-items: center;
@media screen and (max-width: 575px) {
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 {
@ -773,17 +794,15 @@
margin-top: 28px;
flex: 1 0 auto;
.shapeshift-form {
width: auto;
.shapeshift-form, .modal-shapeshift-form {
border-radius: 8px;
background-color: rgba(0, 0, 0, .05);
padding: 17px 15px;
&__caret {
width: auto;
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 {
display: flex;
flex: 1 0 auto;
flex: 1 1 auto;
min-width: 0;
}
// tx view
.tx-view {
flex: 63.5 0 66.5%;
flex: 1 1 66.5%;
background: $tx-view-bg;
min-width: 0;
// No title on mobile
@media screen and (max-width: 575px) {
@ -286,7 +288,7 @@ $wallet-view-bg: $alabaster;
}
.token-balance__amount {
padding-right: 6px;
padding: 0 6px;
}
// first time

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

@ -81,13 +81,9 @@ $wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and (
}
.token-menu-dropdown {
height: 55px;
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;
top: 60px;
top: 52px;
right: 25px;
z-index: 2000;

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

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

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

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

Loading…
Cancel
Save