Merge branch 'develop' into save-brave

feature/default_network_editable
Jenny Pollack 7 years ago
commit e7c2710a55
  1. 75
      .circleci/config.yml
  2. 6
      .circleci/scripts/firefox-download.sh
  3. 8
      .circleci/scripts/firefox-install.sh
  4. 1
      .eslintrc
  5. 14
      CHANGELOG.md
  6. 7
      app/_locales/en/messages.json
  7. 43
      app/_locales/ja/messages.json
  8. 2
      app/manifest.json
  9. 1
      app/scripts/contentscript.js
  10. 11
      app/scripts/controllers/network/network.js
  11. 1
      app/scripts/controllers/preferences.js
  12. 6
      app/scripts/controllers/transactions/index.js
  13. 24
      app/scripts/controllers/transactions/lib/recipient-blacklist-checker.js
  14. 14
      app/scripts/controllers/transactions/lib/recipient-blacklist-config.json
  15. 19
      app/scripts/metamask-controller.js
  16. 7
      development/states/confirm-new-ui.json
  17. 7
      development/states/send-edit.json
  18. 7
      development/states/send-new-ui.json
  19. 7
      development/states/tx-list-items.json
  20. 234
      gentests.js
  21. 50
      package-lock.json
  22. 3
      package.json
  23. 113
      test/e2e/beta/from-import-beta-ui.spec.js
  24. 11
      test/e2e/beta/helpers.js
  25. 145
      test/e2e/beta/metamask-beta-ui.spec.js
  26. 8
      test/integration/lib/send-new-ui.js
  27. 3
      test/unit/app/controllers/metamask-controller-test.js
  28. 77
      test/unit/app/controllers/transactions/recipient-blacklist-checker-test.js
  29. 17
      test/unit/app/controllers/transactions/tx-controller-test.js
  30. 143
      ui/app/actions.js
  31. 11
      ui/app/app.js
  32. 113
      ui/app/components/currency-input.js
  33. 28
      ui/app/components/customize-gas-modal/index.js
  34. 2
      ui/app/components/dropdowns/account-dropdown-mini.js
  35. 8
      ui/app/components/input-number.js
  36. 1
      ui/app/components/page-container/index.js
  37. 18
      ui/app/components/page-container/page-container-content.component.js
  38. 1
      ui/app/components/page-container/page-container-footer/index.js
  39. 54
      ui/app/components/page-container/page-container-footer/page-container-footer.component.js
  40. 0
      ui/app/components/page-container/page-container-footer/tests/page-container-footer.component.test.js
  41. 35
      ui/app/components/page-container/page-container-header.component.js
  42. 1
      ui/app/components/page-container/page-container-header/index.js
  43. 57
      ui/app/components/page-container/page-container-header/page-container-header.component.js
  44. 0
      ui/app/components/page-container/page-container-header/tests/page-container-header.component.test.js
  45. 72
      ui/app/components/page-container/page-container.component.js
  46. 0
      ui/app/components/page-container/tests/page-container.component.test.js
  47. 6
      ui/app/components/pages/add-token/add-token.component.js
  48. 24
      ui/app/components/pages/create-account/import-account/json.js
  49. 25
      ui/app/components/pages/create-account/import-account/private-key.js
  50. 15
      ui/app/components/pages/create-account/index.js
  51. 9
      ui/app/components/pages/settings/index.js
  52. 2
      ui/app/components/pages/unlock-page/unlock-page.component.js
  53. 2
      ui/app/components/pending-tx/confirm-deploy-contract.js
  54. 22
      ui/app/components/pending-tx/confirm-send-ether.js
  55. 20
      ui/app/components/pending-tx/confirm-send-token.js
  56. 74
      ui/app/components/send/account-list-item.js
  57. 65
      ui/app/components/send/currency-display.js
  58. 72
      ui/app/components/send/from-dropdown.js
  59. 106
      ui/app/components/send/gas-tooltip.js
  60. 33
      ui/app/components/send/memo-textarea.js
  61. 78
      ui/app/components/send/send-utils.js
  62. 89
      ui/app/components/send/send-v2-container.js
  63. 2
      ui/app/components/send/to-autocomplete.js
  64. 0
      ui/app/components/send_/README.md
  65. 0
      ui/app/components/send_/account-list-item/account-list-item-README.md
  66. 74
      ui/app/components/send_/account-list-item/account-list-item.component.js
  67. 15
      ui/app/components/send_/account-list-item/account-list-item.container.js
  68. 0
      ui/app/components/send_/account-list-item/account-list-item.scss
  69. 1
      ui/app/components/send_/account-list-item/index.js
  70. 138
      ui/app/components/send_/account-list-item/tests/account-list-item-component.test.js
  71. 32
      ui/app/components/send_/account-list-item/tests/account-list-item-container.test.js
  72. 1
      ui/app/components/send_/index.js
  73. 1
      ui/app/components/send_/send-content/index.js
  74. 0
      ui/app/components/send_/send-content/send-amount-row/README.md
  75. 54
      ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.component.js
  76. 40
      ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.container.js
  77. 9
      ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js
  78. 22
      ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js
  79. 1
      ui/app/components/send_/send-content/send-amount-row/amount-max-button/index.js
  80. 90
      ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js
  81. 91
      ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js
  82. 22
      ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js
  83. 27
      ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js
  84. 1
      ui/app/components/send_/send-content/send-amount-row/index.js
  85. 96
      ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js
  86. 51
      ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js
  87. 0
      ui/app/components/send_/send-content/send-amount-row/send-amount-row.scss
  88. 9
      ui/app/components/send_/send-content/send-amount-row/send-amount-row.selectors.js
  89. 164
      ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js
  90. 109
      ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-container.test.js
  91. 34
      ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-selectors.test.js
  92. 0
      ui/app/components/send_/send-content/send-content-README.md
  93. 28
      ui/app/components/send_/send-content/send-content.component.js
  94. 0
      ui/app/components/send_/send-content/send-content.scss
  95. 1
      ui/app/components/send_/send-content/send-dropdown-list/index.js
  96. 52
      ui/app/components/send_/send-content/send-dropdown-list/send-dropdown-list.component.js
  97. 105
      ui/app/components/send_/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js
  98. 0
      ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown-README.md
  99. 46
      ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.component.js
  100. 0
      ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.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,7 +2,19 @@
