Merge pull request #4603 from MetaMask/v4.8.0

V4.8.0
feature/default_network_editable
kumavis 6 years ago committed by GitHub
commit 4598554fea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 75
      .circleci/config.yml
  2. 6
      .circleci/scripts/firefox-download.sh
  3. 8
      .circleci/scripts/firefox-install.sh
  4. 1
      .eslintrc
  5. 11
      CHANGELOG.md
  6. 13
      app/_locales/en/messages.json
  7. 43
      app/_locales/ja/messages.json
  8. 2
      app/manifest.json
  9. 13
      app/scripts/account-import-strategies/index.js
  10. 14
      app/scripts/background.js
  11. 1
      app/scripts/contentscript.js
  12. 11
      app/scripts/controllers/network/network.js
  13. 1
      app/scripts/controllers/preferences.js
  14. 15
      app/scripts/controllers/transactions/index.js
  15. 24
      app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js
  16. 17
      app/scripts/controllers/transactions/lib/recipient-blacklist.js
  17. 54
      app/scripts/controllers/transactions/nonce-tracker.js
  18. 4
      app/scripts/controllers/transactions/pending-tx-tracker.js
  19. 23
      app/scripts/inpage.js
  20. 24
      app/scripts/lib/createStreamSink.js
  21. 20
      app/scripts/lib/get-first-preferred-lang-code.js
  22. 50
      app/scripts/metamask-controller.js
  23. 33
      app/scripts/notice-controller.js
  24. 6
      development/states/add-token.json
  25. 2
      development/states/conf-tx.json
  26. 7
      development/states/confirm-new-ui.json
  27. 6
      development/states/confirm-sig-requests.json
  28. 6
      development/states/currency-localization.json
  29. 2
      development/states/first-time.json
  30. 2
      development/states/notice.json
  31. 13
      development/states/send-edit.json
  32. 13
      development/states/send-new-ui.json
  33. 13
      development/states/tx-list-items.json
  34. 234
      gentests.js
  35. 14
      mascara/src/app/first-time/notice-screen.js
  36. 6
      notices/archive/notice_4.md
  37. 27
      notices/notice-delete.js
  38. 33
      notices/notice-generator.js
  39. 1
      notices/notice-nonce.json
  40. 34
      notices/notices.js
  41. 1
      notices/notices.json
  42. 6
      old-ui/app/app.js
  43. 5
      old-ui/app/components/account-dropdowns.js
  44. 13018
      package-lock.json
  45. 6
      package.json
  46. 136
      test/e2e/beta/from-import-beta-ui.spec.js
  47. 11
      test/e2e/beta/helpers.js
  48. 168
      test/e2e/beta/metamask-beta-ui.spec.js
  49. 2
      test/e2e/func.js
  50. 25
      test/e2e/metamask.spec.js
  51. 10
      test/integration/lib/send-new-ui.js
  52. 62
      test/unit/app/account-import-strategies.spec.js
  53. 3
      test/unit/app/controllers/metamask-controller-test.js
  54. 50
      test/unit/app/controllers/notice-controller-test.js
  55. 77
      test/unit/app/controllers/transactions/recipient-blacklist-checker-test.js
  56. 17
      test/unit/app/controllers/transactions/tx-controller-test.js
  57. 17
      test/unit/test-utils.js
  58. 143
      ui/app/actions.js
  59. 15
      ui/app/app.js
  60. 7
      ui/app/components/account-menu/index.js
  61. 113
      ui/app/components/currency-input.js
  62. 28
      ui/app/components/customize-gas-modal/index.js
  63. 2
      ui/app/components/dropdowns/account-dropdown-mini.js
  64. 53
      ui/app/components/dropdowns/token-menu-dropdown.js
  65. 8
      ui/app/components/input-number.js
  66. 1
      ui/app/components/page-container/index.js
  67. 18
      ui/app/components/page-container/page-container-content.component.js
  68. 1
      ui/app/components/page-container/page-container-footer/index.js
  69. 54
      ui/app/components/page-container/page-container-footer/page-container-footer.component.js
  70. 0
      ui/app/components/page-container/page-container-footer/tests/page-container-footer.component.test.js
  71. 35
      ui/app/components/page-container/page-container-header.component.js
  72. 1
      ui/app/components/page-container/page-container-header/index.js
  73. 57
      ui/app/components/page-container/page-container-header/page-container-header.component.js
  74. 0
      ui/app/components/page-container/page-container-header/tests/page-container-header.component.test.js
  75. 72
      ui/app/components/page-container/page-container.component.js
  76. 0
      ui/app/components/page-container/tests/page-container.component.test.js
  77. 6
      ui/app/components/pages/add-token/add-token.component.js
  78. 24
      ui/app/components/pages/create-account/import-account/json.js
  79. 25
      ui/app/components/pages/create-account/import-account/private-key.js
  80. 15
      ui/app/components/pages/create-account/index.js
  81. 8
      ui/app/components/pages/home.js
  82. 12
      ui/app/components/pages/notice.js
  83. 9
      ui/app/components/pages/settings/index.js
  84. 2
      ui/app/components/pages/unlock-page/unlock-page.component.js
  85. 2
      ui/app/components/pending-tx/confirm-deploy-contract.js
  86. 24
      ui/app/components/pending-tx/confirm-send-ether.js
  87. 22
      ui/app/components/pending-tx/confirm-send-token.js
  88. 74
      ui/app/components/send/account-list-item.js
  89. 70
      ui/app/components/send/currency-display.js
  90. 72
      ui/app/components/send/from-dropdown.js
  91. 106
      ui/app/components/send/gas-tooltip.js
  92. 33
      ui/app/components/send/memo-textarea.js
  93. 78
      ui/app/components/send/send-utils.js
  94. 89
      ui/app/components/send/send-v2-container.js
  95. 2
      ui/app/components/send/to-autocomplete.js
  96. 0
      ui/app/components/send_/README.md
  97. 0
      ui/app/components/send_/account-list-item/account-list-item-README.md
  98. 74
      ui/app/components/send_/account-list-item/account-list-item.component.js
  99. 15
      ui/app/components/send_/account-list-item/account-list-item.container.js
  100. 0
      ui/app/components/send_/account-list-item/account-list-item.scss
  101. Some files were not shown because too many files have changed in this diff Show More

@ -30,6 +30,15 @@ workflows:
- prep-deps-npm - prep-deps-npm
- prep-deps-firefox - prep-deps-firefox
- prep-build - prep-build
- test-e2e-beta-chrome:
requires:
- prep-deps-npm
- prep-build
- test-e2e-beta-firefox:
requires:
- prep-deps-npm
- prep-deps-firefox
- prep-build
- test-unit: - test-unit:
requires: requires:
- prep-deps-npm - prep-deps-npm
@ -57,6 +66,8 @@ workflows:
- test-unit - test-unit
- test-e2e-chrome - test-e2e-chrome
- test-e2e-firefox - test-e2e-firefox
- test-e2e-beta-chrome
- test-e2e-beta-firefox
- test-integration-mascara-chrome - test-integration-mascara-chrome
- test-integration-mascara-firefox - test-integration-mascara-firefox
- test-integration-flat-chrome - test-integration-flat-chrome
@ -110,9 +121,7 @@ jobs:
- checkout - checkout
- run: - run:
name: Download Firefox name: Download Firefox
command: > command: ./.circleci/scripts/firefox-download.sh
wget https://ftp.mozilla.org/pub/firefox/releases/58.0/linux-x86_64/en-US/firefox-58.0.tar.bz2
&& tar xjf firefox-58.0.tar.bz2
- save_cache: - save_cache:
key: dependency-cache-firefox-{{ .Revision }} key: dependency-cache-firefox-{{ .Revision }}
paths: paths:
@ -203,15 +212,13 @@ jobs:
- restore_cache: - restore_cache:
key: build-cache-{{ .Revision }} key: build-cache-{{ .Revision }}
- run: - run:
name: Test name: test:e2e:chrome
command: npm run test:e2e:chrome command: npm run test:e2e:chrome
- store_artifacts: - store_artifacts:
path: test-artifacts path: test-artifacts
destination: test-artifacts destination: test-artifacts
test-e2e-firefox: test-e2e-firefox:
environment:
browsers: '["Firefox"]'
docker: docker:
- image: circleci/node:8-browsers - image: circleci/node:8-browsers
steps: steps:
@ -220,11 +227,7 @@ jobs:
key: dependency-cache-firefox-{{ .Revision }} key: dependency-cache-firefox-{{ .Revision }}
- run: - run:
name: Install firefox name: Install firefox
command: > command: ./.circleci/scripts/firefox-install.sh
sudo rm -r /opt/firefox
&& sudo mv firefox /opt/firefox58
&& sudo mv /usr/bin/firefox /usr/bin/firefox-old
&& sudo ln -s /opt/firefox58/firefox /usr/bin/firefox
- restore_cache: - restore_cache:
key: dependency-cache-{{ .Revision }} key: dependency-cache-{{ .Revision }}
- restore_cache: - restore_cache:
@ -236,6 +239,43 @@ jobs:
path: test-artifacts path: test-artifacts
destination: test-artifacts destination: test-artifacts
test-e2e-beta-chrome:
docker:
- image: circleci/node:8-browsers
steps:
- checkout
- restore_cache:
key: dependency-cache-{{ .Revision }}
- restore_cache:
key: build-cache-{{ .Revision }}
- run:
name: test:e2e:chrome:beta
command: npm run test:e2e:chrome:beta
- store_artifacts:
path: test-artifacts
destination: test-artifacts
test-e2e-beta-firefox:
docker:
- image: circleci/node:8-browsers
steps:
- checkout
- restore_cache:
key: dependency-cache-firefox-{{ .Revision }}
- run:
name: Install firefox
command: ./.circleci/scripts/firefox-install.sh
- restore_cache:
key: dependency-cache-{{ .Revision }}
- restore_cache:
key: build-cache-{{ .Revision }}
- run:
name: test:e2e:firefox:beta
command: npm run test:e2e:firefox:beta
- store_artifacts:
path: test-artifacts
destination: test-artifacts
job-screens: job-screens:
docker: docker:
- image: circleci/node:8-browsers - image: circleci/node:8-browsers
@ -325,11 +365,7 @@ jobs:
key: dependency-cache-firefox-{{ .Revision }} key: dependency-cache-firefox-{{ .Revision }}
- run: - run:
name: Install firefox name: Install firefox
command: > command: ./.circleci/scripts/firefox-install.sh
sudo rm -r /opt/firefox
&& sudo mv firefox /opt/firefox58
&& sudo mv /usr/bin/firefox /usr/bin/firefox-old
&& sudo ln -s /opt/firefox58/firefox /usr/bin/firefox
- restore_cache: - restore_cache:
key: dependency-cache-{{ .Revision }} key: dependency-cache-{{ .Revision }}
- run: - run:
@ -372,11 +408,7 @@ jobs:
key: dependency-cache-firefox-{{ .Revision }} key: dependency-cache-firefox-{{ .Revision }}
- run: - run:
name: Install firefox name: Install firefox
command: > command: ./.circleci/scripts/firefox-install.sh
sudo rm -r /opt/firefox
&& sudo mv firefox /opt/firefox58
&& sudo mv /usr/bin/firefox /usr/bin/firefox-old
&& sudo ln -s /opt/firefox58/firefox /usr/bin/firefox
- restore_cache: - restore_cache:
key: dependency-cache-{{ .Revision }} key: dependency-cache-{{ .Revision }}
- run: - run:
@ -415,4 +447,3 @@ jobs:
- run: - run:
name: All Tests Passed name: All Tests Passed
command: echo 'weew - everything passed!' command: echo 'weew - everything passed!'

@ -0,0 +1,6 @@
#!/usr/bin/env bash
echo "Downloading firefox..."
wget https://ftp.mozilla.org/pub/firefox/releases/58.0/linux-x86_64/en-US/firefox-58.0.tar.bz2 \
&& tar xjf firefox-58.0.tar.bz2
echo "firefox download complete"

@ -0,0 +1,8 @@
#!/usr/bin/env bash
echo "Installing firefox..."
sudo rm -r /opt/firefox
sudo mv firefox /opt/firefox58
sudo mv /usr/bin/firefox /usr/bin/firefox-old
sudo ln -s /opt/firefox58/firefox /usr/bin/firefox
echo "Firefox installed."

@ -142,6 +142,7 @@
"operator-linebreak": [1, "after", { "overrides": { "?": "ignore", ":": "ignore" } }], "operator-linebreak": [1, "after", { "overrides": { "?": "ignore", ":": "ignore" } }],
"padded-blocks": "off", "padded-blocks": "off",
"quotes": [2, "single", {"avoidEscape": true, "allowTemplateLiterals": true}], "quotes": [2, "single", {"avoidEscape": true, "allowTemplateLiterals": true}],
"react/no-deprecated": 0,
"semi": [2, "never"], "semi": [2, "never"],
"semi-spacing": [2, { "before": false, "after": true }], "semi-spacing": [2, { "before": false, "after": true }],
"space-before-blocks": [1, "always"], "space-before-blocks": [1, "always"],

@ -2,6 +2,17 @@
## Current Master ## Current Master
## 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.
- [#4558](https://github.com/MetaMask/metamask-extension/pull/4558): Stop reloading browser page on Ethereum network change.
- [#4566](https://github.com/MetaMask/metamask-extension/pull/4566): Add phishing notice.
- [#4591](https://github.com/MetaMask/metamask-extension/pull/4591): Allow Copying Token Addresses and link to Token on Etherscan.
## 4.7.4 Tue Jun 05 2018 ## 4.7.4 Tue Jun 05 2018
- Add diagnostic reporting for users with multiple HD keyrings - Add diagnostic reporting for users with multiple HD keyrings

@ -146,6 +146,9 @@
"copy": { "copy": {
"message": "Copy" "message": "Copy"
}, },
"copyContractAddress": {
"message": "Copy Contract Address"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Copy to clipboard" "message": "Copy to clipboard"
}, },
@ -253,6 +256,9 @@
"editAccountName": { "editAccountName": {
"message": "Edit Account Name" "message": "Edit Account Name"
}, },
"editingTransaction": {
"message": "Make changes to your transaction"
},
"emailUs": { "emailUs": {
"message": "Email us!" "message": "Email us!"
}, },
@ -771,6 +777,10 @@
"onlySendToEtherAddress": { "onlySendToEtherAddress": {
"message": "Only send ETH to an Ethereum address." "message": "Only send ETH to an Ethereum address."
}, },
"onlySendTokensToAccountAddress": {
"message": "Only send $1 to an Ethereum account address.",
"description": "displays token symbol"
},
"searchTokens": { "searchTokens": {
"message": "Search Tokens" "message": "Search Tokens"
}, },
@ -948,6 +958,9 @@
"viewAccount": { "viewAccount": {
"message": "View Account" "message": "View Account"
}, },
"viewOnEtherscan": {
"message": "View on Etherscan"
},
"visitWebSite": { "visitWebSite": {
"message": "Visit our web site" "message": "Visit our web site"
}, },

@ -62,6 +62,9 @@
"message": " $1以上 $2以下にして下さい。", "message": " $1以上 $2以下にして下さい。",
"description": "helper for inputting hex as decimal input" "description": "helper for inputting hex as decimal input"
}, },
"blockiesIdenticon": {
"message": "Blockies Identicon を使用"
},
"borrowDharma": { "borrowDharma": {
"message": "Dharmaで借りる(ベータ版)" "message": "Dharmaで借りる(ベータ版)"
}, },
@ -95,6 +98,9 @@
"confirmTransaction": { "confirmTransaction": {
"message": "トランザクションの確認" "message": "トランザクションの確認"
}, },
"continue": {
"message": "続行"
},
"continueToCoinbase": { "continueToCoinbase": {
"message": "Coinbaseを開く" "message": "Coinbaseを開く"
}, },
@ -359,6 +365,9 @@
"likeToAddTokens": { "likeToAddTokens": {
"message": "トークンを追加しますか?" "message": "トークンを追加しますか?"
}, },
"links": {
"message": "リンク"
},
"limit": { "limit": {
"message": "リミット" "message": "リミット"
}, },
@ -371,12 +380,18 @@
"localhost": { "localhost": {
"message": "Localhost 8545" "message": "Localhost 8545"
}, },
"login": {
"message": "ログイン"
},
"logout": { "logout": {
"message": "ログアウト" "message": "ログアウト"
}, },
"loose": { "loose": {
"message": "外部秘密鍵" "message": "外部秘密鍵"
}, },
"max": {
"message": "最大"
},
"mainnet": { "mainnet": {
"message": "Ethereumメインネットワーク" "message": "Ethereumメインネットワーク"
}, },
@ -417,7 +432,7 @@
"message": "新規コントラクト" "message": "新規コントラクト"
}, },
"newPassword": { "newPassword": {
"message": "新規パスワード(最低文字)" "message": "新規パスワード(最低8文字)"
}, },
"newRecipient": { "newRecipient": {
"message": "新規受取人" "message": "新規受取人"
@ -453,6 +468,9 @@
"message": "または", "message": "または",
"description": "choice between creating or importing a new account" "description": "choice between creating or importing a new account"
}, },
"password": {
"message": "パスワード"
},
"passwordMismatch": { "passwordMismatch": {
"message": "パスワードが一致しません。", "message": "パスワードが一致しません。",
"description": "in password creation process, the two new password fields did not match" "description": "in password creation process, the two new password fields did not match"
@ -474,6 +492,9 @@
"popularTokens": { "popularTokens": {
"message": "人気のトークン" "message": "人気のトークン"
}, },
"privacyMsg": {
"message": "プライバシーポリシー"
},
"privateKey": { "privateKey": {
"message": "秘密鍵", "message": "秘密鍵",
"description": "select this type of file to use to import an account" "description": "select this type of file to use to import an account"
@ -546,6 +567,12 @@
"message": "ファイルとして保存", "message": "ファイルとして保存",
"description": "Account export process" "description": "Account export process"
}, },
"search": {
"message": "検索"
},
"searchResults": {
"message": "検索結果"
},
"selectService": { "selectService": {
"message": "サービスを選択" "message": "サービスを選択"
}, },
@ -575,7 +602,7 @@
}, },
"info": { "info": {
"message": "情報" "message": "情報"
}, },
"shapeshiftBuy": { "shapeshiftBuy": {
"message": "Shapeshiftで交換" "message": "Shapeshiftで交換"
}, },
@ -609,6 +636,9 @@
"takesTooLong": { "takesTooLong": {
"message": "送信に時間がかかりますか?" "message": "送信に時間がかかりますか?"
}, },
"terms": {
"message": "利用規約"
},
"testFaucet": { "testFaucet": {
"message": "Faucetをテスト" "message": "Faucetをテスト"
}, },
@ -619,6 +649,9 @@
"message": "ShapeShiftで $1をETHにする", "message": "ShapeShiftで $1をETHにする",
"description": "system will fill in deposit type in start of message" "description": "system will fill in deposit type in start of message"
}, },
"token": {
"message": "トークン"
},
"tokenAddress": { "tokenAddress": {
"message": "トークンアドレス" "message": "トークンアドレス"
}, },
@ -690,6 +723,12 @@
"warning": { "warning": {
"message": "警告" "message": "警告"
}, },
"welcomeBack": {
"message": "おかえりなさい!"
},
"welcomeBeta": {
"message": "MetaMask ベータ版へようこそ!"
},
"whatsThis": { "whatsThis": {
"message": "この機能について" "message": "この機能について"
}, },

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

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

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