## Current Master ## Current Master
- Fixes issue where old nicknames were kept around causing errors. - Fix bug where account reset did not work with custom RPC providers.
## 4.7.4 Tue Jun 05 2018
- Add diagnostic reporting for users with multiple HD keyrings
- Throw explicit error when selected account is unset
## 4.7.3 Mon Jun 04 2018
- Hide token now uses new modal
- Indicate the current selected account on the popup account view
- Reduce height of notice container in onboarding
- Fixes issue where old nicknames were kept around causing errors
## 4.7.2 Sun Jun 03 2018 ## 4.7.2 Sun Jun 03 2018

@ -253,6 +253,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 +774,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"
}, },

@ -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.2", "version": "4.7.4",
"manifest_version": 2, "manifest_version": 2,
"author": "https://metamask.io", "author": "https://metamask.io",
"description": "__MSG_appDescription__", "description": "__MSG_appDescription__",

@ -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,8 +158,11 @@ 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) console.log(error)

@ -0,0 +1,24 @@
const Config = require('./recipient-blacklist-config.json')
/** @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,14 @@
{
"blacklist": [
"0x627306090abab3a6e1400e9345bc60c78a8bef57",
"0xf17f52151ebef6c7334fad080c5704d77216b732",
"0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef",
"0x821aea9a577a9b44299b9c15c88cf3087f3b5544",
"0x0d1d4e623d10f9fba5db95830f7d3839406c6af2",
"0x2932b7a2355d6fecc4b5c0b6bd44cc31df247a2e",
"0x2191ef87e392377ec08e7c08eb105ef5448eced5",
"0x0f4f2ac550a1b4e2280d04c21cea7ebd822934b5",
"0x6330a553fc93768f612722bb8c2ec78ac90b3bbc",
"0x5aeda56215b167893e80b4fe645ba6d5bab767de"
]
}

@ -394,6 +394,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),
@ -628,10 +630,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 +957,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
//============================================================================= //=============================================================================

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

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

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

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

50
package-lock.json generated

@ -9597,6 +9597,16 @@
"integrity": "sha512-ZH7loueKBoDb7yG9esn1U+fgq7BzlzW6NRi5/rMdxIZ05dj7GFD/Xc5rq2CDt5Yq86CyfSYVyx4242QQNZbx1g==", "integrity": "sha512-ZH7loueKBoDb7yG9esn1U+fgq7BzlzW6NRi5/rMdxIZ05dj7GFD/Xc5rq2CDt5Yq86CyfSYVyx4242QQNZbx1g==",
"dev": true "dev": true
}, },
"fill-keys": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz",
"integrity": "sha1-mo+jb06K1jTjv2tPPIiCVRRS6yA=",
"dev": true,
"requires": {
"is-object": "1.0.1",
"merge-descriptors": "1.0.1"
}
},
"fill-range": { "fill-range": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz",
@ -18712,6 +18722,12 @@
} }
} }
}, },
"module-not-found-error": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz",
"integrity": "sha1-z4tP9PKWQGdNbN0CsOO8UjwrvcA=",
"dev": true
},
"moment": { "moment": {
"version": "2.22.1", "version": "2.22.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.22.1.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.1.tgz",
@ -24501,6 +24517,28 @@
"integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=", "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=",
"dev": true "dev": true
}, },
"proxyquire": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.0.1.tgz",
"integrity": "sha512-fQr3VQrbdzHrdaDn3XuisVoJlJNDJizHAvUXw9IuXRR8BpV2x0N7LsCxrpJkeKfPbNjiNU/V5vc008cI0TmzzQ==",
"dev": true,
"requires": {
"fill-keys": "1.0.2",
"module-not-found-error": "1.0.1",
"resolve": "1.5.0"
},
"dependencies": {
"resolve": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz",
"integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==",
"dev": true,
"requires": {
"path-parse": "1.0.5"
}
}
}
},
"prr": { "prr": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
@ -26474,6 +26512,18 @@
"object-assign": "4.1.1" "object-assign": "4.1.1"
} }
}, },
"react": {
"version": "15.6.2",
"resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz",
"integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=",
"requires": {
"create-react-class": "15.6.2",
"fbjs": "0.8.16",
"loose-envify": "1.3.1",
"object-assign": "4.1.1",
"prop-types": "15.6.1"
}
},
"react-hyperscript": { "react-hyperscript": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/react-hyperscript/-/react-hyperscript-2.4.2.tgz", "resolved": "https://registry.npmjs.org/react-hyperscript/-/react-hyperscript-2.4.2.tgz",

@ -9,7 +9,7 @@
"dist": "gulp dist", "dist": "gulp dist",
"doc": "jsdoc -c development/tools/.jsdoc.json", "doc": "jsdoc -c development/tools/.jsdoc.json",
"test": "npm run test:unit && npm run test:integration && npm run lint", "test": "npm run test:unit && npm run test:integration && npm run lint",
"test:unit": "cross-env METAMASK_ENV=test mocha --exit --require test/setup.js --recursive \"test/unit/**/*.js\" && dot-only-hunter", "test:unit": "cross-env METAMASK_ENV=test mocha --exit --require test/setup.js --recursive \"test/unit/**/*.js\" \"ui/app/**/*.test.js\" && dot-only-hunter",
"test:single": "cross-env METAMASK_ENV=test mocha --require test/helper.js", "test:single": "cross-env METAMASK_ENV=test mocha --require test/helper.js",
"test:integration": "npm run test:integration:build && npm run test:flat && npm run test:mascara", "test:integration": "npm run test:integration:build && npm run test:flat && npm run test:mascara",
"test:integration:build": "gulp build:scss", "test:integration:build": "gulp build:scss",
@ -276,6 +276,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,36 +116,36 @@ 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 privacy notice', async () => { it('clicks through the privacy notice', async () => {
const [nextScreen] = await driver.findElements(By.css('.tou button')) const [nextScreen] = await findElements(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() 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 element = await driver.findElement(By.linkText('Attributions')) const element = await findElement(driver, By.linkText('Attributions'))
await driver.executeScript('arguments[0].scrollIntoView(true)', element) await driver.executeScript('arguments[0].scrollIntoView(true)', element)
await delay(regularDelayMs) await delay(regularDelayMs)
const [acceptTos] = await driver.findElements(By.css('.tou button')) const acceptTos = await findElement(driver, By.xpath(`//button[contains(text(), 'Accept')]`))
await acceptTos.click() await acceptTos.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
@ -147,11 +153,12 @@ describe('Using MetaMask with an existing account', function () {
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 +168,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 +201,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 +228,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 +236,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 +285,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 +293,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 +310,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 +353,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 +368,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 +383,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,23 +123,23 @@ 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 privacy notice', async () => {
const [nextScreen] = await driver.findElements(By.css('.tou button')) 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() 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 driver.findElements(By.css('.tou button')) const acceptTos = await findElement(driver, By.css('.tou button'))
await acceptTos.click() await acceptTos.click()
await delay(regularDelayMs) await delay(regularDelayMs)
}) })
@ -142,7 +147,7 @@ describe('MetaMask', function () {
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 +155,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 +163,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 +241,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 +259,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 +286,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 +309,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 +318,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 +367,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 +375,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 +392,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 +436,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 +451,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 +466,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)

@ -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,7 +117,7 @@ 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.000198264',
'send gas field should show estimated gas total' 'send gas field should show estimated gas total'
) )
assert.equal( assert.equal(
@ -127,7 +127,7 @@ async function runSendFlowTest(assert, done) {
) )
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])

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

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

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

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

@ -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 => ({

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

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

@ -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,51 @@ 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',
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 +91,18 @@ CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValu
: convertedValue : convertedValue
} }
CurrencyDisplay.prototype.handleChange = function (newVal) {
this.setState({ valueToRender: 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 +112,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 +123,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),
}
}

@ -0,0 +1 @@
export { default } from './account-list-item.container'

@ -0,0 +1,138 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import sinon from 'sinon'
import proxyquire from 'proxyquire'
import Identicon from '../../../identicon'
import CurrencyDisplay from '../../../send/currency-display'
const utilsMethodStubs = {
checksumAddress: sinon.stub().returns('mockCheckSumAddress'),
}
const AccountListItem = proxyquire('../account-list-item.component.js', {
'../../../util': utilsMethodStubs,
}).default
const propsMethodSpies = {
handleClick: sinon.spy(),
}
describe('AccountListItem Component', function () {
let wrapper
beforeEach(() => {
wrapper = shallow(<AccountListItem
account={ { address: 'mockAddress', name: 'mockName', balance: 'mockBalance' } }
className={'mockClassName'}
conversionRate={4}
currentCurrency={'mockCurrentyCurrency'}
displayAddress={false}
displayBalance={false}
handleClick={propsMethodSpies.handleClick}
icon={<i className="mockIcon" />}
/>, { context: { t: str => str + '_t' } })
})
afterEach(() => {
propsMethodSpies.handleClick.resetHistory()
})
describe('render', () => {
it('should render a div with the passed className', () => {
assert.equal(wrapper.find('.mockClassName').length, 1)
assert(wrapper.find('.mockClassName').is('div'))
assert(wrapper.find('.mockClassName').hasClass('account-list-item'))
})
it('should call handleClick with the expected props when the root div is clicked', () => {
const { onClick } = wrapper.find('.mockClassName').props()
assert.equal(propsMethodSpies.handleClick.callCount, 0)
onClick()
assert.equal(propsMethodSpies.handleClick.callCount, 1)
assert.deepEqual(
propsMethodSpies.handleClick.getCall(0).args,
[{ address: 'mockAddress', name: 'mockName', balance: 'mockBalance' }]
)
})
it('should have a top row div', () => {
assert.equal(wrapper.find('.mockClassName > .account-list-item__top-row').length, 1)
assert(wrapper.find('.mockClassName > .account-list-item__top-row').is('div'))
})
it('should have an identicon, name and icon in the top row', () => {
const topRow = wrapper.find('.mockClassName > .account-list-item__top-row')
assert.equal(topRow.find(Identicon).length, 1)
assert.equal(topRow.find('.account-list-item__account-name').length, 1)
assert.equal(topRow.find('.account-list-item__icon').length, 1)
})
it('should show the account name if it exists', () => {
const topRow = wrapper.find('.mockClassName > .account-list-item__top-row')
assert.equal(topRow.find('.account-list-item__account-name').text(), 'mockName')
})
it('should show the account address if there is no name', () => {
wrapper.setProps({ account: { address: 'addressButNoName' } })
const topRow = wrapper.find('.mockClassName > .account-list-item__top-row')
assert.equal(topRow.find('.account-list-item__account-name').text(), 'addressButNoName')
})
it('should render the passed icon', () => {
const topRow = wrapper.find('.mockClassName > .account-list-item__top-row')
assert(topRow.find('.account-list-item__icon').childAt(0).is('i'))
assert(topRow.find('.account-list-item__icon').childAt(0).hasClass('mockIcon'))
})
it('should not render an icon if none is passed', () => {
wrapper.setProps({ icon: null })
const topRow = wrapper.find('.mockClassName > .account-list-item__top-row')
assert.equal(topRow.find('.account-list-item__icon').length, 0)
})
it('should render the account address as a checksumAddress if displayAddress is true and name is provided', () => {
wrapper.setProps({ displayAddress: true })
assert.equal(wrapper.find('.account-list-item__account-address').length, 1)
assert.equal(wrapper.find('.account-list-item__account-address').text(), 'mockCheckSumAddress')
assert.deepEqual(
utilsMethodStubs.checksumAddress.getCall(0).args,
['mockAddress']
)
})
it('should not render the account address as a checksumAddress if displayAddress is false', () => {
wrapper.setProps({ displayAddress: false })
assert.equal(wrapper.find('.account-list-item__account-address').length, 0)
})
it('should not render the account address as a checksumAddress if name is not provided', () => {
wrapper.setProps({ account: { address: 'someAddressButNoName' } })
assert.equal(wrapper.find('.account-list-item__account-address').length, 0)
})
it('should render a CurrencyDisplay with the correct props if displayBalance is true', () => {
wrapper.setProps({ displayBalance: true })
assert.equal(wrapper.find(CurrencyDisplay).length, 1)
assert.deepEqual(
wrapper.find(CurrencyDisplay).props(),
{
className: 'account-list-item__account-balances',
conversionRate: 4,
convertedBalanceClassName: 'account-list-item__account-secondary-balance',
convertedCurrency: 'mockCurrentyCurrency',
primaryBalanceClassName: 'account-list-item__account-primary-balance',
primaryCurrency: 'ETH',
readOnly: true,
value: 'mockBalance',
}
)
})
it('should not render a CurrencyDisplay if displayBalance is false', () => {
wrapper.setProps({ displayBalance: false })
assert.equal(wrapper.find(CurrencyDisplay).length, 0)
})
})
})