@ -176,6 +176,7 @@ function blacklistedDomainCheck () {
'webbyawards.com', 'webbyawards.com',
'cdn.shopify.com/s/javascripts/tricorder/xtld-read-only-frame.html', 'cdn.shopify.com/s/javascripts/tricorder/xtld-read-only-frame.html',
'adyen.com', 'adyen.com',
'gravityforms.com',
] ]
var currentUrl = window.location.href var currentUrl = window.location.href
var currentRegex var currentRegex

@ -89,14 +89,21 @@ module.exports = class NetworkController extends EventEmitter {
type: 'rpc', type: 'rpc',
rpcTarget, rpcTarget,
} }
this.providerStore.updateState(providerConfig) this.providerConfig = providerConfig
this._switchNetwork(providerConfig)
} }
async setProviderType (type) { async setProviderType (type) {
assert.notEqual(type, 'rpc', `NetworkController - cannot call "setProviderType" with type 'rpc'. use "setRpcTarget"`) assert.notEqual(type, 'rpc', `NetworkController - cannot call "setProviderType" with type 'rpc'. use "setRpcTarget"`)
assert(INFURA_PROVIDER_TYPES.includes(type) || type === LOCALHOST, `NetworkController - Unknown rpc type "${type}"`) assert(INFURA_PROVIDER_TYPES.includes(type) || type === LOCALHOST, `NetworkController - Unknown rpc type "${type}"`)
const providerConfig = { type } const providerConfig = { type }
this.providerConfig = providerConfig
}
resetConnection () {
this.providerConfig = this.getProviderConfig()
}
set providerConfig (providerConfig) {
this.providerStore.updateState(providerConfig) this.providerStore.updateState(providerConfig)
this._switchNetwork(providerConfig) this._switchNetwork(providerConfig)
} }