@ -0,0 +1,32 @@
import assert from 'assert'
import proxyquire from 'proxyquire'
let mapStateToProps
proxyquire('../account-list-item.container.js', {
'react-redux': {
connect: (ms, md) => {
mapStateToProps = ms
return () => ({})
},
},
'../send.selectors.js': {
getConversionRate: (s) => `mockConversionRate:${s}`,
getConvertedCurrency: (s) => `mockCurrentCurrency:${s}`,
},
})
describe('account-list-item container', () => {
describe('mapStateToProps()', () => {
it('should map the correct properties to props', () => {
assert.deepEqual(mapStateToProps('mockState'), {
conversionRate: 'mockConversionRate:mockState',
currentCurrency: 'mockCurrentCurrency:mockState',
})
})
})
})

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

@ -0,0 +1 @@
export { default } from './send-content.component'

@ -0,0 +1,54 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class AmountMaxButton extends Component {
static propTypes = {
balance: PropTypes.string,
gasTotal: PropTypes.string,
maxModeOn: PropTypes.bool,
selectedToken: PropTypes.object,
setAmountToMax: PropTypes.func,
setMaxModeTo: PropTypes.func,
tokenBalance: PropTypes.string,
};
setMaxAmount () {
const {
balance,
gasTotal,
selectedToken,
setAmountToMax,
tokenBalance,
} = this.props
setAmountToMax({
balance,
gasTotal,
selectedToken,
tokenBalance,
})
}
render () {
const { setMaxModeTo, maxModeOn } = this.props
return (
<div
className="send-v2__amount-max"
onClick={(event) => {
event.preventDefault()
setMaxModeTo(true)
this.setMaxAmount()
}}
>
{!maxModeOn ? this.context.t('max') : ''}
</div>
)
}
}
AmountMaxButton.contextTypes = {
t: PropTypes.func,
}

@ -0,0 +1,40 @@
import { connect } from 'react-redux'
import {
getGasTotal,
getSelectedToken,
getSendFromBalance,
getTokenBalance,
} from '../../../send.selectors.js'
import { getMaxModeOn } from './amount-max-button.selectors.js'
import { calcMaxAmount } from './amount-max-button.utils.js'
import {
updateSendAmount,
setMaxModeTo,
} from '../../../../../actions'
import AmountMaxButton from './amount-max-button.component'
import {
updateSendErrors,
} from '../../../../../ducks/send.duck'
export default connect(mapStateToProps, mapDispatchToProps)(AmountMaxButton)
function mapStateToProps (state) {
return {
balance: getSendFromBalance(state),
gasTotal: getGasTotal(state),
maxModeOn: getMaxModeOn(state),
selectedToken: getSelectedToken(state),
tokenBalance: getTokenBalance(state),
}
}
function mapDispatchToProps (dispatch) {
return {
setAmountToMax: maxAmountDataObject => {
dispatch(updateSendErrors({ amount: null }))
dispatch(updateSendAmount(calcMaxAmount(maxAmountDataObject)))
},
setMaxModeTo: bool => dispatch(setMaxModeTo(bool)),
}
}

@ -0,0 +1,9 @@
const selectors = {
getMaxModeOn,
}
module.exports = selectors
function getMaxModeOn (state) {
return state.metamask.send.maxModeOn
}

@ -0,0 +1,22 @@
const {
multiplyCurrencies,
subtractCurrencies,
} = require('../../../../../conversion-util')
const ethUtil = require('ethereumjs-util')
function calcMaxAmount ({ balance, gasTotal, selectedToken, tokenBalance }) {
const { decimals } = selectedToken || {}
const multiplier = Math.pow(10, Number(decimals || 0))
return selectedToken
? multiplyCurrencies(tokenBalance, multiplier, {toNumericBase: 'hex'})
: subtractCurrencies(
ethUtil.addHexPrefix(balance),
ethUtil.addHexPrefix(gasTotal),
{ toNumericBase: 'hex' }
)
}
module.exports = {
calcMaxAmount,
}

@ -0,0 +1 @@
export { default } from './amount-max-button.container'

@ -0,0 +1,90 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import sinon from 'sinon'
import AmountMaxButton from '../amount-max-button.component.js'
const propsMethodSpies = {
setAmountToMax: sinon.spy(),
setMaxModeTo: sinon.spy(),
}
const MOCK_EVENT = { preventDefault: () => {} }
sinon.spy(AmountMaxButton.prototype, 'setMaxAmount')
describe('AmountMaxButton Component', function () {
let wrapper
let instance
beforeEach(() => {
wrapper = shallow(<AmountMaxButton
balance={'mockBalance'}
gasTotal={'mockGasTotal'}
maxModeOn={false}
selectedToken={ { address: 'mockTokenAddress' } }
setAmountToMax={propsMethodSpies.setAmountToMax}
setMaxModeTo={propsMethodSpies.setMaxModeTo}
tokenBalance={'mockTokenBalance'}
/>, { context: { t: str => str + '_t' } })
instance = wrapper.instance()
})
afterEach(() => {
propsMethodSpies.setAmountToMax.resetHistory()
propsMethodSpies.setMaxModeTo.resetHistory()
AmountMaxButton.prototype.setMaxAmount.resetHistory()
})
describe('setMaxAmount', () => {
it('should call setAmountToMax with the correct params', () => {
assert.equal(propsMethodSpies.setAmountToMax.callCount, 0)
instance.setMaxAmount()
assert.equal(propsMethodSpies.setAmountToMax.callCount, 1)
assert.deepEqual(
propsMethodSpies.setAmountToMax.getCall(0).args,
[{
balance: 'mockBalance',
gasTotal: 'mockGasTotal',
selectedToken: { address: 'mockTokenAddress' },
tokenBalance: 'mockTokenBalance',
}]
)
})
})
describe('render', () => {
it('should render a div with a send-v2__amount-max class', () => {
assert.equal(wrapper.find('.send-v2__amount-max').length, 1)
assert(wrapper.find('.send-v2__amount-max').is('div'))
})
it('should call setMaxModeTo and setMaxAmount when the send-v2__amount-max div is clicked', () => {
const {
onClick,
} = wrapper.find('.send-v2__amount-max').props()
assert.equal(AmountMaxButton.prototype.setMaxAmount.callCount, 0)
assert.equal(propsMethodSpies.setMaxModeTo.callCount, 0)
onClick(MOCK_EVENT)
assert.equal(AmountMaxButton.prototype.setMaxAmount.callCount, 1)
assert.equal(propsMethodSpies.setMaxModeTo.callCount, 1)
assert.deepEqual(
propsMethodSpies.setMaxModeTo.getCall(0).args,
[true]
)
})
it('should not render text when maxModeOn is true', () => {
wrapper.setProps({ maxModeOn: true })
assert.equal(wrapper.find('.send-v2__amount-max').text(), '')
})
it('should render the expected text when maxModeOn is false', () => {
wrapper.setProps({ maxModeOn: false })
assert.equal(wrapper.find('.send-v2__amount-max').text(), 'max_t')
})
})
})