@ -247,6 +247,7 @@ class PreferencesController {
* @return {Promise<string>} * @return {Promise<string>}
*/ */
setAccountLabel (account, label) { setAccountLabel (account, label) {
if (!account) throw new Error('setAccountLabel requires a valid address, got ' + String(account))
const address = normalizeAddress(account) const address = normalizeAddress(account)
const {identities} = this.store.getState() const {identities} = this.store.getState()
identities[address] = identities[address] || {} identities[address] = identities[address] || {}

@ -10,6 +10,7 @@ const NonceTracker = require('./nonce-tracker')
const txUtils = require('./lib/util') const txUtils = require('./lib/util')
const cleanErrorStack = require('../../lib/cleanErrorStack') const cleanErrorStack = require('../../lib/cleanErrorStack')
const log = require('loglevel') const log = require('loglevel')
const recipientBlacklistChecker = require('./lib/recipient-blacklist-checker')
/** /**
Transaction Controller is an aggregate of sub-controllers and trackers Transaction Controller is an aggregate of sub-controllers and trackers
@ -157,11 +158,14 @@ class TransactionController extends EventEmitter {
let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams }) let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams })
this.addTx(txMeta) this.addTx(txMeta)
this.emit('newUnapprovedTx', txMeta) this.emit('newUnapprovedTx', txMeta)
// add default tx params
try { try {
// check whether recipient account is blacklisted
recipientBlacklistChecker.checkAccount(txMeta.metamaskNetworkId, normalizedTxParams.to)
// add default tx params
txMeta = await this.addTxGasDefaults(txMeta) txMeta = await this.addTxGasDefaults(txMeta)
} catch (error) { } catch (error) {
console.log(error) log.warn(error)
this.txStateManager.setTxStatusFailed(txMeta.id, error) this.txStateManager.setTxStatusFailed(txMeta.id, error)
throw error throw error
} }
@ -260,7 +264,12 @@ class TransactionController extends EventEmitter {
// must set transaction to submitted/failed before releasing lock // must set transaction to submitted/failed before releasing lock
nonceLock.releaseLock() nonceLock.releaseLock()
} catch (err) { } catch (err) {
this.txStateManager.setTxStatusFailed(txId, err) // this is try-catch wrapped so that we can guarantee that the nonceLock is released
try {
this.txStateManager.setTxStatusFailed(txId, err)
} catch (err) {
log.error(err)
}
// must set transaction to submitted/failed before releasing lock // must set transaction to submitted/failed before releasing lock
if (nonceLock) nonceLock.releaseLock() if (nonceLock) nonceLock.releaseLock()
// continue with error chain // continue with error chain

@ -0,0 +1,24 @@
const Config = require('./recipient-blacklist.js')
/** @module*/
module.exports = {
checkAccount,
}
/**
* Checks if a specified account on a specified network is blacklisted.
@param networkId {number}
@param account {string}
*/
function checkAccount (networkId, account) {
const mainnetId = 1
if (networkId !== mainnetId) {
return
}
const accountToCheck = account.toLowerCase()
if (Config.blacklist.includes(accountToCheck)) {
throw new Error('Recipient is a public account')
}
}

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

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

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

@ -38,9 +38,30 @@ web3.setProvider = function () {
log.debug('MetaMask - overrode web3.setProvider') log.debug('MetaMask - overrode web3.setProvider')
} }
log.debug('MetaMask - injected web3') log.debug('MetaMask - injected web3')
// export global web3, with usage-detection
setupDappAutoReload(web3, inpageProvider.publicConfigStore) setupDappAutoReload(web3, inpageProvider.publicConfigStore)
// export global web3, with usage-detection and deprecation warning
/* TODO: Uncomment this area once auto-reload.js has been deprecated:
let hasBeenWarned = false
global.web3 = new Proxy(web3, {
get: (_web3, key) => {
// show warning once on web3 access
if (!hasBeenWarned && key !== 'currentProvider') {
console.warn('MetaMask: web3 will be deprecated in the near future in favor of the ethereumProvider \nhttps://github.com/MetaMask/faq/blob/master/detecting_metamask.md#web3-deprecation')
hasBeenWarned = true
}
// return value normally
return _web3[key]
},
set: (_web3, key, value) => {
// set value normally
_web3[key] = value
},
})
*/
// set web3 defaultAccount // set web3 defaultAccount
inpageProvider.publicConfigStore.subscribe(function (state) { inpageProvider.publicConfigStore.subscribe(function (state) {
web3.eth.defaultAccount = state.selectedAddress web3.eth.defaultAccount = state.selectedAddress

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

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

@ -46,7 +46,6 @@ const GWEI_BN = new BN('1000000000')
const percentile = require('percentile') const percentile = require('percentile')
const seedPhraseVerifier = require('./lib/seed-phrase-verifier') const seedPhraseVerifier = require('./lib/seed-phrase-verifier')
const cleanErrorStack = require('./lib/cleanErrorStack') const cleanErrorStack = require('./lib/cleanErrorStack')
const DiagnosticsReporter = require('./lib/diagnostics-reporter')
const log = require('loglevel') const log = require('loglevel')
module.exports = class MetamaskController extends EventEmitter { module.exports = class MetamaskController extends EventEmitter {
@ -65,12 +64,6 @@ module.exports = class MetamaskController extends EventEmitter {
const initState = opts.initState || {} const initState = opts.initState || {}
this.recordFirstTimeInfo(initState) this.recordFirstTimeInfo(initState)
// metamask diagnostics reporter
this.diagnostics = opts.diagnostics || new DiagnosticsReporter({
firstTimeInfo: initState.firstTimeInfo,
version,
})
// platform-specific api // platform-specific api
this.platform = opts.platform this.platform = opts.platform
@ -92,7 +85,6 @@ module.exports = class MetamaskController extends EventEmitter {
this.preferencesController = new PreferencesController({ this.preferencesController = new PreferencesController({
initState: initState.PreferencesController, initState: initState.PreferencesController,
initLangCode: opts.initLangCode, initLangCode: opts.initLangCode,
diagnostics: this.diagnostics,
}) })
// currency controller // currency controller
@ -189,9 +181,6 @@ module.exports = class MetamaskController extends EventEmitter {
version, version,
firstVersion: initState.firstTimeInfo.version, firstVersion: initState.firstTimeInfo.version,
}) })
this.noticeController.updateNoticesList()
// to be uncommented when retrieving notices from a remote server.
// this.noticeController.startPolling()
this.shapeshiftController = new ShapeShiftController({ this.shapeshiftController = new ShapeShiftController({
initState: initState.ShapeShiftController, initState: initState.ShapeShiftController,
@ -394,6 +383,8 @@ module.exports = class MetamaskController extends EventEmitter {
updateAndApproveTransaction: nodeify(txController.updateAndApproveTransaction, txController), updateAndApproveTransaction: nodeify(txController.updateAndApproveTransaction, txController),
retryTransaction: nodeify(this.retryTransaction, this), retryTransaction: nodeify(this.retryTransaction, this),
getFilteredTxList: nodeify(txController.getFilteredTxList, txController), getFilteredTxList: nodeify(txController.getFilteredTxList, txController),
isNonceTaken: nodeify(txController.isNonceTaken, txController),
estimateGas: nodeify(this.estimateGas, this),
// messageManager // messageManager
signMessage: nodeify(this.signMessage, this), signMessage: nodeify(this.signMessage, this),
@ -434,28 +425,24 @@ module.exports = class MetamaskController extends EventEmitter {
* @returns {Object} vault * @returns {Object} vault
*/ */
async createNewVaultAndKeychain (password) { async createNewVaultAndKeychain (password) {
const release = await this.createVaultMutex.acquire() const releaseLock = await this.createVaultMutex.acquire()
let vault
try { try {
let vault
const accounts = await this.keyringController.getAccounts() const accounts = await this.keyringController.getAccounts()
if (accounts.length > 0) { if (accounts.length > 0) {
vault = await this.keyringController.fullUpdate() vault = await this.keyringController.fullUpdate()
} else { } else {
vault = await this.keyringController.createNewVaultAndKeychain(password) vault = await this.keyringController.createNewVaultAndKeychain(password)
const accounts = await this.keyringController.getAccounts() const accounts = await this.keyringController.getAccounts()
this.preferencesController.setAddresses(accounts) this.preferencesController.setAddresses(accounts)
this.selectFirstIdentity() this.selectFirstIdentity()
} }
release() releaseLock()
return vault
} catch (err) { } catch (err) {
release() releaseLock()
throw err throw err
} }
return vault
} }
/** /**
@ -464,7 +451,7 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {} seed * @param {} seed
*/ */
async createNewVaultAndRestore (password, seed) { async createNewVaultAndRestore (password, seed) {
const release = await this.createVaultMutex.acquire() const releaseLock = await this.createVaultMutex.acquire()
try { try {
// clear known identities // clear known identities
this.preferencesController.setAddresses([]) this.preferencesController.setAddresses([])
@ -474,10 +461,10 @@ module.exports = class MetamaskController extends EventEmitter {
const accounts = await this.keyringController.getAccounts() const accounts = await this.keyringController.getAccounts()
this.preferencesController.setAddresses(accounts) this.preferencesController.setAddresses(accounts)
this.selectFirstIdentity() this.selectFirstIdentity()
release() releaseLock()
return vault return vault
} catch (err) { } catch (err) {
release() releaseLock()
throw err throw err
} }
} }
@ -628,10 +615,7 @@ module.exports = class MetamaskController extends EventEmitter {
async resetAccount () { async resetAccount () {
const selectedAddress = this.preferencesController.getSelectedAddress() const selectedAddress = this.preferencesController.getSelectedAddress()
this.txController.wipeTransactions(selectedAddress) this.txController.wipeTransactions(selectedAddress)
this.networkController.resetConnection()
const networkController = this.networkController
const oldType = networkController.getProviderConfig().type
await networkController.setProviderType(oldType, true)
return selectedAddress return selectedAddress
} }
@ -958,6 +942,18 @@ module.exports = class MetamaskController extends EventEmitter {
return state return state
} }
estimateGas (estimateGasParams) {
return new Promise((resolve, reject) => {
return this.txController.txGasUtil.query.estimateGas(estimateGasParams, (err, res) => {
if (err) {
return reject(err)
}
return resolve(res)
})
})
}
//============================================================================= //=============================================================================
// PASSWORD MANAGEMENT // PASSWORD MANAGEMENT
//============================================================================= //=============================================================================

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

@ -75,9 +75,9 @@
{ {
"type": "HD Key Tree", "type": "HD Key Tree",
"accounts": [ "accounts": [
"fdea65c8e26263f6d9a1b5de9555d2931a33b825", "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"c5b8dbac4c1d3f152cdeb400e2313f309c410acb", "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb",
"2f8d4a878cfa04a6e60d46362f5644deab66572d" "0x2f8d4a878cfa04a6e60d46362f5644deab66572d"
] ]
}, },
{ {

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

@ -151,5 +151,10 @@
"scrollToBottom": false, "scrollToBottom": false,
"forgottenPassword": null "forgottenPassword": null
}, },
"identities": {} "identities": {},
"send": {
"fromDropdownOpen": false,
"toDropdownOpen": false,
"errors": {}
}
} }

@ -115,9 +115,9 @@
{ {
"type": "HD Key Tree", "type": "HD Key Tree",
"accounts": [ "accounts": [
"fdea65c8e26263f6d9a1b5de9555d2931a33b825", "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"c5b8dbac4c1d3f152cdeb400e2313f309c410acb", "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb",
"2f8d4a878cfa04a6e60d46362f5644deab66572d" "0x2f8d4a878cfa04a6e60d46362f5644deab66572d"
] ]
}, },
{ {

@ -76,9 +76,9 @@
{ {
"type": "HD Key Tree", "type": "HD Key Tree",
"accounts": [ "accounts": [
"fdea65c8e26263f6d9a1b5de9555d2931a33b825", "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"c5b8dbac4c1d3f152cdeb400e2313f309c410acb", "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb",
"2f8d4a878cfa04a6e60d46362f5644deab66572d" "0x2f8d4a878cfa04a6e60d46362f5644deab66572d"
] ]
}, },
{ {

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

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

@ -94,9 +94,9 @@
{ {
"type": "HD Key Tree", "type": "HD Key Tree",
"accounts": [ "accounts": [
"fdea65c8e26263f6d9a1b5de9555d2931a33b825", "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"c5b8dbac4c1d3f152cdeb400e2313f309c410acb", "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb",
"2f8d4a878cfa04a6e60d46362f5644deab66572d" "0x2f8d4a878cfa04a6e60d46362f5644deab66572d"
] ]
}, },
{ {
@ -151,5 +151,10 @@
"scrollToBottom": false, "scrollToBottom": false,
"forgottenPassword": null "forgottenPassword": null
}, },
"identities": {} "identities": {},
"send": {
"fromDropdownOpen": false,
"toDropdownOpen": false,
"errors": {}
}
} }

@ -76,9 +76,9 @@
{ {
"type": "HD Key Tree", "type": "HD Key Tree",
"accounts": [ "accounts": [
"fdea65c8e26263f6d9a1b5de9555d2931a33b825", "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"c5b8dbac4c1d3f152cdeb400e2313f309c410acb", "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb",
"2f8d4a878cfa04a6e60d46362f5644deab66572d" "0x2f8d4a878cfa04a6e60d46362f5644deab66572d"
] ]
}, },
{ {
@ -130,5 +130,10 @@
"scrollToBottom": false, "scrollToBottom": false,
"forgottenPassword": null "forgottenPassword": null
}, },
"identities": {} "identities": {},
"send": {
"fromDropdownOpen": false,
"toDropdownOpen": false,
"errors": {}
}
} }

@ -83,9 +83,9 @@
{ {
"type": "HD Key Tree", "type": "HD Key Tree",
"accounts": [ "accounts": [
"fdea65c8e26263f6d9a1b5de9555d2931a33b825", "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"c5b8dbac4c1d3f152cdeb400e2313f309c410acb", "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb",
"2f8d4a878cfa04a6e60d46362f5644deab66572d" "0x2f8d4a878cfa04a6e60d46362f5644deab66572d"
] ]
}, },
{ {
@ -124,5 +124,10 @@
"scrollToBottom": false, "scrollToBottom": false,
"forgottenPassword": null "forgottenPassword": null
}, },
"identities": {} "identities": {},
"send": {
"fromDropdownOpen": false,
"toDropdownOpen": false,
"errors": {}
}
} }

@ -0,0 +1,234 @@
const fs = require('fs')
const async = require('async')
const path = require('path')
const promisify = require('pify')
// start(/\.selectors.js/, generateSelectorTest).catch(console.error)
// start(/\.utils.js/, generateUtilTest).catch(console.error)
startContainer(/\.container.js/, generateContainerTest).catch(console.error)
async function getAllFileNames (dirName) {
const rootPath = path.join(__dirname, dirName)
const allNames = (await promisify(fs.readdir)(dirName))
const fileNames = allNames.filter(name => name.match(/^.+\./))
const dirNames = allNames.filter(name => name.match(/^[^.]+$/))
const fullPathDirNames = dirNames.map(d => `${dirName}/${d}`)
const subNameArrays = await promisify(async.map)(fullPathDirNames, getAllFileNames)
let subNames = []
subNameArrays.forEach(subNameArray => subNames = [...subNames, ...subNameArray])
return [
...fileNames.map(name => dirName + '/' + name),
...subNames,
]
}
async function start (fileRegEx, testGenerator) {
const fileNames = await getAllFileNames('./ui/app')
const sFiles = fileNames.filter(name => name.match(fileRegEx))
let sFileMethodNames
let testFilePath
async.each(sFiles, async (sFile, cb) => {
let [, sRootPath, sPath] = sFile.match(/^(.+\/)([^/]+)$/)
sFileMethodNames = Object.keys(require(__dirname + '/' + sFile))
testFilePath = sPath.replace('.', '-').replace('.', '.test.')
await promisify(fs.writeFile)(
`${__dirname}/${sRootPath}tests/${testFilePath}`,
testGenerator(sPath, sFileMethodNames),
'utf8'
)
}, (err) => {
console.log(err)
})
}
async function startContainer (fileRegEx, testGenerator) {
const fileNames = await getAllFileNames('./ui/app')
const sFiles = fileNames.filter(name => name.match(fileRegEx))
let sFileMethodNames
async.each(sFiles, async (sFile, cb) => {
console.log(`sFile`, sFile);
let [, sRootPath, sPath] = sFile.match(/^(.+\/)([^/]+)$/)
let testFilePath = sPath.replace('.', '-').replace('.', '.test.')
await promisify(fs.readFile)(
__dirname + '/' + sFile,
'utf8',
async (err, result) => {
console.log(`result`, result.length);
const returnObjectStrings = result
.match(/return\s(\{[\s\S]+?})\n}/g)
.map(str => {
return str
.slice(0, str.length - 1)
.slice(7)
.replace(/\n/g, '')
.replace(/\s\s+/g, ' ')
})
const mapStateToPropsAssertionObject = returnObjectStrings[0]
.replace(/\w+:\s\w+\([\w,\s]+\),/g, str => {
const strKey = str.match(/^\w+/)[0]
return strKey + ': \'mock' + str.match(/^\w+/)[0].replace(/^./, c => c.toUpperCase()) + ':mockState\',\n'
})
.replace(/{\s\w.+/, firstLinePair => `{\n ${firstLinePair.slice(2)}`)
.replace(/\w+:.+,/g, s => ` ${s}`)
.replace(/}/g, s => ` ${s}`)
let mapDispatchToPropsMethodNames
if (returnObjectStrings[1]) {
mapDispatchToPropsMethodNames = returnObjectStrings[1].match(/\s\w+:\s/g).map(str => str.match(/\w+/)[0])
}
const proxyquireObject = ('{\n ' + result
.match(/import\s{[\s\S]+?}\sfrom\s.+/g)
.map(s => s.replace(/\n/g, ''))
.map((s, i) => {
const proxyKeys = s.match(/{.+}/)[0].match(/\w+/g)
return '\'' + s.match(/'(.+)'/)[1] + '\': { ' + (proxyKeys.length > 1
? '\n ' + proxyKeys.join(': () => {},\n ') + ': () => {},\n '
: proxyKeys[0] + ': () => {},') + ' }'
})
.join(',\n ') + '\n}')
.replace('{ connect: () => {}, },', `{
connect: (ms, md) => {
mapStateToProps = ms
mapDispatchToProps = md
return () => ({})
},
},`)
// console.log(`proxyquireObject`, proxyquireObject);
// console.log(`mapStateToPropsAssertionObject`, mapStateToPropsAssertionObject);
// console.log(`mapDispatchToPropsMethodNames`, mapDispatchToPropsMethodNames);
const containerTest = generateContainerTest(sPath, {
mapStateToPropsAssertionObject,
mapDispatchToPropsMethodNames,
proxyquireObject,
})
// console.log(`containerTest`, `${__dirname}/${sRootPath}tests/${testFilePath}`, containerTest);
console.log('----')
console.log(`sRootPath`, sRootPath);
console.log(`testFilePath`, testFilePath);
await promisify(fs.writeFile)(
`${__dirname}/${sRootPath}tests/${testFilePath}`,
containerTest,
'utf8'
)
}
)
}, (err) => {
console.log('123', err)
})
}
function generateMethodList (methodArray) {
return methodArray.map(n => ' ' + n).join(',\n') + ','
}
function generateMethodDescribeBlock (methodName, index) {
const describeBlock =
`${index ? ' ' : ''}describe('${methodName}()', () => {
it('should', () => {
const state = {}
assert.equal(${methodName}(state), )
})
})`
return describeBlock
}
function generateDispatchMethodDescribeBlock (methodName, index) {
const describeBlock =
`${index ? ' ' : ''}describe('${methodName}()', () => {
it('should dispatch an action', () => {
mapDispatchToPropsObject.${methodName}()
assert(dispatchSpy.calledOnce)
})
})`
return describeBlock
}
function generateMethodDescribeBlocks (methodArray) {
return methodArray
.map((methodName, index) => generateMethodDescribeBlock(methodName, index))
.join('\n\n')
}
function generateDispatchMethodDescribeBlocks (methodArray) {
return methodArray
.map((methodName, index) => generateDispatchMethodDescribeBlock(methodName, index))
.join('\n\n')
}
function generateSelectorTest (name, methodArray) {
return `import assert from 'assert'
import {
${generateMethodList(methodArray)}
} from '../${name}'
describe('${name.match(/^[^.]+/)} selectors', () => {
${generateMethodDescribeBlocks(methodArray)}
})`
}
function generateUtilTest (name, methodArray) {
return `import assert from 'assert'
import {
${generateMethodList(methodArray)}
} from '../${name}'
describe('${name.match(/^[^.]+/)} utils', () => {
${generateMethodDescribeBlocks(methodArray)}
})`
}
function generateContainerTest (sPath, {
mapStateToPropsAssertionObject,
mapDispatchToPropsMethodNames,
proxyquireObject,
}) {
return `import assert from 'assert'
import proxyquire from 'proxyquire'
import sinon from 'sinon'
let mapStateToProps
let mapDispatchToProps
proxyquire('../${sPath}', ${proxyquireObject})
describe('${sPath.match(/^[^.]+/)} container', () => {
describe('mapStateToProps()', () => {
it('should map the correct properties to props', () => {
assert.deepEqual(mapStateToProps('mockState'), ${mapStateToPropsAssertionObject})
})
})
describe('mapDispatchToProps()', () => {
let dispatchSpy
let mapDispatchToPropsObject
beforeEach(() => {
dispatchSpy = sinon.spy()
mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
})
${mapDispatchToPropsMethodNames ? generateDispatchMethodDescribeBlocks(mapDispatchToPropsMethodNames) : 'delete'}
})
})`
}

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

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

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

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

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

File diff suppressed because one or more lines are too long

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

@ -23,9 +23,10 @@ class AccountDropdowns extends Component {
renderAccounts () { renderAccounts () {
const { identities, selected, keyrings } = this.props const { identities, selected, keyrings } = this.props
const accountOrder = keyrings.reduce((list, keyring) => list.concat(keyring.accounts), [])
return Object.keys(identities).map((key, index) => { return accountOrder.map((address, index) => {
const identity = identities[key] const identity = identities[address]
const isSelected = identity.address === selected const isSelected = identity.address === selected
const simpleAddress = identity.address.substring(2).toLowerCase() const simpleAddress = identity.address.substring(2).toLowerCase()

13018
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -9,7 +9,8 @@
"dist": "gulp dist", "dist": "gulp dist",
"doc": "jsdoc -c development/tools/.jsdoc.json", "doc": "jsdoc -c development/tools/.jsdoc.json",
"test": "npm run test:unit && npm run test:integration && npm run lint", "test": "npm run test:unit && npm run test:integration && npm run lint",
"test:unit": "cross-env METAMASK_ENV=test mocha --exit --require test/setup.js --recursive \"test/unit/**/*.js\" && dot-only-hunter", "watch:test:unit": "nodemon --exec \"npm run test:unit\" ./test ./app ./ui",
"test:unit": "cross-env METAMASK_ENV=test mocha --exit --require test/setup.js --recursive \"test/unit/**/*.js\" \"ui/app/**/*.test.js\" && dot-only-hunter",
"test:single": "cross-env METAMASK_ENV=test mocha --require test/helper.js", "test:single": "cross-env METAMASK_ENV=test mocha --require test/helper.js",
"test:integration": "npm run test:integration:build && npm run test:flat && npm run test:mascara", "test:integration": "npm run test:integration:build && npm run test:flat && npm run test:mascara",
"test:integration:build": "gulp build:scss", "test:integration:build": "gulp build:scss",
@ -45,8 +46,6 @@
"disc": "gulp disc --debug", "disc": "gulp disc --debug",
"announce": "node development/announcer.js", "announce": "node development/announcer.js",
"version:bump": "node development/run-version-bump.js", "version:bump": "node development/run-version-bump.js",
"generateNotice": "node notices/notice-generator.js",
"deleteNotice": "node notices/notice-delete.js",
"storybook": "start-storybook -p 6006 -c .storybook" "storybook": "start-storybook -p 6006 -c .storybook"
}, },
"browserify": { "browserify": {
@ -276,6 +275,7 @@
"path": "^0.12.7", "path": "^0.12.7",
"png-file-stream": "^1.0.0", "png-file-stream": "^1.0.0",
"prompt": "^1.0.0", "prompt": "^1.0.0",
"proxyquire": "2.0.1",
"qs": "^6.2.0", "qs": "^6.2.0",
"qunitjs": "^2.4.1", "qunitjs": "^2.4.1",
"radgrad-jsdoc-template": "^1.1.3", "radgrad-jsdoc-template": "^1.1.3",

@ -1,7 +1,7 @@
const path = require('path') const path = require('path')
const assert = require('assert') const assert = require('assert')
const webdriver = require('selenium-webdriver') const webdriver = require('selenium-webdriver')
const { By, Key } = webdriver const { By, Key, until } = webdriver
const { const {
delay, delay,
buildChromeWebDriver, buildChromeWebDriver,
@ -14,8 +14,11 @@ const {
checkBrowserForConsoleErrors, checkBrowserForConsoleErrors,
loadExtension, loadExtension,
verboseReportOnFailure, verboseReportOnFailure,
findElement,
findElements,
} = require('./helpers') } = require('./helpers')
describe('Using MetaMask with an existing account', function () { describe('Using MetaMask with an existing account', function () {
let extensionId let extensionId
let driver let driver
@ -79,30 +82,33 @@ describe('Using MetaMask with an existing account', function () {
}) })
it('use the local network', async function () { it('use the local network', async function () {
const [networkSelector] = await driver.findElements(By.css('#network_component')) const networkSelector = await findElement(driver, By.css('#network_component'))
await networkSelector.click() await networkSelector.click()
await delay(regularDelayMs) await delay(regularDelayMs)
const [localhost] = await driver.findElements(By.xpath(`//li[contains(text(), 'Localhost')]`)) const [localhost] = await findElements(driver, By.xpath(`//li[contains(text(), 'Localhost')]`))
await localhost.click() await localhost.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
it('selects the new UI option', async () => { it('selects the new UI option', async () => {
const button = await driver.findElement(By.xpath("//p[contains(text(), 'Try Beta Version')]")) const button = await findElement(driver, By.xpath("//p[contains(text(), 'Try Beta Version')]"))
await button.click() await button.click()
await delay(regularDelayMs) await delay(regularDelayMs)
// Close all other tabs // Close all other tabs
const [oldUi, infoPage, newUi] = await driver.getAllWindowHandles() let [oldUi, infoPage, newUi] = await driver.getAllWindowHandles()
newUi = newUi || infoPage
await driver.switchTo().window(oldUi) await driver.switchTo().window(oldUi)
await driver.close() await driver.close()
await driver.switchTo().window(infoPage) if (infoPage !== newUi) {
await driver.close() await driver.switchTo().window(infoPage)
await driver.close()
}
await driver.switchTo().window(newUi) await driver.switchTo().window(newUi)
await delay(regularDelayMs) await delay(regularDelayMs)
const [continueBtn] = await driver.findElements(By.css('.welcome-screen__button')) const continueBtn = await findElement(driver, By.css('.welcome-screen__button'))
await continueBtn.click() await continueBtn.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
@ -110,48 +116,62 @@ describe('Using MetaMask with an existing account', function () {
describe('First time flow starting from an existing seed phrase', () => { describe('First time flow starting from an existing seed phrase', () => {
it('imports a seed phrase', async () => { it('imports a seed phrase', async () => {
const [seedPhrase] = await driver.findElements(By.xpath(`//a[contains(text(), 'Import with seed phrase')]`)) const [seedPhrase] = await findElements(driver, By.xpath(`//a[contains(text(), 'Import with seed phrase')]`))
await seedPhrase.click() await seedPhrase.click()
await delay(regularDelayMs) await delay(regularDelayMs)
const [seedTextArea] = await driver.findElements(By.css('textarea.import-account__secret-phrase')) const [seedTextArea] = await findElements(driver, By.css('textarea.import-account__secret-phrase'))
await seedTextArea.sendKeys(testSeedPhrase) await seedTextArea.sendKeys(testSeedPhrase)
await delay(regularDelayMs) await delay(regularDelayMs)
const [password] = await driver.findElements(By.id('password')) const [password] = await findElements(driver, By.id('password'))
await password.sendKeys('correct horse battery staple') await password.sendKeys('correct horse battery staple')
const [confirmPassword] = await driver.findElements(By.id('confirm-password')) const [confirmPassword] = await findElements(driver, By.id('confirm-password'))
confirmPassword.sendKeys('correct horse battery staple') confirmPassword.sendKeys('correct horse battery staple')
const [importButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Import')]`)) const [importButton] = await findElements(driver, By.xpath(`//button[contains(text(), 'Import')]`))
await importButton.click() await importButton.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
it('clicks through the ToS', async () => {
// terms of use
const canClickThrough = await driver.findElement(By.css('.tou button')).isEnabled()
assert.equal(canClickThrough, false, 'disabled continue button')
const bottomOfTos = await findElement(driver, By.linkText('Attributions'))
await driver.executeScript('arguments[0].scrollIntoView(true)', bottomOfTos)
await delay(regularDelayMs)
const acceptTos = await findElement(driver, By.css('.tou button'))
await acceptTos.click()
await delay(regularDelayMs)
})
it('clicks through the privacy notice', async () => { it('clicks through the privacy notice', async () => {
const [nextScreen] = await driver.findElements(By.css('.tou button')) // privacy notice
const nextScreen = await findElement(driver, By.css('.tou button'))
await nextScreen.click() await nextScreen.click()
await delay(regularDelayMs) await delay(regularDelayMs)
})
const canClickThrough = await driver.findElement(By.css('.tou button')).isEnabled() it('clicks through the phishing notice', async () => {
assert.equal(canClickThrough, false, 'disabled continue button') // phishing notice
const element = await driver.findElement(By.linkText('Attributions')) const noticeElement = await driver.findElement(By.css('.markdown'))
await driver.executeScript('arguments[0].scrollIntoView(true)', element) await driver.executeScript('arguments[0].scrollTop = arguments[0].scrollHeight', noticeElement)
await delay(regularDelayMs) await delay(regularDelayMs)
const nextScreen = await findElement(driver, By.css('.tou button'))
const [acceptTos] = await driver.findElements(By.css('.tou button')) await nextScreen.click()
await acceptTos.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
}) })
describe('Show account information', () => { describe('Show account information', () => {
it('shows the correct account address', async () => { it('shows the correct account address', async () => {
await driver.findElement(By.css('.wallet-view__details-button')).click() const detailsButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Details')]`))
detailsButton.click()
await driver.findElement(By.css('.qr-wrapper')).isDisplayed() await driver.findElement(By.css('.qr-wrapper')).isDisplayed()
await delay(regularDelayMs) await delay(regularDelayMs)
const [address] = await driver.findElements(By.css('input.qr-ellip-address')) const [address] = await findElements(driver, By.css('input.qr-ellip-address'))
assert.equal(await address.getAttribute('value'), testAddress) assert.equal(await address.getAttribute('value'), testAddress)
await driver.executeScript("document.querySelector('.account-modal-close').click()") await driver.executeScript("document.querySelector('.account-modal-close').click()")
@ -161,19 +181,22 @@ describe('Using MetaMask with an existing account', function () {
it('shows a QR code for the account', async () => { it('shows a QR code for the account', async () => {
await driver.findElement(By.css('.wallet-view__details-button')).click() await driver.findElement(By.css('.wallet-view__details-button')).click()
await driver.findElement(By.css('.qr-wrapper')).isDisplayed() await driver.findElement(By.css('.qr-wrapper')).isDisplayed()
const detailModal = await driver.findElement(By.css('span .modal'))
await delay(regularDelayMs) await delay(regularDelayMs)
await driver.executeScript("document.querySelector('.account-modal-close').click()") await driver.executeScript("document.querySelector('.account-modal-close').click()")
await driver.wait(until.stalenessOf(detailModal))
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
}) })
describe('Log out and log back in', () => { describe('Log out and log back in', () => {
it('logs out of the account', async () => { it('logs out of the account', async () => {
await driver.findElement(By.css('.account-menu__icon')).click() const accountIdenticon = driver.findElement(By.css('.account-menu__icon .identicon'))
accountIdenticon.click()
await delay(regularDelayMs) await delay(regularDelayMs)
const [logoutButton] = await driver.findElements(By.css('.account-menu__logout-button')) const [logoutButton] = await findElements(driver, By.css('.account-menu__logout-button'))
assert.equal(await logoutButton.getText(), 'Log out') assert.equal(await logoutButton.getText(), 'Log out')
await logoutButton.click() await logoutButton.click()
await delay(regularDelayMs) await delay(regularDelayMs)
@ -191,23 +214,23 @@ describe('Using MetaMask with an existing account', function () {
await driver.findElement(By.css('.account-menu__icon')).click() await driver.findElement(By.css('.account-menu__icon')).click()
await delay(regularDelayMs) await delay(regularDelayMs)
const [createAccount] = await driver.findElements(By.xpath(`//div[contains(text(), 'Create Account')]`)) const [createAccount] = await findElements(driver, By.xpath(`//div[contains(text(), 'Create Account')]`))
await createAccount.click() await createAccount.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
it('set account name', async () => { it('set account name', async () => {
const [accountName] = await driver.findElements(By.css('.new-account-create-form input')) const [accountName] = await findElements(driver, By.css('.new-account-create-form input'))
await accountName.sendKeys('2nd account') await accountName.sendKeys('2nd account')
await delay(regularDelayMs) await delay(regularDelayMs)
const [createButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Create')]`)) const [createButton] = await findElements(driver, By.xpath(`//button[contains(text(), 'Create')]`))
await createButton.click() await createButton.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
it('should show the correct account name', async () => { it('should show the correct account name', async () => {
const [accountName] = await driver.findElements(By.css('.account-name')) const [accountName] = await findElements(driver, By.css('.account-name'))
assert.equal(await accountName.getText(), '2nd account') assert.equal(await accountName.getText(), '2nd account')
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
@ -218,7 +241,7 @@ describe('Using MetaMask with an existing account', function () {
await driver.findElement(By.css('.account-menu__icon')).click() await driver.findElement(By.css('.account-menu__icon')).click()
await delay(regularDelayMs) await delay(regularDelayMs)
const [originalAccountMenuItem] = await driver.findElements(By.css('.account-menu__name')) const [originalAccountMenuItem] = await findElements(driver, By.css('.account-menu__name'))
await originalAccountMenuItem.click() await originalAccountMenuItem.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
@ -226,41 +249,41 @@ describe('Using MetaMask with an existing account', function () {
describe('Send ETH from inside MetaMask', () => { describe('Send ETH from inside MetaMask', () => {
it('starts to send a transaction', async function () { it('starts to send a transaction', async function () {
const [sendButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Send')]`)) const sendButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Send')]`))
await sendButton.click() await sendButton.click()
await delay(regularDelayMs) await delay(regularDelayMs)
const [inputAddress] = await driver.findElements(By.css('input[placeholder="Recipient Address"]')) const inputAddress = await findElement(driver, By.css('input[placeholder="Recipient Address"]'))
const [inputAmount] = await driver.findElements(By.css('.currency-display__input')) const inputAmount = await findElement(driver, By.css('.currency-display__input'))
await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970') await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
await inputAmount.sendKeys('1') await inputAmount.sendKeys('1')
// Set the gas limit // Set the gas limit
const [configureGas] = await driver.findElements(By.css('.send-v2__gas-fee-display button')) const configureGas = await findElement(driver, By.css('.send-v2__gas-fee-display button'))
await configureGas.click() await configureGas.click()
await delay(regularDelayMs) await delay(regularDelayMs)
const [save] = await driver.findElements(By.xpath(`//button[contains(text(), 'Save')]`)) const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`))
await save.click() await save.click()
await delay(regularDelayMs) await delay(regularDelayMs)
// Continue to next screen // Continue to next screen
const [nextScreen] = await driver.findElements(By.xpath(`//button[contains(text(), 'Next')]`)) const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), 'Next')]`))
await nextScreen.click() await nextScreen.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
it('confirms the transaction', async function () { it('confirms the transaction', async function () {
const [confirmButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Confirm')]`)) const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirmButton.click() await confirmButton.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
it('finds the transaction in the transactions list', async function () { it('finds the transaction in the transactions list', async function () {
const transactions = await driver.findElements(By.css('.tx-list-item')) const transactions = await findElements(driver, By.css('.tx-list-item'))
assert.equal(transactions.length, 1) assert.equal(transactions.length, 1)
const txValues = await driver.findElements(By.css('.tx-list-value')) const txValues = await findElements(driver, By.css('.tx-list-value'))
assert.equal(txValues.length, 1) assert.equal(txValues.length, 1)
assert.equal(await txValues[0].getText(), '1 ETH') assert.equal(await txValues[0].getText(), '1 ETH')
}) })
@ -275,7 +298,7 @@ describe('Using MetaMask with an existing account', function () {
await driver.switchTo().window(faucet) await driver.switchTo().window(faucet)
await delay(regularDelayMs) await delay(regularDelayMs)
const [send1eth] = await driver.findElements(By.xpath(`//button[contains(text(), '10 ether')]`)) const send1eth = await findElement(driver, By.xpath(`//button[contains(text(), '10 ether')]`), 14000)
await send1eth.click() await send1eth.click()
await delay(regularDelayMs) await delay(regularDelayMs)
@ -283,7 +306,7 @@ describe('Using MetaMask with an existing account', function () {
await loadExtension(driver, extensionId) await loadExtension(driver, extensionId)
await delay(regularDelayMs) await delay(regularDelayMs)
const [confirmButton] = await driver.findElements(By.xpath(`//button[contains(text(),'Confirm')]`)) const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`), 14000)
await confirmButton.click() await confirmButton.click()
await delay(regularDelayMs) await delay(regularDelayMs)
@ -300,31 +323,31 @@ describe('Using MetaMask with an existing account', function () {
describe('Add existing token using search', () => { describe('Add existing token using search', () => {
it('clicks on the Add Token button', async () => { it('clicks on the Add Token button', async () => {
const [addToken] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Token')]`)) const addToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Token')]`))
await addToken.click() await addToken.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
it('picks an existing token', async () => { it('picks an existing token', async () => {
const [tokenSearch] = await driver.findElements(By.css('input.add-token__input')) const tokenSearch = await findElement(driver, By.css('#search-tokens'))
await tokenSearch.sendKeys('BAT') await tokenSearch.sendKeys('BAT')
await delay(regularDelayMs) await delay(regularDelayMs)
const [token] = await driver.findElements(By.xpath("//div[contains(text(), 'BAT')]")) const token = await findElement(driver, By.xpath("//span[contains(text(), 'BAT')]"))
await token.click() await token.click()
await delay(regularDelayMs) await delay(regularDelayMs)
const [nextScreen] = await driver.findElements(By.xpath(`//button[contains(text(), 'Next')]`)) const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), 'Next')]`))
await nextScreen.click() await nextScreen.click()
await delay(regularDelayMs) await delay(regularDelayMs)
const [addTokens] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Tokens')]`)) const addTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Tokens')]`))
await addTokens.click() await addTokens.click()
await delay(largeDelayMs) await delay(largeDelayMs)
}) })
it('renders the balance for the new token', async () => { it('renders the balance for the new token', async () => {
const balance = await driver.findElement(By.css('.tx-view .balance-display .token-amount')) const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
const tokenAmount = await balance.getText() const tokenAmount = await balance.getText()
assert.equal(tokenAmount, '0BAT') assert.equal(tokenAmount, '0BAT')
await delay(regularDelayMs) await delay(regularDelayMs)
@ -343,14 +366,14 @@ describe('Using MetaMask with an existing account', function () {
tokenName, tokenName,
tokenDecimal, tokenDecimal,
tokenSymbol, tokenSymbol,
] = await driver.findElements(By.css('input')) ] = await findElements(driver, By.css('.form-control'))
await totalSupply.sendKeys('100') await totalSupply.sendKeys('100')
await tokenName.sendKeys('Test') await tokenName.sendKeys('Test')
await tokenDecimal.sendKeys('0') await tokenDecimal.sendKeys('0')
await tokenSymbol.sendKeys('TST') await tokenSymbol.sendKeys('TST')
const [createToken] = await driver.findElements(By.xpath(`//button[contains(text(), 'Create Token')]`)) const createToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Create Token')]`))
await createToken.click() await createToken.click()
await delay(regularDelayMs) await delay(regularDelayMs)
@ -358,7 +381,7 @@ describe('Using MetaMask with an existing account', function () {
await loadExtension(driver, extensionId) await loadExtension(driver, extensionId)
await delay(regularDelayMs) await delay(regularDelayMs)
const [confirmButton] = await driver.findElements(By.xpath(`//button[contains(text(),'Confirm')]`)) const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirmButton.click() await confirmButton.click()
await delay(regularDelayMs) await delay(regularDelayMs)
@ -373,31 +396,32 @@ describe('Using MetaMask with an existing account', function () {
}) })
it('clicks on the Add Token button', async () => { it('clicks on the Add Token button', async () => {
const [addToken] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Token')]`)) const addToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Token')]`))
await addToken.click() await addToken.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
it('picks the new Test token', async () => { it('picks the new Test token', async () => {
const [addCustomToken] = await driver.findElements(By.xpath("//div[contains(text(), 'Custom Token')]")) const addCustomToken = await findElement(driver, By.xpath("//div[contains(text(), 'Custom Token')]"))
await addCustomToken.click() await addCustomToken.click()
await delay(regularDelayMs) await delay(regularDelayMs)
const [newTokenAddress] = await driver.findElements(By.css('.add-token__add-custom-form input')) const newTokenAddress = await findElement(driver, By.css('#custom-address'))
await newTokenAddress.sendKeys(tokenAddress) await newTokenAddress.sendKeys(tokenAddress)
await delay(regularDelayMs) await delay(regularDelayMs)
const [nextScreen] = await driver.findElements(By.xpath(`//button[contains(text(), 'Next')]`)) const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), 'Next')]`))
await nextScreen.click() await nextScreen.click()
await delay(regularDelayMs) await delay(regularDelayMs)
const [addTokens] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Tokens')]`)) const addTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Tokens')]`))
await addTokens.click() await addTokens.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
it('renders the balance for the new token', async () => { it('renders the balance for the new token', async () => {
const [balance] = await driver.findElements(By.css('.tx-view .balance-display .token-amount')) const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
await driver.wait(until.elementTextIs(balance, '100TST'))
const tokenAmount = await balance.getText() const tokenAmount = await balance.getText()
assert.equal(tokenAmount, '100TST') assert.equal(tokenAmount, '100TST')
await delay(regularDelayMs) await delay(regularDelayMs)

@ -1,11 +1,14 @@
const fs = require('fs') const fs = require('fs')
const mkdirp = require('mkdirp') const mkdirp = require('mkdirp')
const pify = require('pify') const pify = require('pify')
const {until} = require('selenium-webdriver')
module.exports = { module.exports = {
checkBrowserForConsoleErrors, checkBrowserForConsoleErrors,
loadExtension, loadExtension,
verboseReportOnFailure, verboseReportOnFailure,
findElement,
findElements,
} }
async function loadExtension (driver, extensionId) { async function loadExtension (driver, extensionId) {
@ -53,3 +56,11 @@ async function verboseReportOnFailure (driver, test) {
const htmlSource = await driver.getPageSource() const htmlSource = await driver.getPageSource()
await pify(fs.writeFile)(`${filepathBase}-dom.html`, htmlSource) await pify(fs.writeFile)(`${filepathBase}-dom.html`, htmlSource)
} }
async function findElement (driver, by, timeout = 10000) {
return driver.wait(until.elementLocated(by), timeout)
}
async function findElements (driver, by, timeout = 10000) {
return driver.wait(until.elementsLocated(by), timeout)
}

@ -1,7 +1,7 @@
const path = require('path') const path = require('path')
const assert = require('assert') const assert = require('assert')
const webdriver = require('selenium-webdriver') const webdriver = require('selenium-webdriver')
const { By, Key } = webdriver const { By, Key, until } = webdriver
const { const {
delay, delay,
buildChromeWebDriver, buildChromeWebDriver,
@ -11,6 +11,8 @@ const {
getExtensionIdFirefox, getExtensionIdFirefox,
} = require('../func') } = require('../func')
const { const {
findElement,
findElements,
checkBrowserForConsoleErrors, checkBrowserForConsoleErrors,
loadExtension, loadExtension,
verboseReportOnFailure, verboseReportOnFailure,
@ -22,10 +24,10 @@ describe('MetaMask', function () {
let tokenAddress let tokenAddress
const testSeedPhrase = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent' const testSeedPhrase = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent'
const tinyDelayMs = 500 const tinyDelayMs = 1000
const regularDelayMs = tinyDelayMs * 2 const regularDelayMs = tinyDelayMs * 2
const largeDelayMs = regularDelayMs * 2 const largeDelayMs = regularDelayMs * 2
const waitingNewPageDelayMs = regularDelayMs * 10 const waitingNewPageDelayMs = regularDelayMs * 30
this.timeout(0) this.timeout(0)
this.bail(true) this.bail(true)
@ -76,30 +78,33 @@ describe('MetaMask', function () {
}) })
it('use the local network', async function () { it('use the local network', async function () {
const [networkSelector] = await driver.findElements(By.css('#network_component')) const networkSelector = await findElement(driver, By.css('#network_component'))
await networkSelector.click() await networkSelector.click()
await delay(regularDelayMs) await delay(regularDelayMs)
const [localhost] = await driver.findElements(By.xpath(`//li[contains(text(), 'Localhost')]`)) const localhost = await findElement(driver, By.xpath(`//li[contains(text(), 'Localhost')]`))
await localhost.click() await localhost.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
it('selects the new UI option', async () => { it('selects the new UI option', async () => {
const button = await driver.findElement(By.xpath("//p[contains(text(), 'Try Beta Version')]")) const button = await findElement(driver, By.xpath("//p[contains(text(), 'Try Beta Version')]"))
await button.click() await button.click()
await delay(regularDelayMs) await delay(regularDelayMs)
// Close all other tabs // Close all other tabs
const [oldUi, infoPage, newUi] = await driver.getAllWindowHandles() let [oldUi, infoPage, newUi] = await driver.getAllWindowHandles()
newUi = newUi || infoPage
await driver.switchTo().window(oldUi) await driver.switchTo().window(oldUi)
await driver.close() await driver.close()
await driver.switchTo().window(infoPage) if (infoPage !== newUi) {
await driver.close() await driver.switchTo().window(infoPage)
await driver.close()
}
await driver.switchTo().window(newUi) await driver.switchTo().window(newUi)
await delay(regularDelayMs) await delay(regularDelayMs)
const [continueBtn] = await driver.findElements(By.css('.welcome-screen__button')) const continueBtn = await findElement(driver, By.css('.welcome-screen__button'))
await continueBtn.click() await continueBtn.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
@ -107,9 +112,9 @@ describe('MetaMask', function () {
describe('Going through the first time flow', () => { describe('Going through the first time flow', () => {
it('accepts a secure password', async () => { it('accepts a secure password', async () => {
const [passwordBox] = await driver.findElements(By.css('.create-password #create-password')) const passwordBox = await findElement(driver, By.css('.create-password #create-password'))
const [passwordBoxConfirm] = await driver.findElements(By.css('.create-password #confirm-password')) const passwordBoxConfirm = await findElement(driver, By.css('.create-password #confirm-password'))
const [button] = await driver.findElements(By.css('.create-password button')) const button = await findElement(driver, By.css('.create-password button'))
await passwordBox.sendKeys('correct horse battery staple') await passwordBox.sendKeys('correct horse battery staple')
await passwordBoxConfirm.sendKeys('correct horse battery staple') await passwordBoxConfirm.sendKeys('correct horse battery staple')
@ -118,31 +123,44 @@ describe('MetaMask', function () {
}) })
it('clicks through the unique image screen', async () => { it('clicks through the unique image screen', async () => {
const [nextScreen] = await driver.findElements(By.css('.unique-image button')) const nextScreen = await findElement(driver, By.css('.unique-image button'))
await nextScreen.click() await nextScreen.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
it('clicks through the privacy notice', async () => { it('clicks through the ToS', async () => {
const [nextScreen] = await driver.findElements(By.css('.tou button')) // terms of use
await nextScreen.click()
await delay(regularDelayMs)
const canClickThrough = await driver.findElement(By.css('.tou button')).isEnabled() const canClickThrough = await driver.findElement(By.css('.tou button')).isEnabled()
assert.equal(canClickThrough, false, 'disabled continue button') assert.equal(canClickThrough, false, 'disabled continue button')
const [bottomOfTos] = await driver.findElements(By.linkText('Attributions')) const bottomOfTos = await findElement(driver, By.linkText('Attributions'))
await driver.executeScript('arguments[0].scrollIntoView(true)', bottomOfTos) await driver.executeScript('arguments[0].scrollIntoView(true)', bottomOfTos)
await delay(regularDelayMs) await delay(regularDelayMs)
const acceptTos = await findElement(driver, By.css('.tou button'))
const [acceptTos] = await driver.findElements(By.css('.tou button'))
await acceptTos.click() await acceptTos.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
it('clicks through the privacy notice', async () => {
// privacy notice
const nextScreen = await findElement(driver, By.css('.tou button'))
await nextScreen.click()
await delay(regularDelayMs)
})
it('clicks through the phishing notice', async () => {
// phishing notice
const noticeElement = await driver.findElement(By.css('.markdown'))
await driver.executeScript('arguments[0].scrollTop = arguments[0].scrollHeight', noticeElement)
await delay(regularDelayMs)
const nextScreen = await findElement(driver, By.css('.tou button'))
await nextScreen.click()
await delay(regularDelayMs)
})
let seedPhrase let seedPhrase
it('reveals the seed phrase', async () => { it('reveals the seed phrase', async () => {
const [revealSeedPhrase] = await driver.findElements(By.css('.backup-phrase__secret-blocker')) const revealSeedPhrase = await findElement(driver, By.css('.backup-phrase__secret-blocker'))
await revealSeedPhrase.click() await revealSeedPhrase.click()
await delay(regularDelayMs) await delay(regularDelayMs)
@ -150,7 +168,7 @@ describe('MetaMask', function () {
assert.equal(seedPhrase.split(' ').length, 12) assert.equal(seedPhrase.split(' ').length, 12)
await delay(regularDelayMs) await delay(regularDelayMs)
const [nextScreen] = await driver.findElements(By.css('.backup-phrase button')) const nextScreen = await findElement(driver, By.css('.backup-phrase button'))
await nextScreen.click() await nextScreen.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
@ -158,62 +176,64 @@ describe('MetaMask', function () {
it('can retype the seed phrase', async () => { it('can retype the seed phrase', async () => {
const words = seedPhrase.split(' ') const words = seedPhrase.split(' ')
const [word0] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[0]}')]`)) const word0 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[0]}')]`))
await word0.click() await word0.click()
await delay(tinyDelayMs) await delay(tinyDelayMs)
const [word1] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[1]}')]`)) const word1 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[1]}')]`))
await word1.click() await word1.click()
await delay(tinyDelayMs) await delay(tinyDelayMs)
const [word2] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[2]}')]`)) const word2 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[2]}')]`))
await word2.click() await word2.click()
await delay(tinyDelayMs) await delay(tinyDelayMs)
const [word3] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[3]}')]`)) const word3 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[3]}')]`))
await word3.click() await word3.click()
await delay(tinyDelayMs) await delay(tinyDelayMs)
const [word4] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[4]}')]`)) const word4 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[4]}')]`))
await word4.click() await word4.click()
await delay(tinyDelayMs) await delay(tinyDelayMs)
const [word5] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[5]}')]`)) const word5 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[5]}')]`))
await word5.click() await word5.click()
await delay(tinyDelayMs) await delay(tinyDelayMs)
const [word6] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[6]}')]`)) const word6 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[6]}')]`))
await word6.click() await word6.click()
await delay(tinyDelayMs) await delay(tinyDelayMs)
const [word7] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[7]}')]`)) const word7 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[7]}')]`))
await word7.click() await word7.click()
await delay(tinyDelayMs) await delay(tinyDelayMs)
const [word8] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[8]}')]`)) const word8 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[8]}')]`))
await word8.click() await word8.click()
await delay(tinyDelayMs) await delay(tinyDelayMs)
const [word9] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[9]}')]`)) const word9 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[9]}')]`))
await word9.click() await word9.click()
await delay(tinyDelayMs) await delay(tinyDelayMs)
const [word10] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[10]}')]`)) const word10 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[10]}')]`))
await word10.click() await word10.click()
await delay(tinyDelayMs) await delay(tinyDelayMs)
const [word11] = await driver.findElements(By.xpath(`//button[contains(text(), '${words[11]}')]`)) const word11 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[11]}')]`))
await word11.click() await word11.click()
await delay(tinyDelayMs) await delay(tinyDelayMs)
const [confirm] = await driver.findElements(By.xpath(`//button[contains(text(), 'Confirm')]`)) const confirm = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirm.click() await confirm.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
it('clicks through the deposit modal', async () => { it('clicks through the deposit modal', async () => {
const [closeModal] = await driver.findElements(By.css('.page-container__header-close')) const buyModal = await driver.findElement(By.css('span .modal'))
const closeModal = await findElement(driver, By.css('.page-container__header-close'))
await closeModal.click() await closeModal.click()
await driver.wait(until.stalenessOf(buyModal))
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
}) })
@ -234,7 +254,7 @@ describe('MetaMask', function () {
await driver.findElement(By.css('.account-menu__icon')).click() await driver.findElement(By.css('.account-menu__icon')).click()
await delay(regularDelayMs) await delay(regularDelayMs)
const [logoutButton] = await driver.findElements(By.css('.account-menu__logout-button')) const logoutButton = await findElement(driver, By.css('.account-menu__logout-button'))
assert.equal(await logoutButton.getText(), 'Log out') assert.equal(await logoutButton.getText(), 'Log out')
await logoutButton.click() await logoutButton.click()
await delay(regularDelayMs) await delay(regularDelayMs)
@ -252,23 +272,23 @@ describe('MetaMask', function () {
await driver.findElement(By.css('.account-menu__icon')).click() await driver.findElement(By.css('.account-menu__icon')).click()
await delay(regularDelayMs) await delay(regularDelayMs)
const [createAccount] = await driver.findElements(By.xpath(`//div[contains(text(), 'Create Account')]`)) const createAccount = await findElement(driver, By.xpath(`//div[contains(text(), 'Create Account')]`))
await createAccount.click() await createAccount.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
it('set account name', async () => { it('set account name', async () => {
const [accountName] = await driver.findElements(By.css('.new-account-create-form input')) const accountName = await findElement(driver, By.css('.new-account-create-form input'))
await accountName.sendKeys('2nd account') await accountName.sendKeys('2nd account')
await delay(regularDelayMs) await delay(regularDelayMs)
const [create] = await driver.findElements(By.xpath(`//button[contains(text(), 'Create')]`)) const create = await findElement(driver, By.xpath(`//button[contains(text(), 'Create')]`))
await create.click() await create.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
it('should correct account name', async () => { it('should correct account name', async () => {
const [accountName] = await driver.findElements(By.css('.account-name')) const accountName = await findElement(driver, By.css('.account-name'))
assert.equal(await accountName.getText(), '2nd account') assert.equal(await accountName.getText(), '2nd account')
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
@ -279,19 +299,19 @@ describe('MetaMask', function () {
await driver.findElement(By.css('.account-menu__icon')).click() await driver.findElement(By.css('.account-menu__icon')).click()
await delay(regularDelayMs) await delay(regularDelayMs)
const [logoutButton] = await driver.findElements(By.css('.account-menu__logout-button')) const logoutButton = await findElement(driver, By.css('.account-menu__logout-button'))
assert.equal(await logoutButton.getText(), 'Log out') assert.equal(await logoutButton.getText(), 'Log out')
await logoutButton.click() await logoutButton.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
it('imports seed phrase', async () => { it('imports seed phrase', async () => {
const [restoreSeedLink] = await driver.findElements(By.css('.unlock-page__link--import')) const restoreSeedLink = await findElement(driver, By.css('.unlock-page__link--import'))
assert.equal(await restoreSeedLink.getText(), 'Import using account seed phrase') assert.equal(await restoreSeedLink.getText(), 'Import using account seed phrase')
await restoreSeedLink.click() await restoreSeedLink.click()
await delay(regularDelayMs) await delay(regularDelayMs)
const [seedTextArea] = await driver.findElements(By.css('textarea')) const seedTextArea = await findElement(driver, By.css('textarea'))
await seedTextArea.sendKeys(testSeedPhrase) await seedTextArea.sendKeys(testSeedPhrase)
await delay(regularDelayMs) await delay(regularDelayMs)
@ -302,7 +322,7 @@ describe('MetaMask', function () {
}) })
it('balance renders', async () => { it('balance renders', async () => {
const balance = await driver.findElement(By.css('.balance-display .token-amount')) const balance = await findElement(driver, By.css('.balance-display .token-amount'))
const tokenAmount = await balance.getText() const tokenAmount = await balance.getText()
assert.equal(tokenAmount, '100.000 ETH') assert.equal(tokenAmount, '100.000 ETH')
await delay(regularDelayMs) await delay(regularDelayMs)
@ -311,41 +331,41 @@ describe('MetaMask', function () {
describe('Send ETH from inside MetaMask', () => { describe('Send ETH from inside MetaMask', () => {
it('starts to send a transaction', async function () { it('starts to send a transaction', async function () {
const [sendButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Send')]`)) const sendButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Send')]`))
await sendButton.click() await sendButton.click()
await delay(regularDelayMs) await delay(regularDelayMs)
const [inputAddress] = await driver.findElements(By.css('input[placeholder="Recipient Address"]')) const inputAddress = await findElement(driver, By.css('input[placeholder="Recipient Address"]'))
const [inputAmount] = await driver.findElements(By.css('.currency-display__input')) const inputAmount = await findElement(driver, By.css('.currency-display__input'))
await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970') await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
await inputAmount.sendKeys('1') await inputAmount.sendKeys('1')
// Set the gas limit // Set the gas limit
const [configureGas] = await driver.findElements(By.css('.send-v2__gas-fee-display button')) const configureGas = await findElement(driver, By.css('.send-v2__gas-fee-display button'))
await configureGas.click() await configureGas.click()
await delay(regularDelayMs) await delay(regularDelayMs)
const [save] = await driver.findElements(By.xpath(`//button[contains(text(), 'Save')]`)) const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`))
await save.click() await save.click()
await delay(regularDelayMs) await delay(regularDelayMs)
// Continue to next screen // Continue to next screen
const [nextScreen] = await driver.findElements(By.xpath(`//button[contains(text(), 'Next')]`)) const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), 'Next')]`))
await nextScreen.click() await nextScreen.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
it('confirms the transaction', async function () { it('confirms the transaction', async function () {
const [confirmButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Confirm')]`)) const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirmButton.click() await confirmButton.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
it('finds the transaction in the transactions list', async function () { it('finds the transaction in the transactions list', async function () {
const transactions = await driver.findElements(By.css('.tx-list-item')) const transactions = await findElements(driver, By.css('.tx-list-item'))
assert.equal(transactions.length, 1) assert.equal(transactions.length, 1)
const txValues = await driver.findElements(By.css('.tx-list-value')) const txValues = await findElements(driver, By.css('.tx-list-value'))
assert.equal(txValues.length, 1) assert.equal(txValues.length, 1)
assert.equal(await txValues[0].getText(), '1 ETH') assert.equal(await txValues[0].getText(), '1 ETH')
}) })
@ -360,7 +380,7 @@ describe('MetaMask', function () {
await driver.switchTo().window(faucet) await driver.switchTo().window(faucet)
await delay(regularDelayMs) await delay(regularDelayMs)
const [send1eth] = await driver.findElements(By.xpath(`//button[contains(text(), '10 ether')]`)) const send1eth = await findElement(driver, By.xpath(`//button[contains(text(), '10 ether')]`), 14000)
await send1eth.click() await send1eth.click()
await delay(regularDelayMs) await delay(regularDelayMs)
@ -368,7 +388,7 @@ describe('MetaMask', function () {
await loadExtension(driver, extensionId) await loadExtension(driver, extensionId)
await delay(regularDelayMs) await delay(regularDelayMs)
const [confirmButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Confirm')]`)) const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`), 14000)
await confirmButton.click() await confirmButton.click()
await delay(regularDelayMs) await delay(regularDelayMs)
@ -385,31 +405,32 @@ describe('MetaMask', function () {
describe('Add existing token using search', () => { describe('Add existing token using search', () => {
it('clicks on the Add Token button', async () => { it('clicks on the Add Token button', async () => {
const [addToken] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Token')]`)) const addToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Token')]`))
await addToken.click() await addToken.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
it('can pick a token from the existing options', async () => { it('can pick a token from the existing options', async () => {
const [tokenSearch] = await driver.findElements(By.css('input.add-token__input')) const tokenSearch = await findElement(driver, By.css('#search-tokens'))
await tokenSearch.sendKeys('BAT') await tokenSearch.sendKeys('BAT')
await delay(regularDelayMs) await delay(regularDelayMs)
const [token] = await driver.findElements(By.xpath("//div[contains(text(), 'BAT')]")) const token = await findElement(driver, By.xpath("//span[contains(text(), 'BAT')]"))
await token.click() await token.click()
await delay(regularDelayMs) await delay(regularDelayMs)
const [nextScreen] = await driver.findElements(By.xpath(`//button[contains(text(), 'Next')]`)) const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), 'Next')]`))
await nextScreen.click() await nextScreen.click()
await delay(regularDelayMs) await delay(regularDelayMs)
const [addTokens] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Tokens')]`)) const addTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Tokens')]`))
await addTokens.click() await addTokens.click()
await delay(largeDelayMs) await delay(largeDelayMs)
}) })
it('renders the balance for the chosen token', async () => { it('renders the balance for the chosen token', async () => {
const balance = await driver.findElement(By.css('.tx-view .balance-display .token-amount')) const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
await driver.wait(until.elementTextIs(balance, '0BAT'))
const tokenAmount = await balance.getText() const tokenAmount = await balance.getText()
assert.equal(tokenAmount, '0BAT') assert.equal(tokenAmount, '0BAT')
await delay(regularDelayMs) await delay(regularDelayMs)
@ -428,14 +449,14 @@ describe('MetaMask', function () {
tokenName, tokenName,
tokenDecimal, tokenDecimal,
tokenSymbol, tokenSymbol,
] = await driver.findElements(By.css('input')) ] = await findElements(driver, By.css('.form-control'))
await totalSupply.sendKeys('100') await totalSupply.sendKeys('100')
await tokenName.sendKeys('Test') await tokenName.sendKeys('Test')
await tokenDecimal.sendKeys('0') await tokenDecimal.sendKeys('0')
await tokenSymbol.sendKeys('TST') await tokenSymbol.sendKeys('TST')
const [createToken] = await driver.findElements(By.xpath(`//button[contains(text(), 'Create Token')]`)) const createToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Create Token')]`))
await createToken.click() await createToken.click()
await delay(regularDelayMs) await delay(regularDelayMs)
@ -443,7 +464,7 @@ describe('MetaMask', function () {
await loadExtension(driver, extensionId) await loadExtension(driver, extensionId)
await delay(regularDelayMs) await delay(regularDelayMs)
const [confirmButton] = await driver.findElements(By.xpath(`//button[contains(text(), 'Confirm')]`)) const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirmButton.click() await confirmButton.click()
await delay(regularDelayMs) await delay(regularDelayMs)
@ -458,31 +479,32 @@ describe('MetaMask', function () {
}) })
it('clicks on the Add Token button', async () => { it('clicks on the Add Token button', async () => {
const [addToken] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Token')]`)) const addToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Token')]`))
await addToken.click() await addToken.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
it('picks the newly created Test token', async () => { it('picks the newly created Test token', async () => {
const [addCustomToken] = await driver.findElements(By.xpath("//div[contains(text(), 'Custom Token')]")) const addCustomToken = await findElement(driver, By.xpath("//div[contains(text(), 'Custom Token')]"))
await addCustomToken.click() await addCustomToken.click()
await delay(regularDelayMs) await delay(regularDelayMs)
const [newTokenAddress] = await driver.findElements(By.css('.add-token__add-custom-form input')) const newTokenAddress = await findElement(driver, By.css('#custom-address'))
await newTokenAddress.sendKeys(tokenAddress) await newTokenAddress.sendKeys(tokenAddress)
await delay(regularDelayMs) await delay(regularDelayMs)
const [nextScreen] = await driver.findElements(By.xpath(`//button[contains(text(), 'Next')]`)) const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), 'Next')]`))
await nextScreen.click() await nextScreen.click()
await delay(regularDelayMs) await delay(regularDelayMs)
const [addTokens] = await driver.findElements(By.xpath(`//button[contains(text(), 'Add Tokens')]`)) const addTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Tokens')]`))
await addTokens.click() await addTokens.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
it('renders the balance for the new token', async () => { it('renders the balance for the new token', async () => {
const [balance] = await driver.findElements(By.css('.tx-view .balance-display .token-amount')) const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
await driver.wait(until.elementTextIs(balance, '100TST'))
const tokenAmount = await balance.getText() const tokenAmount = await balance.getText()
assert.equal(tokenAmount, '100TST') assert.equal(tokenAmount, '100TST')
await delay(regularDelayMs) await delay(regularDelayMs)

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

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

@ -101,7 +101,7 @@ async function runSendFlowTest(assert, done) {
const sendAmountField = await queryAsync($, '.send-v2__form-row:eq(2)') const sendAmountField = await queryAsync($, '.send-v2__form-row:eq(2)')
sendAmountField.find('.currency-display')[0].click() sendAmountField.find('.currency-display')[0].click()
const sendAmountFieldInput = await findAsync(sendAmountField, 'input:text') const sendAmountFieldInput = await findAsync(sendAmountField, '.currency-display__input')
sendAmountFieldInput.val('5.1') sendAmountFieldInput.val('5.1')
reactTriggerChange(sendAmountField.find('input')[0]) reactTriggerChange(sendAmountField.find('input')[0])
@ -117,17 +117,17 @@ async function runSendFlowTest(assert, done) {
const sendGasField = await queryAsync($, '.send-v2__gas-fee-display') const sendGasField = await queryAsync($, '.send-v2__gas-fee-display')
assert.equal( assert.equal(
sendGasField.find('.currency-display__input-wrapper > input').val(), sendGasField.find('.currency-display__input-wrapper > input').val(),
'0.000198', '0.000021',
'send gas field should show estimated gas total' 'send gas field should show estimated gas total'
) )
assert.equal( assert.equal(
sendGasField.find('.currency-display__converted-value')[0].textContent, sendGasField.find('.currency-display__converted-value')[0].textContent,
'$0.24 USD', '$0.03 USD',
'send gas field should show estimated gas total converted to USD' 'send gas field should show estimated gas total converted to USD'
) )
await customizeGas(assert, 0, 21000, '0', '$0.00 USD') await customizeGas(assert, 0, 21000, '0', '$0.00 USD')
await customizeGas(assert, 500, 60000, '0.003', '$3.60 USD') await customizeGas(assert, 500, 60000, '0.03', '$36.03 USD')
const sendButton = await queryAsync($, 'button.btn-primary.btn--large.page-container__footer-button') const sendButton = await queryAsync($, 'button.btn-primary.btn--large.page-container__footer-button')
assert.equal(sendButton[0].textContent, 'Next', 'next button rendered') assert.equal(sendButton[0].textContent, 'Next', 'next button rendered')
@ -165,7 +165,7 @@ async function runSendFlowTest(assert, done) {
const sendAmountFieldInEdit = await queryAsync($, '.send-v2__form-row:eq(2)') const sendAmountFieldInEdit = await queryAsync($, '.send-v2__form-row:eq(2)')
sendAmountFieldInEdit.find('.currency-display')[0].click() sendAmountFieldInEdit.find('.currency-display')[0].click()
const sendAmountFieldInputInEdit = sendAmountFieldInEdit.find('input:text') const sendAmountFieldInputInEdit = sendAmountFieldInEdit.find('.currency-display__input')
sendAmountFieldInputInEdit.val('1.0') sendAmountFieldInputInEdit.val('1.0')
reactTriggerChange(sendAmountFieldInputInEdit[0]) reactTriggerChange(sendAmountFieldInputInEdit[0])

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

@ -53,6 +53,9 @@ describe('MetaMaskController', function () {
}, },
initState: clone(firstTimeState), initState: clone(firstTimeState),
}) })
// disable diagnostics
metamaskController.diagnostics = null
// add sinon method spies
sandbox.spy(metamaskController.keyringController, 'createNewVaultAndKeychain') sandbox.spy(metamaskController.keyringController, 'createNewVaultAndKeychain')
sandbox.spy(metamaskController.keyringController, 'createNewVaultAndRestore') sandbox.spy(metamaskController.keyringController, 'createNewVaultAndRestore')
}) })

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

@ -0,0 +1,77 @@
const assert = require('assert')
const recipientBlackListChecker = require('../../../../../app/scripts/controllers/transactions/lib/recipient-blacklist-checker')
const {
ROPSTEN_CODE,
RINKEYBY_CODE,
KOVAN_CODE,
} = require('../../../../../app/scripts/controllers/network/enums')
const KeyringController = require('eth-keyring-controller')
describe('Recipient Blacklist Checker', function () {
let publicAccounts
before(async function () {
const damnedMnemonic = 'candy maple cake sugar pudding cream honey rich smooth crumble sweet treat'
const keyringController = new KeyringController({})
const Keyring = keyringController.getKeyringClassForType('HD Key Tree')
const opts = {
mnemonic: damnedMnemonic,
numberOfAccounts: 10,
}
const keyring = new Keyring(opts)
publicAccounts = await keyring.getAccounts()
})
describe('#checkAccount', function () {
it('does not fail on test networks', function () {
let callCount = 0
const networks = [ROPSTEN_CODE, RINKEYBY_CODE, KOVAN_CODE]
for (let networkId in networks) {
publicAccounts.forEach((account) => {
recipientBlackListChecker.checkAccount(networkId, account)
callCount++
})
}
assert.equal(callCount, 30)
})
it('fails on mainnet', function () {
const mainnetId = 1
let callCount = 0
publicAccounts.forEach((account) => {
try {
recipientBlackListChecker.checkAccount(mainnetId, account)
assert.fail('function should have thrown an error')
} catch (err) {
assert.equal(err.message, 'Recipient is a public account')
}
callCount++
})
assert.equal(callCount, 10)
})
it('fails for public account - uppercase', function () {
const mainnetId = 1
const publicAccount = '0X0D1D4E623D10F9FBA5DB95830F7D3839406C6AF2'
try {
recipientBlackListChecker.checkAccount(mainnetId, publicAccount)
assert.fail('function should have thrown an error')
} catch (err) {
assert.equal(err.message, 'Recipient is a public account')
}
})
it('fails for public account - lowercase', async function () {
const mainnetId = 1
const publicAccount = '0x0d1d4e623d10f9fba5db95830f7d3839406c6af2'
try {
await recipientBlackListChecker.checkAccount(mainnetId, publicAccount)
assert.fail('function should have thrown an error')
} catch (err) {
assert.equal(err.message, 'Recipient is a public account')
}
})
})
})

@ -185,6 +185,23 @@ describe('Transaction Controller', function () {
.catch(done) .catch(done)
}) })
it('should fail if recipient is public', function (done) {
txController.networkStore = new ObservableStore(1)
txController.addUnapprovedTransaction({ from: '0x1678a085c290ebd122dc42cba69373b5953b831d', to: '0x0d1d4e623D10F9FBA5Db95830F7d3839406C6AF2' })
.catch((err) => {
if (err.message === 'Recipient is a public account') done()
else done(err)
})
})
it('should not fail if recipient is public but not on mainnet', function (done) {
txController.once('newUnapprovedTx', (txMetaFromEmit) => {
assert(txMetaFromEmit, 'txMeta is falsey')
done()
})
txController.addUnapprovedTransaction({ from: '0x1678a085c290ebd122dc42cba69373b5953b831d', to: '0x0d1d4e623D10F9FBA5Db95830F7d3839406C6AF2' })
.catch(done)
})
}) })
describe('#addTxGasDefaults', function () { describe('#addTxGasDefaults', function () {

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

@ -2,6 +2,12 @@ const abi = require('human-standard-token-abi')
const pify = require('pify') const pify = require('pify')
const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url') const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url')
const { getTokenAddressFromTokenObject } = require('./util') const { getTokenAddressFromTokenObject } = require('./util')
const {
calcGasTotal,
calcTokenBalance,
estimateGas,
estimateGasPriceFromRecentBlocks,
} = require('./components/send_/send.utils')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const { fetchLocale } = require('../i18n-helper') const { fetchLocale } = require('../i18n-helper')
const log = require('loglevel') const log = require('loglevel')
@ -155,8 +161,6 @@ var actions = {
updateTransactionParams, updateTransactionParams,
UPDATE_TRANSACTION_PARAMS: 'UPDATE_TRANSACTION_PARAMS', UPDATE_TRANSACTION_PARAMS: 'UPDATE_TRANSACTION_PARAMS',
// send screen // send screen
estimateGas,
getGasPrice,
UPDATE_GAS_LIMIT: 'UPDATE_GAS_LIMIT', UPDATE_GAS_LIMIT: 'UPDATE_GAS_LIMIT',
UPDATE_GAS_PRICE: 'UPDATE_GAS_PRICE', UPDATE_GAS_PRICE: 'UPDATE_GAS_PRICE',
UPDATE_GAS_TOTAL: 'UPDATE_GAS_TOTAL', UPDATE_GAS_TOTAL: 'UPDATE_GAS_TOTAL',
@ -169,17 +173,21 @@ var actions = {
UPDATE_MAX_MODE: 'UPDATE_MAX_MODE', UPDATE_MAX_MODE: 'UPDATE_MAX_MODE',
UPDATE_SEND: 'UPDATE_SEND', UPDATE_SEND: 'UPDATE_SEND',
CLEAR_SEND: 'CLEAR_SEND', CLEAR_SEND: 'CLEAR_SEND',
updateGasLimit, OPEN_FROM_DROPDOWN: 'OPEN_FROM_DROPDOWN',
updateGasPrice, CLOSE_FROM_DROPDOWN: 'CLOSE_FROM_DROPDOWN',
updateGasTotal, setGasLimit,
setGasPrice,
updateGasData,
setGasTotal,
setSendTokenBalance,
updateSendTokenBalance, updateSendTokenBalance,
updateSendFrom, updateSendFrom,
updateSendTo, updateSendTo,
updateSendAmount, updateSendAmount,
updateSendMemo, updateSendMemo,
updateSendErrors,
setMaxModeTo, setMaxModeTo,
updateSend, updateSend,
updateSendErrors,
clearSend, clearSend,
setSelectedAddress, setSelectedAddress,
// app messages // app messages
@ -550,10 +558,12 @@ function importNewAccount (strategy, args) {
} }
dispatch(actions.hideLoadingIndication()) dispatch(actions.hideLoadingIndication())
dispatch(actions.updateMetamaskState(newState)) dispatch(actions.updateMetamaskState(newState))
dispatch({ if (newState.selectedAddress) {
type: actions.SHOW_ACCOUNT_DETAIL, dispatch({
value: newState.selectedAddress, type: actions.SHOW_ACCOUNT_DETAIL,
}) value: newState.selectedAddress,
})
}
return newState return newState
} }
} }
@ -701,60 +711,96 @@ function signTx (txData) {
} }
} }
function estimateGas (params = {}) { function setGasLimit (gasLimit) {
return (dispatch) => { return {
return new Promise((resolve, reject) => { type: actions.UPDATE_GAS_LIMIT,
global.ethQuery.estimateGas(params, (err, data) => { value: gasLimit,
if (err) {
dispatch(actions.displayWarning(err.message))
return reject(err)
}
dispatch(actions.hideWarning())
dispatch(actions.updateGasLimit(data))
return resolve(data)
})
})
} }
} }
function updateGasLimit (gasLimit) { function setGasPrice (gasPrice) {
return { return {
type: actions.UPDATE_GAS_LIMIT, type: actions.UPDATE_GAS_PRICE,
value: gasLimit, value: gasPrice,
}
}
function setGasTotal (gasTotal) {
return {
type: actions.UPDATE_GAS_TOTAL,
value: gasTotal,
} }
} }
function getGasPrice () { function updateGasData ({
blockGasLimit,
recentBlocks,
selectedAddress,
selectedToken,
to,
value,
}) {
const estimatedGasPrice = estimateGasPriceFromRecentBlocks(recentBlocks)
return (dispatch) => { return (dispatch) => {
return new Promise((resolve, reject) => { return Promise.all([
global.ethQuery.gasPrice((err, data) => { Promise.resolve(estimatedGasPrice),
if (err) { estimateGas({
dispatch(actions.displayWarning(err.message)) estimateGasMethod: background.estimateGas,
return reject(err) blockGasLimit,
} selectedAddress,
dispatch(actions.hideWarning()) selectedToken,
dispatch(actions.updateGasPrice(data)) to,
return resolve(data) value,
}) gasPrice: estimatedGasPrice,
}),
])
.then(([gasPrice, gas]) => {
dispatch(actions.setGasPrice(gasPrice))
dispatch(actions.setGasLimit(gas))
return calcGasTotal(gas, gasPrice)
})
.then((gasEstimate) => {
dispatch(actions.setGasTotal(gasEstimate))
dispatch(updateSendErrors({ gasLoadingError: null }))
})
.catch(err => {
log.error(err)
dispatch(updateSendErrors({ gasLoadingError: 'gasLoadingError' }))
}) })
} }
} }
function updateGasPrice (gasPrice) { function updateSendTokenBalance ({
return { selectedToken,
type: actions.UPDATE_GAS_PRICE, tokenContract,
value: gasPrice, address,
}) {
return (dispatch) => {
const tokenBalancePromise = tokenContract
? tokenContract.balanceOf(address)
: Promise.resolve()
return tokenBalancePromise
.then(usersToken => {
if (usersToken) {
const newTokenBalance = calcTokenBalance({ selectedToken, usersToken })
dispatch(setSendTokenBalance(newTokenBalance.toString(10)))
}
})
.catch(err => {
log.error(err)
updateSendErrors({ tokenBalance: 'tokenBalanceError' })
})
} }
} }
function updateGasTotal (gasTotal) { function updateSendErrors (errorObject) {
return { return {
type: actions.UPDATE_GAS_TOTAL, type: actions.UPDATE_SEND_ERRORS,
value: gasTotal, value: errorObject,
} }
} }
function updateSendTokenBalance (tokenBalance) { function setSendTokenBalance (tokenBalance) {
return { return {
type: actions.UPDATE_SEND_TOKEN_BALANCE, type: actions.UPDATE_SEND_TOKEN_BALANCE,
value: tokenBalance, value: tokenBalance,
@ -789,13 +835,6 @@ function updateSendMemo (memo) {
} }
} }
function updateSendErrors (error) {
return {
type: actions.UPDATE_SEND_ERRORS,
value: error,
}
}
function setMaxModeTo (bool) { function setMaxModeTo (bool) {
return { return {
type: actions.UPDATE_MAX_MODE, type: actions.UPDATE_MAX_MODE,

@ -11,7 +11,7 @@ const log = require('loglevel')
// init // init
const InitializeScreen = require('../../mascara/src/app/first-time').default const InitializeScreen = require('../../mascara/src/app/first-time').default
// accounts // accounts
const SendTransactionScreen2 = require('./components/send/send-v2-container') const SendTransactionScreen = require('./components/send_/send.container')
const ConfirmTxScreen = require('./conf-tx') const ConfirmTxScreen = require('./conf-tx')
// slideout menu // slideout menu
@ -77,7 +77,7 @@ class App extends Component {
h(Authenticated, { path: SETTINGS_ROUTE, component: Settings }), h(Authenticated, { path: SETTINGS_ROUTE, component: Settings }),
h(Authenticated, { path: NOTICE_ROUTE, exact, component: NoticeScreen }), h(Authenticated, { path: NOTICE_ROUTE, exact, component: NoticeScreen }),
h(Authenticated, { path: `${CONFIRM_TRANSACTION_ROUTE}/:id?`, component: ConfirmTxScreen }), h(Authenticated, { path: `${CONFIRM_TRANSACTION_ROUTE}/:id?`, component: ConfirmTxScreen }),
h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen2 }), h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen }),
h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }), h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }),
h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }), h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }),
h(Authenticated, { path: NEW_ACCOUNT_ROUTE, component: CreateAccountPage }), h(Authenticated, { path: NEW_ACCOUNT_ROUTE, component: CreateAccountPage }),
@ -99,7 +99,7 @@ class App extends Component {
} = this.props } = this.props
const isLoadingNetwork = network === 'loading' && currentView.name !== 'config' const isLoadingNetwork = network === 'loading' && currentView.name !== 'config'
const loadMessage = loadingMessage || isLoadingNetwork ? const loadMessage = loadingMessage || isLoadingNetwork ?
this.getConnectingLabel() : null this.getConnectingLabel(loadingMessage) : null
log.debug('Main ui render function') log.debug('Main ui render function')
return ( return (
@ -210,7 +210,10 @@ class App extends Component {
} }
} }
getConnectingLabel = function () { getConnectingLabel = function (loadingMessage) {
if (loadingMessage) {
return loadingMessage
}
const { provider } = this.props const { provider } = this.props
const providerName = provider.type const providerName = provider.type
@ -311,7 +314,7 @@ function mapStateToProps (state) {
noActiveNotices, noActiveNotices,
seedWords, seedWords,
unapprovedTxs, unapprovedTxs,
lastUnreadNotice, nextUnreadNotice,
lostAccounts, lostAccounts,
unapprovedMsgCount, unapprovedMsgCount,
unapprovedPersonalMsgCount, unapprovedPersonalMsgCount,
@ -345,7 +348,7 @@ function mapStateToProps (state) {
network: state.metamask.network, network: state.metamask.network,
provider: state.metamask.provider, provider: state.metamask.provider,
forgottenPassword: state.appState.forgottenPassword, forgottenPassword: state.appState.forgottenPassword,
lastUnreadNotice, nextUnreadNotice,
lostAccounts, lostAccounts,
frequentRpcList: state.metamask.frequentRpcList || [], frequentRpcList: state.metamask.frequentRpcList || [],
currentCurrency: state.metamask.currentCurrency, currentCurrency: state.metamask.currentCurrency,

@ -135,11 +135,12 @@ AccountMenu.prototype.renderAccounts = function () {
showAccountDetail, showAccountDetail,
} = this.props } = this.props
return Object.keys(identities).map((key, index) => { const accountOrder = keyrings.reduce((list, keyring) => list.concat(keyring.accounts), [])
const identity = identities[key] return accountOrder.map((address) => {
const identity = identities[address]
const isSelected = identity.address === selectedAddress const isSelected = identity.address === selectedAddress
const balanceValue = accounts[key] ? accounts[key].balance : '' const balanceValue = accounts[address] ? accounts[address].balance : ''
const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...' const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...'
const simpleAddress = identity.address.substring(2).toLowerCase() const simpleAddress = identity.address.substring(2).toLowerCase()

@ -1,113 +0,0 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
module.exports = CurrencyInput
inherits(CurrencyInput, Component)
function CurrencyInput (props) {
Component.call(this)
const sanitizedValue = sanitizeValue(props.value)
this.state = {
value: sanitizedValue,
emptyState: false,
focused: false,
}
}
function removeNonDigits (str) {
return str.match(/\d|$/g).join('')
}
// Removes characters that are not digits, then removes leading zeros
function sanitizeInteger (val) {
return String(parseInt(removeNonDigits(val) || '0', 10))
}
function sanitizeDecimal (val) {
return removeNonDigits(val)
}
// Take a single string param and returns a non-negative integer or float as a string.
// Breaks the input into three parts: the integer, the decimal point, and the decimal/fractional part.
// Removes leading zeros from the integer, and non-digits from the integer and decimal
// The integer is returned as '0' in cases where it would be empty. A decimal point is
// included in the returned string if one is included in the param
// Examples:
// sanitizeValue('0') -> '0'
// sanitizeValue('a') -> '0'
// sanitizeValue('010.') -> '10.'
// sanitizeValue('0.005') -> '0.005'
// sanitizeValue('22.200') -> '22.200'
// sanitizeValue('.200') -> '0.200'
// sanitizeValue('a.b.1.c,89.123') -> '0.189123'
function sanitizeValue (value) {
let [ , integer, point, decimal] = (/([^.]*)([.]?)([^.]*)/).exec(value)
integer = sanitizeInteger(integer) || '0'
decimal = sanitizeDecimal(decimal)
return `${integer}${point}${decimal}`
}
CurrencyInput.prototype.handleChange = function (newValue) {
const { onInputChange } = this.props
const { value } = this.state
let parsedValue = newValue
const newValueLastIndex = newValue.length - 1
if (value === '0' && newValue[newValueLastIndex] === '0') {
parsedValue = parsedValue.slice(0, newValueLastIndex)
}
const sanitizedValue = sanitizeValue(parsedValue)
this.setState({
value: sanitizedValue,
emptyState: newValue === '' && sanitizedValue === '0',
})
onInputChange(sanitizedValue)
}
// If state.value === props.value plus a decimal point, or at least one
// zero or a decimal point and at least one zero, then this returns state.value
// after it is sanitized with getValueParts
CurrencyInput.prototype.getValueToRender = function () {
const { value } = this.props
const { value: stateValue } = this.state
const trailingStateString = (new RegExp(`^${value}(.+)`)).exec(stateValue)
const trailingDecimalAndZeroes = trailingStateString && (/^[.0]0*/).test(trailingStateString[1])
return sanitizeValue(trailingDecimalAndZeroes
? stateValue
: value)
}
CurrencyInput.prototype.render = function () {
const {
className,
placeholder,
readOnly,
inputRef,
type,
} = this.props
const { emptyState, focused } = this.state
const inputSizeMultiplier = readOnly ? 1 : 1.2
const valueToRender = this.getValueToRender()
return h('input', {
className,
type,
value: emptyState ? '' : valueToRender,
placeholder: focused ? '' : placeholder,
size: valueToRender.length * inputSizeMultiplier,
readOnly,
onFocus: () => this.setState({ focused: true, emptyState: valueToRender === '0' }),
onBlur: () => this.setState({ focused: false, emptyState: false }),
onChange: e => this.handleChange(e.target.value),
ref: inputRef,
})
}

@ -8,15 +8,19 @@ const GasModalCard = require('./gas-modal-card')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
import {
updateSendErrors,
} from '../../ducks/send.duck'
const { const {
MIN_GAS_PRICE_DEC, MIN_GAS_PRICE_DEC,
MIN_GAS_LIMIT_DEC, MIN_GAS_LIMIT_DEC,
MIN_GAS_PRICE_GWEI, MIN_GAS_PRICE_GWEI,
} = require('../send/send-constants') } = require('../send_/send.constants')
const { const {
isBalanceSufficient, isBalanceSufficient,
} = require('../send/send-utils') } = require('../send_/send.utils')
const { const {
conversionUtil, conversionUtil,
@ -61,11 +65,11 @@ function mapStateToProps (state) {
function mapDispatchToProps (dispatch) { function mapDispatchToProps (dispatch) {
return { return {
hideModal: () => dispatch(actions.hideModal()), hideModal: () => dispatch(actions.hideModal()),
updateGasPrice: newGasPrice => dispatch(actions.updateGasPrice(newGasPrice)), setGasPrice: newGasPrice => dispatch(actions.setGasPrice(newGasPrice)),
updateGasLimit: newGasLimit => dispatch(actions.updateGasLimit(newGasLimit)), setGasLimit: newGasLimit => dispatch(actions.setGasLimit(newGasLimit)),
updateGasTotal: newGasTotal => dispatch(actions.updateGasTotal(newGasTotal)), setGasTotal: newGasTotal => dispatch(actions.setGasTotal(newGasTotal)),
updateSendAmount: newAmount => dispatch(actions.updateSendAmount(newAmount)), updateSendAmount: newAmount => dispatch(actions.updateSendAmount(newAmount)),
updateSendErrors: error => dispatch(actions.updateSendErrors(error)), updateSendErrors: error => dispatch(updateSendErrors(error)),
} }
} }
@ -105,10 +109,10 @@ module.exports = connect(mapStateToProps, mapDispatchToProps)(CustomizeGasModal)
CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) { CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) {
const { const {
updateGasPrice, setGasPrice,
updateGasLimit, setGasLimit,
hideModal, hideModal,
updateGasTotal, setGasTotal,
maxModeOn, maxModeOn,
selectedToken, selectedToken,
balance, balance,
@ -125,9 +129,9 @@ CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) {
updateSendAmount(maxAmount) updateSendAmount(maxAmount)
} }
updateGasPrice(ethUtil.addHexPrefix(gasPrice)) setGasPrice(ethUtil.addHexPrefix(gasPrice))
updateGasLimit(ethUtil.addHexPrefix(gasLimit)) setGasLimit(ethUtil.addHexPrefix(gasLimit))
updateGasTotal(ethUtil.addHexPrefix(gasTotal)) setGasTotal(ethUtil.addHexPrefix(gasTotal))
updateSendErrors({ insufficientFunds: false }) updateSendErrors({ insufficientFunds: false })
hideModal() hideModal()
} }

@ -1,7 +1,7 @@
const Component = require('react').Component const Component = require('react').Component
const h = require('react-hyperscript') const h = require('react-hyperscript')
const inherits = require('util').inherits const inherits = require('util').inherits
const AccountListItem = require('../send/account-list-item') const AccountListItem = require('../send_/account-list-item/account-list-item.component').default
module.exports = AccountDropdownMini module.exports = AccountDropdownMini

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

@ -1,7 +1,6 @@
const Component = require('react').Component const Component = require('react').Component
const h = require('react-hyperscript') const h = require('react-hyperscript')
const inherits = require('util').inherits const inherits = require('util').inherits
const CurrencyInput = require('./currency-input')
const { const {
addCurrencies, addCurrencies,
conversionGTE, conversionGTE,
@ -51,14 +50,15 @@ InputNumber.prototype.render = function () {
const { unitLabel, step = 1, placeholder, value = 0 } = this.props const { unitLabel, step = 1, placeholder, value = 0 } = this.props
return h('div.customize-gas-input-wrapper', {}, [ return h('div.customize-gas-input-wrapper', {}, [
h(CurrencyInput, { h('input', {
className: 'customize-gas-input', className: 'customize-gas-input',
value, value,
placeholder, placeholder,
type: 'number', type: 'number',
onInputChange: newValue => { onChange: e => {
this.setValue(newValue) this.setValue(e.target.value)
}, },
min: 0,
}), }),
h('span.gas-tooltip-input-detail', {}, [unitLabel]), h('span.gas-tooltip-input-detail', {}, [unitLabel]),
h('div.gas-tooltip-input-arrows', {}, [ h('div.gas-tooltip-input-arrows', {}, [

@ -0,0 +1 @@
export { default } from './page-container.component'

@ -0,0 +1,18 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class PageContainerContent extends Component {
static propTypes = {
children: PropTypes.node.isRequired,
};
render () {
return (
<div className="page-container__content">
{this.props.children}
</div>
)
}
}

@ -0,0 +1 @@
export { default } from './page-container-footer.component'

@ -0,0 +1,54 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Button from '../../button'
export default class PageContainerFooter extends Component {
static propTypes = {
onCancel: PropTypes.func,
cancelText: PropTypes.string,
onSubmit: PropTypes.func,
submitText: PropTypes.string,
disabled: PropTypes.bool,
}
static contextTypes = {
t: PropTypes.func,
}
render () {
const {
onCancel,
cancelText,
onSubmit,
submitText,
disabled,
} = this.props
return (
<div className="page-container__footer">
<Button
type="default"
large={true}
className="page-container__footer-button"
onClick={() => onCancel()}
>
{ cancelText || this.context.t('cancel') }
</Button>
<Button
type="primary"
large={true}
className="page-container__footer-button"
disabled={disabled}
onClick={e => onSubmit(e)}
>
{ submitText || this.context.t('next') }
</Button>
</div>
)
}
}

@ -0,0 +1,35 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class PageContainerHeader extends Component {
static propTypes = {
title: PropTypes.string,
subtitle: PropTypes.string,
onClose: PropTypes.func,
};
render () {
const { title, subtitle, onClose } = this.props
return (
<div className="page-container__header">
<div className="page-container__title">
{title}
</div>
<div className="page-container__subtitle">
{subtitle}
</div>
<div
className="page-container__header-close"
onClick={() => onClose()}
/>
</div>
)
}
}

@ -0,0 +1 @@
export { default } from './page-container-header.component'

@ -0,0 +1,57 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class PageContainerHeader extends Component {
static propTypes = {
title: PropTypes.string.isRequired,
subtitle: PropTypes.string,
onClose: PropTypes.func,
showBackButton: PropTypes.bool,
onBackButtonClick: PropTypes.func,
backButtonStyles: PropTypes.object,
backButtonString: PropTypes.string,
};
renderHeaderRow () {
const { showBackButton, onBackButtonClick, backButtonStyles, backButtonString } = this.props
return showBackButton && (
<div className="page-container__header-row">
<span
className="page-container__back-button"
onClick={onBackButtonClick}
style={backButtonStyles}
>
{ backButtonString || 'Back' }
</span>
</div>
)
}
render () {
const { title, subtitle, onClose } = this.props
return (
<div className="page-container__header">
{ this.renderHeaderRow() }
<div className="page-container__title">
{title}
</div>
<div className="page-container__subtitle">
{subtitle}
</div>
<div
className="page-container__header-close"
onClick={() => onClose()}
/>
</div>
)
}
}

@ -0,0 +1,72 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import PageContainerHeader from './page-container-header'
import PageContainerFooter from './page-container-footer'
export default class PageContainer extends Component {
static propTypes = {
// PageContainerHeader props
title: PropTypes.string.isRequired,
subtitle: PropTypes.string,
onClose: PropTypes.func,
showBackButton: PropTypes.bool,
onBackButtonClick: PropTypes.func,
backButtonStyles: PropTypes.object,
backButtonString: PropTypes.string,
// Content props
ContentComponent: PropTypes.func,
contentComponentProps: PropTypes.object,
// PageContainerFooter props
onCancel: PropTypes.func,
cancelText: PropTypes.string,
onSubmit: PropTypes.func,
submitText: PropTypes.string,
disabled: PropTypes.bool,
};
render () {
const {
title,
subtitle,
onClose,
showBackButton,
onBackButtonClick,
backButtonStyles,
backButtonString,
ContentComponent,
contentComponentProps,
onCancel,
cancelText,
onSubmit,
submitText,
disabled,
} = this.props
return (
<div className="page-container">
<PageContainerHeader
title={title}
subtitle={subtitle}
onClose={onClose}
showBackButton={showBackButton}
onBackButtonClick={onBackButtonClick}
backButtonStyles={backButtonStyles}
backButtonString={backButtonString}
/>
<div className="page-container__content">
<ContentComponent { ...contentComponentProps } />
</div>
<PageContainerFooter
onCancel={onCancel}
cancelText={cancelText}
onSubmit={onSubmit}
submitText={submitText}
disabled={disabled}
/>
</div>
)
}
}

@ -231,7 +231,7 @@ class AddToken extends Component {
<div className="add-token__custom-token-form"> <div className="add-token__custom-token-form">
<TextField <TextField
id="custom-address" id="custom-address"
label="Token Address" label={this.context.t('tokenAddress')}
type="text" type="text"
value={customAddress} value={customAddress}
onChange={e => this.handleCustomAddressChange(e.target.value)} onChange={e => this.handleCustomAddressChange(e.target.value)}
@ -241,7 +241,7 @@ class AddToken extends Component {
/> />
<TextField <TextField
id="custom-symbol" id="custom-symbol"
label="Token Symbol" label={this.context.t('tokenSymbol')}
type="text" type="text"
value={customSymbol} value={customSymbol}
onChange={e => this.handleCustomSymbolChange(e.target.value)} onChange={e => this.handleCustomSymbolChange(e.target.value)}
@ -252,7 +252,7 @@ class AddToken extends Component {
/> />
<TextField <TextField
id="custom-decimals" id="custom-decimals"
label="Decimals of Precision" label={this.context.t('decimal')}
type="number" type="number"
value={customDecimals} value={customDecimals}
onChange={e => this.handleCustomDecimalsChange(e.target.value)} onChange={e => this.handleCustomDecimalsChange(e.target.value)}

@ -82,18 +82,19 @@ class JsonImportSubview extends Component {
} }
createNewKeychain () { createNewKeychain () {
const { firstAddress, displayWarning, importNewJsonAccount, setSelectedAddress } = this.props
const state = this.state const state = this.state
if (!state) { if (!state) {
const message = this.context.t('validFileImport') const message = this.context.t('validFileImport')
return this.props.displayWarning(message) return displayWarning(message)
} }
const { fileContents } = state const { fileContents } = state
if (!fileContents) { if (!fileContents) {
const message = this.context.t('needImportFile') const message = this.context.t('needImportFile')
return this.props.displayWarning(message) return displayWarning(message)
} }
const passwordInput = document.getElementById('json-password-box') const passwordInput = document.getElementById('json-password-box')
@ -101,12 +102,19 @@ class JsonImportSubview extends Component {
if (!password) { if (!password) {
const message = this.context.t('needImportPassword') const message = this.context.t('needImportPassword')
return this.props.displayWarning(message) return displayWarning(message)
} }
this.props.importNewJsonAccount([ fileContents, password ]) importNewJsonAccount([ fileContents, password ])
// JS runtime requires caught rejections but failures are handled by Redux .then(({ selectedAddress }) => {
.catch() if (selectedAddress) {
history.push(DEFAULT_ROUTE)
} else {
displayWarning('Error importing account.')
setSelectedAddress(firstAddress)
}
})
.catch(err => displayWarning(err))
} }
} }
@ -114,14 +122,17 @@ JsonImportSubview.propTypes = {
error: PropTypes.string, error: PropTypes.string,
goHome: PropTypes.func, goHome: PropTypes.func,
displayWarning: PropTypes.func, displayWarning: PropTypes.func,
firstAddress: PropTypes.string,
importNewJsonAccount: PropTypes.func, importNewJsonAccount: PropTypes.func,
history: PropTypes.object, history: PropTypes.object,
setSelectedAddress: PropTypes.func,
t: PropTypes.func, t: PropTypes.func,
} }
const mapStateToProps = state => { const mapStateToProps = state => {
return { return {
error: state.appState.warning, error: state.appState.warning,
firstAddress: Object.keys(state.metamask.accounts)[0],
} }
} }
@ -130,6 +141,7 @@ const mapDispatchToProps = dispatch => {
goHome: () => dispatch(actions.goHome()), goHome: () => dispatch(actions.goHome()),
displayWarning: warning => dispatch(actions.displayWarning(warning)), displayWarning: warning => dispatch(actions.displayWarning(warning)),
importNewJsonAccount: options => dispatch(actions.importNewAccount('JSON File', options)), importNewJsonAccount: options => dispatch(actions.importNewAccount('JSON File', options)),
setSelectedAddress: (address) => dispatch(actions.setSelectedAddress(address)),
} }
} }

@ -21,6 +21,7 @@ module.exports = compose(
function mapStateToProps (state) { function mapStateToProps (state) {
return { return {
error: state.appState.warning, error: state.appState.warning,
firstAddress: Object.keys(state.metamask.accounts)[0],
} }
} }
@ -29,7 +30,8 @@ function mapDispatchToProps (dispatch) {
importNewAccount: (strategy, [ privateKey ]) => { importNewAccount: (strategy, [ privateKey ]) => {
return dispatch(actions.importNewAccount(strategy, [ privateKey ])) return dispatch(actions.importNewAccount(strategy, [ privateKey ]))
}, },
displayWarning: () => dispatch(actions.displayWarning(null)), displayWarning: (message) => dispatch(actions.displayWarning(message || null)),
setSelectedAddress: (address) => dispatch(actions.setSelectedAddress(address)),
} }
} }
@ -40,7 +42,7 @@ function PrivateKeyImportView () {
} }
PrivateKeyImportView.prototype.render = function () { PrivateKeyImportView.prototype.render = function () {
const { error } = this.props const { error, displayWarning } = this.props
return ( return (
h('div.new-account-import-form__private-key', [ h('div.new-account-import-form__private-key', [
@ -60,7 +62,10 @@ PrivateKeyImportView.prototype.render = function () {
h('div.new-account-import-form__buttons', {}, [ h('div.new-account-import-form__buttons', {}, [
h('button.btn-default.btn--large.new-account-create-form__button', { h('button.btn-default.btn--large.new-account-create-form__button', {
onClick: () => this.props.history.push(DEFAULT_ROUTE), onClick: () => {
displayWarning(null)
this.props.history.push(DEFAULT_ROUTE)
},
}, [ }, [
this.context.t('cancel'), this.context.t('cancel'),
]), ]),
@ -88,10 +93,16 @@ PrivateKeyImportView.prototype.createKeyringOnEnter = function (event) {
PrivateKeyImportView.prototype.createNewKeychain = function () { PrivateKeyImportView.prototype.createNewKeychain = function () {
const input = document.getElementById('private-key-box') const input = document.getElementById('private-key-box')
const privateKey = input.value const privateKey = input.value
const { importNewAccount, history } = this.props const { importNewAccount, history, displayWarning, setSelectedAddress, firstAddress } = this.props
importNewAccount('Private Key', [ privateKey ]) importNewAccount('Private Key', [ privateKey ])
// JS runtime requires caught rejections but failures are handled by Redux .then(({ selectedAddress }) => {
.catch() if (selectedAddress) {
.then(() => history.push(DEFAULT_ROUTE)) history.push(DEFAULT_ROUTE)
} else {
displayWarning('Error importing account.')
setSelectedAddress(firstAddress)
}
})
.catch(err => displayWarning(err))
} }

@ -22,7 +22,9 @@ class CreateAccountPage extends Component {
}), }),
}), }),
onClick: () => history.push(NEW_ACCOUNT_ROUTE), onClick: () => history.push(NEW_ACCOUNT_ROUTE),
}, 'Create'), }, [
this.context.t('create'),
]),
h('div.new-account__tabs__tab', { h('div.new-account__tabs__tab', {
className: classnames('new-account__tabs__tab', { className: classnames('new-account__tabs__tab', {
@ -31,14 +33,16 @@ class CreateAccountPage extends Component {
}), }),
}), }),
onClick: () => history.push(IMPORT_ACCOUNT_ROUTE), onClick: () => history.push(IMPORT_ACCOUNT_ROUTE),
}, 'Import'), }, [
this.context.t('import'),
]),
]) ])
} }
render () { render () {
return h('div.new-account', {}, [ return h('div.new-account', {}, [
h('div.new-account__header', [ h('div.new-account__header', [
h('div.new-account__title', 'New Account'), h('div.new-account__title', this.context.t('newAccount') ),
this.renderTabs(), this.renderTabs(),
]), ]),
h('div.new-account__form', [ h('div.new-account__form', [
@ -62,6 +66,11 @@ class CreateAccountPage extends Component {
CreateAccountPage.propTypes = { CreateAccountPage.propTypes = {
location: PropTypes.object, location: PropTypes.object,
history: PropTypes.object, history: PropTypes.object,
t: PropTypes.func,
}
CreateAccountPage.contextTypes = {
t: PropTypes.func,
} }
const mapStateToProps = state => ({ const mapStateToProps = state => ({

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

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

@ -14,8 +14,8 @@ class Config extends Component {
return h('div.settings__tabs', [ return h('div.settings__tabs', [
h(TabBar, { h(TabBar, {
tabs: [ tabs: [
{ content: 'Settings', key: SETTINGS_ROUTE }, { content: this.context.t('settings'), key: SETTINGS_ROUTE },
{ content: 'Info', key: INFO_ROUTE }, { content: this.context.t('info'), key: INFO_ROUTE },
], ],
isActive: key => matchPath(location.pathname, { path: key, exact: true }), isActive: key => matchPath(location.pathname, { path: key, exact: true }),
onSelect: key => history.push(key), onSelect: key => history.push(key),
@ -54,6 +54,11 @@ class Config extends Component {
Config.propTypes = { Config.propTypes = {
location: PropTypes.object, location: PropTypes.object,
history: PropTypes.object, history: PropTypes.object,
t: PropTypes.func,
}
Config.contextTypes = {
t: PropTypes.func,
} }
module.exports = Config module.exports = Config

@ -120,7 +120,7 @@ class UnlockPage extends Component {
> >
<TextField <TextField
id="password" id="password"
label="Password" label={this.context.t('password')}
type="password" type="password"
value={this.state.password} value={this.state.password}
onChange={event => this.handleInputChange(event)} onChange={event => this.handleInputChange(event)}

@ -11,7 +11,7 @@ const { conversionUtil } = require('../../conversion-util')
const SenderToRecipient = require('../sender-to-recipient') const SenderToRecipient = require('../sender-to-recipient')
const NetworkDisplay = require('../network-display') const NetworkDisplay = require('../network-display')
const { MIN_GAS_PRICE_HEX } = require('../send/send-constants') const { MIN_GAS_PRICE_HEX } = require('../send_/send.constants')
class ConfirmDeployContract extends Component { class ConfirmDeployContract extends Component {
constructor (props) { constructor (props) {

@ -17,22 +17,26 @@ const {
multiplyCurrencies, multiplyCurrencies,
} = require('../../conversion-util') } = require('../../conversion-util')
const { const {
getGasTotal, calcGasTotal,
isBalanceSufficient, isBalanceSufficient,
} = require('../send/send-utils') } = require('../send_/send.utils')
const GasFeeDisplay = require('../send/gas-fee-display-v2') const GasFeeDisplay = require('../send/gas-fee-display-v2')
const SenderToRecipient = require('../sender-to-recipient') const SenderToRecipient = require('../sender-to-recipient')
const NetworkDisplay = require('../network-display') const NetworkDisplay = require('../network-display')
const currencyFormatter = require('currency-formatter') const currencyFormatter = require('currency-formatter')
const currencies = require('currency-formatter/currencies') const currencies = require('currency-formatter/currencies')
const { MIN_GAS_PRICE_HEX } = require('../send/send-constants') const { MIN_GAS_PRICE_HEX } = require('../send_/send.constants')
const { SEND_ROUTE, DEFAULT_ROUTE } = require('../../routes') const { SEND_ROUTE, DEFAULT_ROUTE } = require('../../routes')
const { const {
ENVIRONMENT_TYPE_POPUP, ENVIRONMENT_TYPE_POPUP,
ENVIRONMENT_TYPE_NOTIFICATION, ENVIRONMENT_TYPE_NOTIFICATION,
} = require('../../../../app/scripts/lib/enums') } = require('../../../../app/scripts/lib/enums')
import {
updateSendErrors,
} from '../../ducks/send.duck'
ConfirmSendEther.contextTypes = { ConfirmSendEther.contextTypes = {
t: PropTypes.func, t: PropTypes.func,
} }
@ -109,7 +113,7 @@ function mapDispatchToProps (dispatch) {
})) }))
dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' })) dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' }))
}, },
updateSendErrors: error => dispatch(actions.updateSendErrors(error)), updateSendErrors: error => dispatch(updateSendErrors(error)),
} }
} }
@ -145,7 +149,7 @@ ConfirmSendEther.prototype.updateComponentSendErrors = function (prevProps) {
if (shouldUpdateBalanceSendErrors) { if (shouldUpdateBalanceSendErrors) {
const balanceIsSufficient = this.isBalanceSufficient(txMeta) const balanceIsSufficient = this.isBalanceSufficient(txMeta)
updateSendErrors({ updateSendErrors({
insufficientFunds: balanceIsSufficient ? false : this.context.t('insufficientFunds'), insufficientFunds: balanceIsSufficient ? false : 'insufficientFunds',
}) })
} }
@ -153,7 +157,7 @@ ConfirmSendEther.prototype.updateComponentSendErrors = function (prevProps) {
if (shouldUpdateSimulationSendError) { if (shouldUpdateSimulationSendError) {
updateSendErrors({ updateSendErrors({
simulationFails: !txMeta.simulationFails ? false : this.context.t('transactionError'), simulationFails: !txMeta.simulationFails ? false : 'transactionError',
}) })
} }
} }
@ -585,9 +589,9 @@ ConfirmSendEther.prototype.onSubmit = function (event) {
if (valid && this.verifyGasParams() && balanceIsSufficient) { if (valid && this.verifyGasParams() && balanceIsSufficient) {
this.props.sendTransaction(txMeta, event) this.props.sendTransaction(txMeta, event)
} else if (!balanceIsSufficient) { } else if (!balanceIsSufficient) {
updateSendErrors({ insufficientFunds: this.context.t('insufficientFunds') }) updateSendErrors({ insufficientFunds: 'insufficientFunds' })
} else { } else {
updateSendErrors({ invalidGasParams: this.context.t('invalidGasParams') }) updateSendErrors({ invalidGasParams: 'invalidGasParams' })
this.setState({ submitting: false }) this.setState({ submitting: false })
} }
} }
@ -612,7 +616,7 @@ ConfirmSendEther.prototype.isBalanceSufficient = function (txMeta) {
value: amount, value: amount,
}, },
} = txMeta } = txMeta
const gasTotal = getGasTotal(gas, gasPrice) const gasTotal = calcGasTotal(gas, gasPrice)
return isBalanceSufficient({ return isBalanceSufficient({
amount, amount,
@ -643,7 +647,7 @@ ConfirmSendEther.prototype.gatherTxMeta = function () {
const state = this.state const state = this.state
const txData = clone(state.txData) || clone(props.txData) const txData = clone(state.txData) || clone(props.txData)
const { gasPrice: sendGasPrice, gas: sendGasLimit } = props.send const { gasPrice: sendGasPrice, gasLimit: sendGasLimit } = props.send
const { const {
lastGasPrice, lastGasPrice,
txParams: { txParams: {

@ -21,9 +21,9 @@ const {
addCurrencies, addCurrencies,
} = require('../../conversion-util') } = require('../../conversion-util')
const { const {
getGasTotal, calcGasTotal,
isBalanceSufficient, isBalanceSufficient,
} = require('../send/send-utils') } = require('../send_/send.utils')
const { const {
calcTokenAmount, calcTokenAmount,
} = require('../../token-util') } = require('../../token-util')
@ -31,7 +31,7 @@ const classnames = require('classnames')
const currencyFormatter = require('currency-formatter') const currencyFormatter = require('currency-formatter')
const currencies = require('currency-formatter/currencies') const currencies = require('currency-formatter/currencies')
const { MIN_GAS_PRICE_HEX } = require('../send/send-constants') const { MIN_GAS_PRICE_HEX } = require('../send_/send.constants')
const { const {
getTokenExchangeRate, getTokenExchangeRate,
@ -40,6 +40,10 @@ const {
} = require('../../selectors') } = require('../../selectors')
const { SEND_ROUTE, DEFAULT_ROUTE } = require('../../routes') const { SEND_ROUTE, DEFAULT_ROUTE } = require('../../routes')
import {
updateSendErrors,
} from '../../ducks/send.duck'
const { const {
ENVIRONMENT_TYPE_POPUP, ENVIRONMENT_TYPE_POPUP,
ENVIRONMENT_TYPE_NOTIFICATION, ENVIRONMENT_TYPE_NOTIFICATION,
@ -109,7 +113,7 @@ function mapDispatchToProps (dispatch, ownProps) {
to, to,
amount: tokenAmountInHex, amount: tokenAmountInHex,
errors: { to: null, amount: null }, errors: { to: null, amount: null },
editingTransactionId: id, editingTransactionId: id && id.toString(),
token: ownProps.token, token: ownProps.token,
})) }))
dispatch(actions.showSendTokenPage()) dispatch(actions.showSendTokenPage())
@ -147,7 +151,7 @@ function mapDispatchToProps (dispatch, ownProps) {
})) }))
dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' })) dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' }))
}, },
updateSendErrors: error => dispatch(actions.updateSendErrors(error)), updateSendErrors: error => dispatch(updateSendErrors(error)),
} }
} }
@ -589,9 +593,9 @@ ConfirmSendToken.prototype.onSubmit = function (event) {
if (valid && this.verifyGasParams() && balanceIsSufficient) { if (valid && this.verifyGasParams() && balanceIsSufficient) {
this.props.sendTransaction(txMeta, event) this.props.sendTransaction(txMeta, event)
} else if (!balanceIsSufficient) { } else if (!balanceIsSufficient) {
updateSendErrors({ insufficientFunds: this.context.t('insufficientFunds') }) updateSendErrors({ insufficientFunds: 'insufficientFunds' })
} else { } else {
updateSendErrors({ invalidGasParams: this.context.t('invalidGasParams') }) updateSendErrors({ invalidGasParams: 'invalidGasParams' })
this.setState({ submitting: false }) this.setState({ submitting: false })
} }
} }
@ -607,7 +611,7 @@ ConfirmSendToken.prototype.isBalanceSufficient = function (txMeta) {
gasPrice, gasPrice,
}, },
} = txMeta } = txMeta
const gasTotal = getGasTotal(gas, gasPrice) const gasTotal = calcGasTotal(gas, gasPrice)
return isBalanceSufficient({ return isBalanceSufficient({
amount: '0', amount: '0',
@ -647,7 +651,7 @@ ConfirmSendToken.prototype.gatherTxMeta = function () {
const state = this.state const state = this.state
const txData = clone(state.txData) || clone(props.txData) const txData = clone(state.txData) || clone(props.txData)
const { gasPrice: sendGasPrice, gas: sendGasLimit } = props.send const { gasPrice: sendGasPrice, gasLimit: sendGasLimit } = props.send
const { const {
lastGasPrice, lastGasPrice,
txParams: { txParams: {

@ -1,74 +0,0 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const { checksumAddress } = require('../../util')
const Identicon = require('../identicon')
const CurrencyDisplay = require('./currency-display')
const { conversionRateSelector, getCurrentCurrency } = require('../../selectors')
inherits(AccountListItem, Component)
function AccountListItem () {
Component.call(this)
}
function mapStateToProps (state) {
return {
conversionRate: conversionRateSelector(state),
currentCurrency: getCurrentCurrency(state),
}
}
module.exports = connect(mapStateToProps)(AccountListItem)
AccountListItem.prototype.render = function () {
const {
className,
account,
handleClick,
icon = null,
conversionRate,
currentCurrency,
displayBalance = true,
displayAddress = false,
} = this.props
const { name, address, balance } = account || {}
return h('div.account-list-item', {
className,
onClick: () => handleClick({ name, address, balance }),
}, [
h('div.account-list-item__top-row', {}, [
h(
Identicon,
{
address,
diameter: 18,
className: 'account-list-item__identicon',
},
),
h('div.account-list-item__account-name', {}, name || address),
icon && h('div.account-list-item__icon', [icon]),
]),
displayAddress && name && h('div.account-list-item__account-address', checksumAddress(address)),
displayBalance && h(CurrencyDisplay, {
primaryCurrency: 'ETH',
convertedCurrency: currentCurrency,
value: balance,
conversionRate,
readOnly: true,
className: 'account-list-item__account-balances',
primaryBalanceClassName: 'account-list-item__account-primary-balance',
convertedBalanceClassName: 'account-list-item__account-secondary-balance',
}, name),
])
}

@ -1,10 +1,10 @@
const Component = require('react').Component const Component = require('react').Component
const h = require('react-hyperscript') const h = require('react-hyperscript')
const inherits = require('util').inherits const inherits = require('util').inherits
const CurrencyInput = require('../currency-input')
const { conversionUtil, multiplyCurrencies } = require('../../conversion-util') const { conversionUtil, multiplyCurrencies } = require('../../conversion-util')
const currencyFormatter = require('currency-formatter') const currencyFormatter = require('currency-formatter')
const currencies = require('currency-formatter/currencies') const currencies = require('currency-formatter/currencies')
const ethUtil = require('ethereumjs-util')
module.exports = CurrencyDisplay module.exports = CurrencyDisplay
@ -21,36 +21,52 @@ function toHexWei (value) {
}) })
} }
CurrencyDisplay.prototype.componentWillMount = function () {
this.setState({
valueToRender: this.getValueToRender(this.props),
})
}
CurrencyDisplay.prototype.componentWillReceiveProps = function (nextProps) {
const currentValueToRender = this.getValueToRender(this.props)
const newValueToRender = this.getValueToRender(nextProps)
if (currentValueToRender !== newValueToRender) {
this.setState({
valueToRender: newValueToRender,
})
}
}
CurrencyDisplay.prototype.getAmount = function (value) { CurrencyDisplay.prototype.getAmount = function (value) {
const { selectedToken } = this.props const { selectedToken } = this.props
const { decimals } = selectedToken || {} const { decimals } = selectedToken || {}
const multiplier = Math.pow(10, Number(decimals || 0)) const multiplier = Math.pow(10, Number(decimals || 0))
const sendAmount = multiplyCurrencies(value, multiplier, {toNumericBase: 'hex'}) const sendAmount = multiplyCurrencies(value || '0', multiplier, {toNumericBase: 'hex'})
return selectedToken return selectedToken
? sendAmount ? sendAmount
: toHexWei(value) : toHexWei(value)
} }
CurrencyDisplay.prototype.getValueToRender = function () { CurrencyDisplay.prototype.getValueToRender = function ({ selectedToken, conversionRate, value, readOnly }) {
const { selectedToken, conversionRate, value } = this.props if (value === '0x0') return readOnly ? '0' : ''
const { decimals, symbol } = selectedToken || {} const { decimals, symbol } = selectedToken || {}
const multiplier = Math.pow(10, Number(decimals || 0)) const multiplier = Math.pow(10, Number(decimals || 0))
return selectedToken return selectedToken
? conversionUtil(value, { ? conversionUtil(ethUtil.addHexPrefix(value), {
fromNumericBase: 'hex', fromNumericBase: 'hex',
toNumericBase: 'dec',
toCurrency: symbol, toCurrency: symbol,
conversionRate: multiplier, conversionRate: multiplier,
invertConversionRate: true, invertConversionRate: true,
}) })
: conversionUtil(value, { : conversionUtil(ethUtil.addHexPrefix(value), {
fromNumericBase: 'hex', fromNumericBase: 'hex',
toNumericBase: 'dec', toNumericBase: 'dec',
fromDenomination: 'WEI', fromDenomination: 'WEI',
numberOfDecimals: 6, numberOfDecimals: 9,
conversionRate, conversionRate,
}) })
} }
@ -76,6 +92,22 @@ CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValu
: convertedValue : convertedValue
} }
function removeLeadingZeroes (str) {
return str.replace(/^0*(?=\d)/, '')
}
CurrencyDisplay.prototype.handleChange = function (newVal) {
this.setState({ valueToRender: removeLeadingZeroes(newVal) })
this.props.onChange(this.getAmount(newVal))
}
CurrencyDisplay.prototype.getInputWidth = function (valueToRender, readOnly) {
const valueString = String(valueToRender)
const valueLength = valueString.length || 1
const decimalPointDeficit = valueString.match(/\./) ? -0.5 : 0
return (valueLength + decimalPointDeficit + 0.75) + 'ch'
}
CurrencyDisplay.prototype.render = function () { CurrencyDisplay.prototype.render = function () {
const { const {
className = 'currency-display', className = 'currency-display',
@ -85,10 +117,10 @@ CurrencyDisplay.prototype.render = function () {
convertedCurrency, convertedCurrency,
readOnly = false, readOnly = false,
inError = false, inError = false,
handleChange, onBlur,
} = this.props } = this.props
const { valueToRender } = this.state
const valueToRender = this.getValueToRender()
const convertedValueToRender = this.getConvertedValueToRender(valueToRender) const convertedValueToRender = this.getConvertedValueToRender(valueToRender)
return h('div', { return h('div', {
@ -96,24 +128,30 @@ CurrencyDisplay.prototype.render = function () {
style: { style: {
borderColor: inError ? 'red' : null, borderColor: inError ? 'red' : null,
}, },
onClick: () => this.currencyInput && this.currencyInput.focus(), onClick: () => {
this.currencyInput && this.currencyInput.focus()
},
}, [ }, [
h('div.currency-display__primary-row', [ h('div.currency-display__primary-row', [
h('div.currency-display__input-wrapper', [ h('div.currency-display__input-wrapper', [
h(readOnly ? 'input' : CurrencyInput, { h('input', {
className: primaryBalanceClassName, className: primaryBalanceClassName,
value: `${valueToRender}`, value: `${valueToRender}`,
placeholder: '0', placeholder: '0',
type: 'number',
readOnly, readOnly,
...(!readOnly ? { ...(!readOnly ? {
onInputChange: newValue => { onChange: e => this.handleChange(e.target.value),
handleChange(this.getAmount(newValue)) onBlur: () => onBlur(this.getAmount(valueToRender)),
},
inputRef: input => { this.currencyInput = input },
} : {}), } : {}),
ref: input => { this.currencyInput = input },
style: {
width: this.getInputWidth(valueToRender, readOnly),
},
min: 0,
}), }),
h('span.currency-display__currency-symbol', primaryCurrency), h('span.currency-display__currency-symbol', primaryCurrency),

@ -1,72 +0,0 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const AccountListItem = require('./account-list-item')
module.exports = FromDropdown
inherits(FromDropdown, Component)
function FromDropdown () {
Component.call(this)
}
FromDropdown.prototype.getListItemIcon = function (currentAccount, selectedAccount) {
const listItemIcon = h(`i.fa.fa-check.fa-lg`, { style: { color: '#02c9b1' } })
return currentAccount.address === selectedAccount.address
? listItemIcon
: null
}
FromDropdown.prototype.renderDropdown = function () {
const {
accounts,
selectedAccount,
closeDropdown,
onSelect,
} = this.props
return h('div', {}, [
h('div.send-v2__from-dropdown__close-area', {
onClick: closeDropdown,
}),
h('div.send-v2__from-dropdown__list', {}, [
...accounts.map(account => h(AccountListItem, {
className: 'account-list-item__dropdown',
account,
handleClick: () => {
onSelect(account)
closeDropdown()
},
icon: this.getListItemIcon(account, selectedAccount),
})),
]),
])
}
FromDropdown.prototype.render = function () {
const {
selectedAccount,
openDropdown,
dropdownOpen,
} = this.props
return h('div.send-v2__from-dropdown', {}, [
h(AccountListItem, {
account: selectedAccount,
handleClick: openDropdown,
icon: h(`i.fa.fa-caret-down.fa-lg`, { style: { color: '#dedede' } }),
}),
dropdownOpen && this.renderDropdown(),
])
}

@ -1,106 +0,0 @@
const Component = require('react').Component
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const inherits = require('util').inherits
const InputNumber = require('../input-number.js')
const connect = require('react-redux').connect
GasTooltip.contextTypes = {
t: PropTypes.func,
}
module.exports = connect()(GasTooltip)
inherits(GasTooltip, Component)
function GasTooltip () {
Component.call(this)
this.state = {
gasLimit: 0,
gasPrice: 0,
}
this.updateGasPrice = this.updateGasPrice.bind(this)
this.updateGasLimit = this.updateGasLimit.bind(this)
this.onClose = this.onClose.bind(this)
}
GasTooltip.prototype.componentWillMount = function () {
const { gasPrice = 0, gasLimit = 0} = this.props
this.setState({
gasPrice: parseInt(gasPrice, 16) / 1000000000,
gasLimit: parseInt(gasLimit, 16),
})
}
GasTooltip.prototype.updateGasPrice = function (newPrice) {
const { onFeeChange } = this.props
const { gasLimit } = this.state
this.setState({ gasPrice: newPrice })
onFeeChange({
gasLimit: gasLimit.toString(16),
gasPrice: (newPrice * 1000000000).toString(16),
})
}
GasTooltip.prototype.updateGasLimit = function (newLimit) {
const { onFeeChange } = this.props
const { gasPrice } = this.state
this.setState({ gasLimit: newLimit })
onFeeChange({
gasLimit: newLimit.toString(16),
gasPrice: (gasPrice * 1000000000).toString(16),
})
}
GasTooltip.prototype.onClose = function (e) {
e.stopPropagation()
this.props.onClose()
}
GasTooltip.prototype.render = function () {
const { gasPrice, gasLimit } = this.state
return h('div.gas-tooltip', {}, [
h('div.gas-tooltip-close-area', {
onClick: this.onClose,
}),
h('div.customize-gas-tooltip-container', {}, [
h('div.customize-gas-tooltip', {}, [
h('div.gas-tooltip-header.gas-tooltip-label', {}, ['Customize Gas']),
h('div.gas-tooltip-input-label', {}, [
h('span.gas-tooltip-label', {}, ['Gas Price']),
h('i.fa.fa-info-circle'),
]),
h(InputNumber, {
unitLabel: 'GWEI',
step: 1,
min: 0,
placeholder: '0',
value: gasPrice,
onChange: (newPrice) => this.updateGasPrice(newPrice),
}),
h('div.gas-tooltip-input-label', {
style: {
'marginTop': '81px',
},
}, [
h('span.gas-tooltip-label', {}, [this.context.t('gasLimit')]),
h('i.fa.fa-info-circle'),
]),
h(InputNumber, {
unitLabel: 'UNITS',
step: 1,
min: 0,
placeholder: '0',
value: gasLimit,
onChange: (newLimit) => this.updateGasLimit(newLimit),
}),
]),
h('div.gas-tooltip-arrow', {}),
]),
])
}

@ -1,33 +0,0 @@
// const Component = require('react').Component
// const h = require('react-hyperscript')
// const inherits = require('util').inherits
// const Identicon = require('../identicon')
// module.exports = MemoTextArea
// inherits(MemoTextArea, Component)
// function MemoTextArea () {
// Component.call(this)
// }
// MemoTextArea.prototype.render = function () {
// const { memo, identities, onChange } = this.props
// return h('div.send-v2__memo-text-area', [
// h('textarea.send-v2__memo-text-area__input', {
// placeholder: 'Optional',
// value: memo,
// onChange,
// // onBlur: () => {
// // this.setErrorsFor('memo')
// // },
// onFocus: event => {
// // this.clearErrorsFor('memo')
// },
// }),
// ])
// }

@ -1,78 +0,0 @@
const {
addCurrencies,
conversionUtil,
conversionGTE,
multiplyCurrencies,
} = require('../../conversion-util')
const {
calcTokenAmount,
} = require('../../token-util')
function isBalanceSufficient ({
amount = '0x0',
gasTotal = '0x0',
balance,
primaryCurrency,
amountConversionRate,
conversionRate,
}) {
const totalAmount = addCurrencies(amount, gasTotal, {
aBase: 16,
bBase: 16,
toNumericBase: 'hex',
})
const balanceIsSufficient = conversionGTE(
{
value: balance,
fromNumericBase: 'hex',
fromCurrency: primaryCurrency,
conversionRate,
},
{
value: totalAmount,
fromNumericBase: 'hex',
conversionRate: amountConversionRate || conversionRate,
fromCurrency: primaryCurrency,
},
)
return balanceIsSufficient
}
function isTokenBalanceSufficient ({
amount = '0x0',
tokenBalance,
decimals,
}) {
const amountInDec = conversionUtil(amount, {
fromNumericBase: 'hex',
})
const tokenBalanceIsSufficient = conversionGTE(
{
value: tokenBalance,
fromNumericBase: 'dec',
},
{
value: calcTokenAmount(amountInDec, decimals),
fromNumericBase: 'dec',
},
)
return tokenBalanceIsSufficient
}
function getGasTotal (gasLimit, gasPrice) {
return multiplyCurrencies(gasLimit, gasPrice, {
toNumericBase: 'hex',
multiplicandBase: 16,
multiplierBase: 16,
})
}
module.exports = {
getGasTotal,
isBalanceSufficient,
isTokenBalanceSufficient,
}

@ -1,89 +0,0 @@
const connect = require('react-redux').connect
const actions = require('../../actions')
const abi = require('ethereumjs-abi')
const SendEther = require('../../send-v2')
const { withRouter } = require('react-router-dom')
const { compose } = require('recompose')
const {
accountsWithSendEtherInfoSelector,
getCurrentAccountWithSendEtherInfo,
conversionRateSelector,
getSelectedToken,
getSelectedAddress,
getAddressBook,
getSendFrom,
getCurrentCurrency,
getSelectedTokenToFiatRate,
getSelectedTokenContract,
} = require('../../selectors')
module.exports = compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(SendEther)
function mapStateToProps (state) {
const fromAccounts = accountsWithSendEtherInfoSelector(state)
const selectedAddress = getSelectedAddress(state)
const selectedToken = getSelectedToken(state)
const conversionRate = conversionRateSelector(state)
let data
let primaryCurrency
let tokenToFiatRate
if (selectedToken) {
data = Array.prototype.map.call(
abi.rawEncode(['address', 'uint256'], [selectedAddress, '0x0']),
x => ('00' + x.toString(16)).slice(-2)
).join('')
primaryCurrency = selectedToken.symbol
tokenToFiatRate = getSelectedTokenToFiatRate(state)
}
return {
...state.metamask.send,
from: getSendFrom(state) || getCurrentAccountWithSendEtherInfo(state),
fromAccounts,
toAccounts: [...fromAccounts, ...getAddressBook(state)],
conversionRate,
selectedToken,
primaryCurrency,
convertedCurrency: getCurrentCurrency(state),
data,
selectedAddress,
amountConversionRate: selectedToken ? tokenToFiatRate : conversionRate,
tokenContract: getSelectedTokenContract(state),
unapprovedTxs: state.metamask.unapprovedTxs,
network: state.metamask.network,
}
}
function mapDispatchToProps (dispatch) {
return {
showCustomizeGasModal: () => dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' })),
estimateGas: params => dispatch(actions.estimateGas(params)),
getGasPrice: () => dispatch(actions.getGasPrice()),
signTokenTx: (tokenAddress, toAddress, amount, txData) => (
dispatch(actions.signTokenTx(tokenAddress, toAddress, amount, txData))
),
signTx: txParams => dispatch(actions.signTx(txParams)),
updateAndApproveTx: txParams => dispatch(actions.updateAndApproveTx(txParams)),
updateTx: txData => dispatch(actions.updateTransaction(txData)),
setSelectedAddress: address => dispatch(actions.setSelectedAddress(address)),
addToAddressBook: (address, nickname) => dispatch(actions.addToAddressBook(address, nickname)),
updateGasTotal: newTotal => dispatch(actions.updateGasTotal(newTotal)),
updateGasPrice: newGasPrice => dispatch(actions.updateGasPrice(newGasPrice)),
updateGasLimit: newGasLimit => dispatch(actions.updateGasLimit(newGasLimit)),
updateSendTokenBalance: tokenBalance => dispatch(actions.updateSendTokenBalance(tokenBalance)),
updateSendFrom: newFrom => dispatch(actions.updateSendFrom(newFrom)),
updateSendTo: (newTo, nickname) => dispatch(actions.updateSendTo(newTo, nickname)),
updateSendAmount: newAmount => dispatch(actions.updateSendAmount(newAmount)),
updateSendMemo: newMemo => dispatch(actions.updateSendMemo(newMemo)),
updateSendErrors: newError => dispatch(actions.updateSendErrors(newError)),
clearSend: () => dispatch(actions.clearSend()),
setMaxModeTo: bool => dispatch(actions.setMaxModeTo(bool)),
}
}

@ -2,7 +2,7 @@ const Component = require('react').Component
const PropTypes = require('prop-types') const PropTypes = require('prop-types')
const h = require('react-hyperscript') const h = require('react-hyperscript')
const inherits = require('util').inherits const inherits = require('util').inherits
const AccountListItem = require('./account-list-item') const AccountListItem = require('../send_/account-list-item/account-list-item.component').default
const connect = require('react-redux').connect const connect = require('react-redux').connect
ToAutoComplete.contextTypes = { ToAutoComplete.contextTypes = {

@ -0,0 +1,74 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { checksumAddress } from '../../../util'
import Identicon from '../../identicon'
import CurrencyDisplay from '../../send/currency-display'
export default class AccountListItem extends Component {
static propTypes = {
account: PropTypes.object,
className: PropTypes.string,
conversionRate: PropTypes.number,
currentCurrency: PropTypes.string,
displayAddress: PropTypes.bool,
displayBalance: PropTypes.bool,
handleClick: PropTypes.func,
icon: PropTypes.node,
};
render () {
const {
account,
className,
conversionRate,
currentCurrency,
displayAddress = false,
displayBalance = true,
handleClick,
icon = null,
} = this.props
const { name, address, balance } = account || {}
return (<div
className={`account-list-item ${className}`}
onClick={() => handleClick({ name, address, balance })}
>
<div className="account-list-item__top-row">
<Identicon
address={address}
className="account-list-item__identicon"
diameter={18}
/>
<div className="account-list-item__account-name">{ name || address }</div>
{icon && <div className="account-list-item__icon">{ icon }</div>}
</div>
{displayAddress && name && <div className="account-list-item__account-address">
{ checksumAddress(address) }
</div>}
{displayBalance && <CurrencyDisplay
className="account-list-item__account-balances"
conversionRate={conversionRate}
convertedBalanceClassName="account-list-item__account-secondary-balance"
convertedCurrency={currentCurrency}
primaryBalanceClassName="account-list-item__account-primary-balance"
primaryCurrency="ETH"
readOnly={true}
value={balance}
/>}
</div>)
}
}
AccountListItem.contextTypes = {
t: PropTypes.func,
}

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

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save