@ -0,0 +1,91 @@
import assert from 'assert'
import proxyquire from 'proxyquire'
import sinon from 'sinon'
let mapStateToProps
let mapDispatchToProps
const actionSpies = {
setMaxModeTo: sinon.spy(),
updateSendAmount: sinon.spy(),
}
const duckActionSpies = {
updateSendErrors: sinon.spy(),
}
proxyquire('../amount-max-button.container.js', {
'react-redux': {
connect: (ms, md) => {
mapStateToProps = ms
mapDispatchToProps = md
return () => ({})
},
},
'../../../send.selectors.js': {
getGasTotal: (s) => `mockGasTotal:${s}`,
getSelectedToken: (s) => `mockSelectedToken:${s}`,
getSendFromBalance: (s) => `mockBalance:${s}`,
getTokenBalance: (s) => `mockTokenBalance:${s}`,
},
'./amount-max-button.selectors.js': { getMaxModeOn: (s) => `mockMaxModeOn:${s}` },
'./amount-max-button.utils.js': { calcMaxAmount: (mockObj) => mockObj.val + 1 },
'../../../../../actions': actionSpies,
'../../../../../ducks/send.duck': duckActionSpies,
})
describe('amount-max-button container', () => {
describe('mapStateToProps()', () => {
it('should map the correct properties to props', () => {
assert.deepEqual(mapStateToProps('mockState'), {
balance: 'mockBalance:mockState',
gasTotal: 'mockGasTotal:mockState',
maxModeOn: 'mockMaxModeOn:mockState',
selectedToken: 'mockSelectedToken:mockState',
tokenBalance: 'mockTokenBalance:mockState',
})
})
})
describe('mapDispatchToProps()', () => {
let dispatchSpy
let mapDispatchToPropsObject
beforeEach(() => {
dispatchSpy = sinon.spy()
mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
})
describe('setAmountToMax()', () => {
it('should dispatch an action', () => {
mapDispatchToPropsObject.setAmountToMax({ val: 11, foo: 'bar' })
assert(dispatchSpy.calledTwice)
assert(duckActionSpies.updateSendErrors.calledOnce)
assert.deepEqual(
duckActionSpies.updateSendErrors.getCall(0).args[0],
{ amount: null }
)
assert(actionSpies.updateSendAmount.calledOnce)
assert.equal(
actionSpies.updateSendAmount.getCall(0).args[0],
12
)
})
})
describe('setMaxModeTo()', () => {
it('should dispatch an action', () => {
mapDispatchToPropsObject.setMaxModeTo('mockVal')
assert(dispatchSpy.calledOnce)
assert.equal(
actionSpies.setMaxModeTo.getCall(0).args[0],
'mockVal'
)
})
})
})
})

@ -0,0 +1,22 @@
import assert from 'assert'
import {
getMaxModeOn,
} from '../amount-max-button.selectors.js'
describe('amount-max-button selectors', () => {
describe('getMaxModeOn()', () => {
it('should', () => {
const state = {
metamask: {
send: {
maxModeOn: null,
},
},
}
assert.equal(getMaxModeOn(state), null)
})
})
})

@ -0,0 +1,27 @@
import assert from 'assert'
import {
calcMaxAmount,
} from '../amount-max-button.utils.js'
describe('amount-max-button utils', () => {
describe('calcMaxAmount()', () => {
it('should calculate the correct amount when no selectedToken defined', () => {
assert.deepEqual(calcMaxAmount({
balance: 'ffffff',
gasTotal: 'ff',
selectedToken: false,
}), 'ffff00')
})
it('should calculate the correct amount when a selectedToken is defined', () => {
assert.deepEqual(calcMaxAmount({
selectedToken: {
decimals: 10,
},
tokenBalance: 100,
}), 'e8d4a51000')
})
})
})

@ -0,0 +1 @@
export { default } from './send-amount-row.container'

@ -0,0 +1,96 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import SendRowWrapper from '../send-row-wrapper/'
import AmountMaxButton from './amount-max-button/'
import CurrencyDisplay from '../../../send/currency-display'
export default class SendAmountRow extends Component {
static propTypes = {
amount: PropTypes.string,
amountConversionRate: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
balance: PropTypes.string,
conversionRate: PropTypes.number,
convertedCurrency: PropTypes.string,
gasTotal: PropTypes.string,
inError: PropTypes.bool,
primaryCurrency: PropTypes.string,
selectedToken: PropTypes.object,
setMaxModeTo: PropTypes.func,
tokenBalance: PropTypes.string,
updateSendAmount: PropTypes.func,
updateSendAmountError: PropTypes.func,
}
validateAmount (amount) {
const {
amountConversionRate,
balance,
conversionRate,
gasTotal,
primaryCurrency,
selectedToken,
tokenBalance,
updateSendAmountError,
} = this.props
updateSendAmountError({
amount,
amountConversionRate,
balance,
conversionRate,
gasTotal,
primaryCurrency,
selectedToken,
tokenBalance,
})
}
updateAmount (amount) {
const { updateSendAmount, setMaxModeTo } = this.props
setMaxModeTo(false)
updateSendAmount(amount)
}
render () {
const {
amount,
amountConversionRate,
convertedCurrency,
gasTotal,
inError,
primaryCurrency,
selectedToken,
} = this.props
return (
<SendRowWrapper
label={`${this.context.t('amount')}:`}
showError={inError}
errorType={'amount'}
>
{!inError && gasTotal && <AmountMaxButton />}
<CurrencyDisplay
conversionRate={amountConversionRate}
convertedCurrency={convertedCurrency}
onBlur={newAmount => this.updateAmount(newAmount)}
onChange={newAmount => this.validateAmount(newAmount)}
inError={inError}
primaryCurrency={primaryCurrency || 'ETH'}
selectedToken={selectedToken}
value={amount || '0x0'}
/>
</SendRowWrapper>
)
}
}
SendAmountRow.contextTypes = {
t: PropTypes.func,
}

@ -0,0 +1,51 @@
import { connect } from 'react-redux'
import {
getAmountConversionRate,
getConversionRate,
getConvertedCurrency,
getGasTotal,
getPrimaryCurrency,
getSelectedToken,
getSendAmount,
getSendFromBalance,
getTokenBalance,
} from '../../send.selectors'
import {
sendAmountIsInError,
} from './send-amount-row.selectors'
import { getAmountErrorObject } from '../../send.utils'
import {
setMaxModeTo,
updateSendAmount,
} from '../../../../actions'
import {
updateSendErrors,
} from '../../../../ducks/send.duck'
import SendAmountRow from './send-amount-row.component'
export default connect(mapStateToProps, mapDispatchToProps)(SendAmountRow)
function mapStateToProps (state) {
return {
amount: getSendAmount(state),
amountConversionRate: getAmountConversionRate(state),
balance: getSendFromBalance(state),
conversionRate: getConversionRate(state),
convertedCurrency: getConvertedCurrency(state),
gasTotal: getGasTotal(state),
inError: sendAmountIsInError(state),
primaryCurrency: getPrimaryCurrency(state),
selectedToken: getSelectedToken(state),
tokenBalance: getTokenBalance(state),
}
}
function mapDispatchToProps (dispatch) {
return {
setMaxModeTo: bool => dispatch(setMaxModeTo(bool)),
updateSendAmount: newAmount => dispatch(updateSendAmount(newAmount)),
updateSendAmountError: (amountDataObject) => {
dispatch(updateSendErrors(getAmountErrorObject(amountDataObject)))
},
}
}

@ -0,0 +1,9 @@
const selectors = {
sendAmountIsInError,
}
module.exports = selectors
function sendAmountIsInError (state) {
return Boolean(state.send.errors.amount)
}

@ -0,0 +1,164 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import sinon from 'sinon'
import SendAmountRow from '../send-amount-row.component.js'
import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component'
import AmountMaxButton from '../amount-max-button/amount-max-button.container'
import CurrencyDisplay from '../../../../send/currency-display'
const propsMethodSpies = {
setMaxModeTo: sinon.spy(),
updateSendAmount: sinon.spy(),
updateSendAmountError: sinon.spy(),
}
sinon.spy(SendAmountRow.prototype, 'updateAmount')
sinon.spy(SendAmountRow.prototype, 'validateAmount')
describe('SendAmountRow Component', function () {
let wrapper
let instance
beforeEach(() => {
wrapper = shallow(<SendAmountRow
amount={'mockAmount'}
amountConversionRate={'mockAmountConversionRate'}
balance={'mockBalance'}
conversionRate={7}
convertedCurrency={'mockConvertedCurrency'}
gasTotal={'mockGasTotal'}
inError={false}
primaryCurrency={'mockPrimaryCurrency'}
selectedToken={ { address: 'mockTokenAddress' } }
setMaxModeTo={propsMethodSpies.setMaxModeTo}
tokenBalance={'mockTokenBalance'}
updateSendAmount={propsMethodSpies.updateSendAmount}
updateSendAmountError={propsMethodSpies.updateSendAmountError}
/>, { context: { t: str => str + '_t' } })
instance = wrapper.instance()
})
afterEach(() => {
propsMethodSpies.setMaxModeTo.resetHistory()
propsMethodSpies.updateSendAmount.resetHistory()
propsMethodSpies.updateSendAmountError.resetHistory()
SendAmountRow.prototype.validateAmount.resetHistory()
SendAmountRow.prototype.updateAmount.resetHistory()
})
describe('validateAmount', () => {
it('should call updateSendAmountError with the correct params', () => {
assert.equal(propsMethodSpies.updateSendAmountError.callCount, 0)
instance.validateAmount('someAmount')
assert.equal(propsMethodSpies.updateSendAmountError.callCount, 1)
assert.deepEqual(
propsMethodSpies.updateSendAmountError.getCall(0).args,
[{
amount: 'someAmount',
amountConversionRate: 'mockAmountConversionRate',
balance: 'mockBalance',
conversionRate: 7,
gasTotal: 'mockGasTotal',
primaryCurrency: 'mockPrimaryCurrency',
selectedToken: { address: 'mockTokenAddress' },
tokenBalance: 'mockTokenBalance',
}]
)
})
})
describe('updateAmount', () => {
it('should call setMaxModeTo', () => {
assert.equal(propsMethodSpies.setMaxModeTo.callCount, 0)
instance.updateAmount('someAmount')
assert.equal(propsMethodSpies.setMaxModeTo.callCount, 1)
assert.deepEqual(
propsMethodSpies.setMaxModeTo.getCall(0).args,
[false]
)
})
it('should call updateSendAmount', () => {
assert.equal(propsMethodSpies.updateSendAmount.callCount, 0)
instance.updateAmount('someAmount')
assert.equal(propsMethodSpies.updateSendAmount.callCount, 1)
assert.deepEqual(
propsMethodSpies.updateSendAmount.getCall(0).args,
['someAmount']
)
})
})
describe('render', () => {
it('should render a SendRowWrapper component', () => {
assert.equal(wrapper.find(SendRowWrapper).length, 1)
})
it('should pass the correct props to SendRowWrapper', () => {
const {
errorType,
label,
showError,
} = wrapper.find(SendRowWrapper).props()
assert.equal(errorType, 'amount')
assert.equal(label, 'amount_t:')
assert.equal(showError, false)
})
it('should render an AmountMaxButton as the first child of the SendRowWrapper', () => {
assert(wrapper.find(SendRowWrapper).childAt(0).is(AmountMaxButton))
})
it('should render a CurrencyDisplay as the second child of the SendRowWrapper', () => {
assert(wrapper.find(SendRowWrapper).childAt(1).is(CurrencyDisplay))
})
it('should render the CurrencyDisplay with the correct props', () => {
const {
conversionRate,
convertedCurrency,
onBlur,
onChange,
inError,
primaryCurrency,
selectedToken,
value,
} = wrapper.find(SendRowWrapper).childAt(1).props()
assert.equal(conversionRate, 'mockAmountConversionRate')
assert.equal(convertedCurrency, 'mockConvertedCurrency')
assert.equal(inError, false)
assert.equal(primaryCurrency, 'mockPrimaryCurrency')
assert.deepEqual(selectedToken, { address: 'mockTokenAddress' })
assert.equal(value, 'mockAmount')
assert.equal(SendAmountRow.prototype.updateAmount.callCount, 0)
onBlur('mockNewAmount')
assert.equal(SendAmountRow.prototype.updateAmount.callCount, 1)
assert.deepEqual(
SendAmountRow.prototype.updateAmount.getCall(0).args,
['mockNewAmount']
)
assert.equal(SendAmountRow.prototype.validateAmount.callCount, 0)
onChange('mockNewAmount')
assert.equal(SendAmountRow.prototype.validateAmount.callCount, 1)
assert.deepEqual(
SendAmountRow.prototype.validateAmount.getCall(0).args,
['mockNewAmount']
)
})
it('should pass the default primaryCurrency to the CurrencyDisplay if primaryCurrency is falsy', () => {
wrapper.setProps({ primaryCurrency: null })
const { primaryCurrency } = wrapper.find(SendRowWrapper).childAt(1).props()
assert.equal(primaryCurrency, 'ETH')
})
})
})

@ -0,0 +1,109 @@
import assert from 'assert'
import proxyquire from 'proxyquire'
import sinon from 'sinon'
let mapStateToProps
let mapDispatchToProps
const actionSpies = {
setMaxModeTo: sinon.spy(),
updateSendAmount: sinon.spy(),
}
const duckActionSpies = {
updateSendErrors: sinon.spy(),
}
proxyquire('../send-amount-row.container.js', {
'react-redux': {
connect: (ms, md) => {
mapStateToProps = ms
mapDispatchToProps = md
return () => ({})
},
},
'../../send.selectors': {
getAmountConversionRate: (s) => `mockAmountConversionRate:${s}`,
getConversionRate: (s) => `mockConversionRate:${s}`,
getConvertedCurrency: (s) => `mockConvertedCurrency:${s}`,
getGasTotal: (s) => `mockGasTotal:${s}`,
getPrimaryCurrency: (s) => `mockPrimaryCurrency:${s}`,
getSelectedToken: (s) => `mockSelectedToken:${s}`,
getSendAmount: (s) => `mockAmount:${s}`,
getSendFromBalance: (s) => `mockBalance:${s}`,
getTokenBalance: (s) => `mockTokenBalance:${s}`,
},
'./send-amount-row.selectors': { sendAmountIsInError: (s) => `mockInError:${s}` },
'../../send.utils': { getAmountErrorObject: (mockDataObject) => ({ ...mockDataObject, mockChange: true }) },
'../../../../actions': actionSpies,
'../../../../ducks/send.duck': duckActionSpies,
})
describe('send-amount-row container', () => {
describe('mapStateToProps()', () => {
it('should map the correct properties to props', () => {
assert.deepEqual(mapStateToProps('mockState'), {
amount: 'mockAmount:mockState',
amountConversionRate: 'mockAmountConversionRate:mockState',
balance: 'mockBalance:mockState',
conversionRate: 'mockConversionRate:mockState',
convertedCurrency: 'mockConvertedCurrency:mockState',
gasTotal: 'mockGasTotal:mockState',
inError: 'mockInError:mockState',
primaryCurrency: 'mockPrimaryCurrency:mockState',
selectedToken: 'mockSelectedToken:mockState',
tokenBalance: 'mockTokenBalance:mockState',
})
})
})
describe('mapDispatchToProps()', () => {
let dispatchSpy
let mapDispatchToPropsObject
beforeEach(() => {
dispatchSpy = sinon.spy()
mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
})
describe('setMaxModeTo()', () => {
it('should dispatch an action', () => {
mapDispatchToPropsObject.setMaxModeTo('mockBool')
assert(dispatchSpy.calledOnce)
assert(actionSpies.setMaxModeTo.calledOnce)
assert.equal(
actionSpies.setMaxModeTo.getCall(0).args[0],
'mockBool'
)
})
})
describe('updateSendAmount()', () => {
it('should dispatch an action', () => {
mapDispatchToPropsObject.updateSendAmount('mockAmount')
assert(dispatchSpy.calledOnce)
assert(actionSpies.updateSendAmount.calledOnce)
assert.equal(
actionSpies.updateSendAmount.getCall(0).args[0],
'mockAmount'
)
})
})
describe('updateSendAmountError()', () => {
it('should dispatch an action', () => {
mapDispatchToPropsObject.updateSendAmountError({ some: 'data' })
assert(dispatchSpy.calledOnce)
assert(duckActionSpies.updateSendErrors.calledOnce)
assert.deepEqual(
duckActionSpies.updateSendErrors.getCall(0).args[0],
{ some: 'data', mockChange: true }
)
})
})
})
})

@ -0,0 +1,34 @@
import assert from 'assert'
import {
sendAmountIsInError,
} from '../send-amount-row.selectors.js'
describe('send-amount-row selectors', () => {
describe('sendAmountIsInError()', () => {
it('should return true if send.errors.amount is truthy', () => {
const state = {
send: {
errors: {
amount: 'abc',
},
},
}
assert.equal(sendAmountIsInError(state), true)
})
it('should return false if send.errors.amount is falsy', () => {
const state = {
send: {
errors: {
amount: null,
},
},
}
assert.equal(sendAmountIsInError(state), false)
})
})
})

@ -0,0 +1,28 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import PageContainerContent from '../../page-container/page-container-content.component'
import SendAmountRow from './send-amount-row/'
import SendFromRow from './send-from-row/'
import SendGasRow from './send-gas-row/'
import SendToRow from './send-to-row/'
export default class SendContent extends Component {
static propTypes = {
updateGas: PropTypes.func,
};
render () {
return (
<PageContainerContent>
<div className="send-v2__form">
<SendFromRow />
<SendToRow updateGas={(updateData) => this.props.updateGas(updateData)} />
<SendAmountRow />
<SendGasRow />
</div>
</PageContainerContent>
)
}
}

@ -0,0 +1 @@
export { default } from './send-dropdown-list.component'

@ -0,0 +1,52 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import AccountListItem from '../../account-list-item/'
export default class SendDropdownList extends Component {
static propTypes = {
accounts: PropTypes.array,
closeDropdown: PropTypes.func,
onSelect: PropTypes.func,
activeAddress: PropTypes.string,
};
getListItemIcon (accountAddress, activeAddress) {
return accountAddress === activeAddress
? <i className={`fa fa-check fa-lg`} style={ { color: '#02c9b1' } }/>
: null
}
render () {
const {
accounts,
closeDropdown,
onSelect,
activeAddress,
} = this.props
return (<div>
<div
className="send-v2__from-dropdown__close-area"
onClick={() => closeDropdown()}
/>
<div className="send-v2__from-dropdown__list">
{accounts.map((account, index) => <AccountListItem
account={account}
className="account-list-item__dropdown"
handleClick={() => {
onSelect(account)
closeDropdown()
}}
icon={this.getListItemIcon(account.address, activeAddress)}
key={`send-dropdown-account-#${index}`}
/>)}
</div>
</div>)
}
}
SendDropdownList.contextTypes = {
t: PropTypes.func,
}

@ -0,0 +1,105 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import sinon from 'sinon'
import SendDropdownList from '../send-dropdown-list.component.js'
import AccountListItem from '../../../account-list-item/account-list-item.container'
const propsMethodSpies = {
closeDropdown: sinon.spy(),
onSelect: sinon.spy(),
}
sinon.spy(SendDropdownList.prototype, 'getListItemIcon')
describe('SendDropdownList Component', function () {
let wrapper
beforeEach(() => {
wrapper = shallow(<SendDropdownList
accounts={[
{ address: 'mockAccount0' },
{ address: 'mockAccount1' },
{ address: 'mockAccount2' },
]}
closeDropdown={propsMethodSpies.closeDropdown}
onSelect={propsMethodSpies.onSelect}
activeAddress={'mockAddress2'}
/>, { context: { t: str => str + '_t' } })
})
afterEach(() => {
propsMethodSpies.closeDropdown.resetHistory()
propsMethodSpies.onSelect.resetHistory()
SendDropdownList.prototype.getListItemIcon.resetHistory()
})
describe('getListItemIcon', () => {
it('should return check icon if the passed addresses are the same', () => {
assert.deepEqual(
wrapper.instance().getListItemIcon('mockAccount0', 'mockAccount0'),
<i className={`fa fa-check fa-lg`} style={ { color: '#02c9b1' } }/>
)
})
it('should return null if the passed addresses are different', () => {
assert.equal(
wrapper.instance().getListItemIcon('mockAccount0', 'mockAccount1'),
null
)
})
})
describe('render', () => {
it('should render a single div with two children', () => {
assert(wrapper.is('div'))
assert.equal(wrapper.children().length, 2)
})
it('should render the children with the correct classes', () => {
assert(wrapper.childAt(0).hasClass('send-v2__from-dropdown__close-area'))
assert(wrapper.childAt(1).hasClass('send-v2__from-dropdown__list'))
})
it('should call closeDropdown onClick of the send-v2__from-dropdown__close-area', () => {
assert.equal(propsMethodSpies.closeDropdown.callCount, 0)
wrapper.childAt(0).props().onClick()
assert.equal(propsMethodSpies.closeDropdown.callCount, 1)
})
it('should render an AccountListItem for each item in accounts', () => {
assert.equal(wrapper.childAt(1).children().length, 3)
assert(wrapper.childAt(1).children().every(AccountListItem))
})
it('should pass the correct props to the AccountListItem', () => {
wrapper.childAt(1).children().forEach((accountListItem, index) => {
const {
account,
className,
handleClick,
} = accountListItem.props()
assert.deepEqual(account, { address: 'mockAccount' + index })
assert.equal(className, 'account-list-item__dropdown')
assert.equal(propsMethodSpies.onSelect.callCount, 0)
handleClick()
assert.equal(propsMethodSpies.onSelect.callCount, 1)
assert.deepEqual(propsMethodSpies.onSelect.getCall(0).args[0], { address: 'mockAccount' + index })
propsMethodSpies.onSelect.resetHistory()
propsMethodSpies.closeDropdown.resetHistory()
assert.equal(propsMethodSpies.closeDropdown.callCount, 0)
handleClick()
assert.equal(propsMethodSpies.closeDropdown.callCount, 1)
propsMethodSpies.onSelect.resetHistory()
propsMethodSpies.closeDropdown.resetHistory()
})
})
it('should call this.getListItemIcon for each AccountListItem', () => {
assert.equal(SendDropdownList.prototype.getListItemIcon.callCount, 3)
const getListItemIconCalls = SendDropdownList.prototype.getListItemIcon.getCalls()
assert(getListItemIconCalls.every(({ args }, index) => args[0] === 'mockAccount' + index))
})
})
})

@ -0,0 +1,46 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import AccountListItem from '../../../account-list-item/'
import SendDropdownList from '../../send-dropdown-list/'
export default class FromDropdown extends Component {
static propTypes = {
accounts: PropTypes.array,
closeDropdown: PropTypes.func,
dropdownOpen: PropTypes.bool,
onSelect: PropTypes.func,
openDropdown: PropTypes.func,
selectedAccount: PropTypes.object,
};
render () {
const {
accounts,
closeDropdown,
dropdownOpen,
openDropdown,
selectedAccount,
onSelect,
} = this.props
return <div className="send-v2__from-dropdown">
<AccountListItem
account={selectedAccount}
handleClick={openDropdown}
icon={<i className={`fa fa-caret-down fa-lg`} style={ { color: '#dedede' } }/>}
/>
{dropdownOpen && <SendDropdownList
accounts={accounts}
closeDropdown={closeDropdown}
onSelect={onSelect}
activeAddress={selectedAccount.address}
/>}
</div>
}
}
FromDropdown.contextTypes = {
t: PropTypes.func,
}

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

Loading…
Cancel
Save