aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.babelrc4
-rw-r--r--.circleci/config.yml67
-rwxr-xr-x.circleci/scripts/firefox-download.sh17
-rwxr-xr-x.circleci/scripts/firefox-install.sh4
-rw-r--r--.github/CODEOWNERS6
-rw-r--r--.gitignore4
-rw-r--r--.npmrc1
-rw-r--r--CHANGELOG.md7
-rw-r--r--CONTRIBUTING.md31
-rw-r--r--README.md10
-rw-r--r--USER_AGREEMENT.md2
-rw-r--r--app/404.html52
-rw-r--r--app/_locales/en/messages.json155
-rw-r--r--app/error.html79
-rw-r--r--app/images/404.pngbin0 -> 39401 bytes
-rw-r--r--app/images/alert-red.svg14
-rw-r--r--app/images/alert.svg19
-rw-r--r--app/images/cancel.pngbin0 -> 10928 bytes
-rw-r--r--app/images/caret-left.svg18
-rw-r--r--app/images/connect-icon.svg11
-rw-r--r--app/images/deadface.pngbin0 -> 8333 bytes
-rw-r--r--app/images/hardware-wallet-step-1.svg44
-rw-r--r--app/images/hardware-wallet-step-2.svg81
-rw-r--r--app/images/hardware-wallet-step-3.svg42
-rw-r--r--app/images/loginglogo.svg53
-rw-r--r--app/images/logo.pngbin0 -> 6589 bytes
-rw-r--r--app/images/pw-128x128.pngbin0 -> 94988 bytes
-rw-r--r--app/images/pw-48x48.pngbin0 -> 3279 bytes
-rw-r--r--app/images/pw128x128.pngbin0 -> 94988 bytes
-rw-r--r--app/loading.html35
-rw-r--r--app/manifest.json9
-rw-r--r--app/scripts/background.js14
-rw-r--r--app/scripts/contentscript.js3
-rw-r--r--app/scripts/controllers/detect-tokens.js130
-rw-r--r--app/scripts/controllers/network/enums.js3
-rw-r--r--app/scripts/controllers/preferences.js24
-rw-r--r--app/scripts/controllers/transactions/nonce-tracker.js13
-rw-r--r--app/scripts/controllers/transactions/tx-state-manager.js6
-rw-r--r--app/scripts/lib/account-tracker.js4
-rw-r--r--app/scripts/lib/contracts/registrar.js1
-rw-r--r--app/scripts/lib/contracts/resolver.js2
-rw-r--r--app/scripts/lib/ipfsContent.js44
-rw-r--r--app/scripts/lib/resolver.js71
-rw-r--r--app/scripts/lib/setupRaven.js8
-rw-r--r--app/scripts/lib/util.js2
-rw-r--r--app/scripts/metamask-controller.js170
-rw-r--r--app/scripts/migrations/027.js35
-rw-r--r--app/scripts/migrations/index.js1
-rw-r--r--app/scripts/platforms/extension.js67
-rw-r--r--app/scripts/ui.js1
-rw-r--r--app/unsupport.html59
-rw-r--r--development/backGroundConnectionModifiers.js2
-rw-r--r--development/states/conf-tx.json2
-rw-r--r--development/states/confirm-new-ui.json18
-rw-r--r--development/states/confirm-sig-requests.json37
-rw-r--r--development/states/currency-localization.json20
-rw-r--r--development/states/first-time.json22
-rw-r--r--development/states/send-edit.json18
-rw-r--r--development/states/send-new-ui.json18
-rw-r--r--docs/trezor-emulator.md25
-rw-r--r--notices/archive/notice_0.md3
-rw-r--r--old-ui/app/app.js1
-rw-r--r--old-ui/app/keychains/hd/restore-vault.js3
-rw-r--r--package-lock.json677
-rw-r--r--package.json32
-rw-r--r--test/e2e/beta/contract-test/contract.js120
-rw-r--r--test/e2e/beta/contract-test/index.html33
-rw-r--r--test/e2e/beta/from-import-beta-ui.spec.js47
-rw-r--r--test/e2e/beta/helpers.js90
-rw-r--r--test/e2e/beta/metamask-beta-ui.spec.js383
-rw-r--r--test/e2e/metamask.spec.js2
-rw-r--r--test/integration/lib/confirm-sig-requests.js12
-rw-r--r--test/integration/lib/currency-localization.js1
-rw-r--r--test/integration/lib/send-new-ui.js27
-rw-r--r--test/integration/lib/tx-list-items.js4
-rw-r--r--test/lib/migrations/002.json2
-rw-r--r--test/unit/actions/tx_test.js102
-rw-r--r--test/unit/app/controllers/detect-tokens-test.js141
-rw-r--r--test/unit/app/controllers/metamask-controller-test.js156
-rw-r--r--test/unit/app/controllers/preferences-controller-test.js25
-rw-r--r--test/unit/app/controllers/transactions/tx-controller-test.js13
-rw-r--r--test/unit/app/controllers/transactions/tx-state-manager-test.js19
-rw-r--r--test/unit/components/pending-tx-test.js67
-rw-r--r--test/unit/migrations/027-test.js50
-rw-r--r--ui/app/actions.js311
-rw-r--r--ui/app/app.js34
-rw-r--r--ui/app/components/account-menu/index.js69
-rw-r--r--ui/app/components/alert/index.js62
-rw-r--r--ui/app/components/app-header/app-header.component.js3
-rw-r--r--ui/app/components/button-group/button-group.component.js61
-rw-r--r--ui/app/components/button-group/button-group.stories.js49
-rw-r--r--ui/app/components/button-group/index.js1
-rw-r--r--ui/app/components/button-group/index.scss38
-rw-r--r--ui/app/components/button-group/tests/button-group-component.test.js97
-rw-r--r--ui/app/components/button/button.component.js21
-rw-r--r--ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js52
-rw-r--r--ui/app/components/confirm-page-container/confirm-detail-row/index.js1
-rw-r--r--ui/app/components/confirm-page-container/confirm-detail-row/index.scss43
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js105
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/confirm-page-container-error.component.js28
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.js1
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.scss17
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js56
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.js1
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss54
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js22
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.js1
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.scss18
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/index.js4
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-content/index.scss66
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js63
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-header/index.js1
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container-header/index.scss27
-rw-r--r--ui/app/components/confirm-page-container/confirm-page-container.component.js118
-rw-r--r--ui/app/components/confirm-page-container/index.js8
-rw-r--r--ui/app/components/confirm-page-container/index.scss5
-rw-r--r--ui/app/components/customize-gas-modal/index.js11
-rw-r--r--ui/app/components/dropdowns/account-dropdown-mini.js2
-rw-r--r--ui/app/components/dropdowns/components/network-dropdown-icon.js3
-rw-r--r--ui/app/components/ens-input.js2
-rw-r--r--ui/app/components/index.scss12
-rw-r--r--ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js93
-rw-r--r--ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js20
-rw-r--r--ui/app/components/modals/confirm-remove-account/index.js2
-rw-r--r--ui/app/components/modals/customize-gas/customize-gas.component.js140
-rw-r--r--ui/app/components/modals/customize-gas/customize-gas.container.js22
-rw-r--r--ui/app/components/modals/customize-gas/customize-gas.util.js34
-rw-r--r--ui/app/components/modals/customize-gas/index.js1
-rw-r--r--ui/app/components/modals/customize-gas/index.scss110
-rw-r--r--ui/app/components/modals/index.scss54
-rw-r--r--ui/app/components/modals/modal.js42
-rw-r--r--ui/app/components/network-display.js56
-rw-r--r--ui/app/components/network-display/index.js2
-rw-r--r--ui/app/components/network-display/index.scss54
-rw-r--r--ui/app/components/network-display/network-display.component.js69
-rw-r--r--ui/app/components/network-display/network-display.container.js11
-rw-r--r--ui/app/components/page-container/index.js3
-rw-r--r--ui/app/components/page-container/index.scss186
-rw-r--r--ui/app/components/page-container/page-container-footer/page-container-footer.component.js10
-rw-r--r--ui/app/components/page-container/page-container-header.component.js35
-rw-r--r--ui/app/components/page-container/page-container-header/page-container-header.component.js33
-rw-r--r--ui/app/components/pages/confirm-approve/confirm-approve.component.js21
-rw-r--r--ui/app/components/pages/confirm-approve/confirm-approve.container.js15
-rw-r--r--ui/app/components/pages/confirm-approve/index.js1
-rw-r--r--ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.component.js64
-rw-r--r--ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.container.js12
-rw-r--r--ui/app/components/pages/confirm-deploy-contract/index.js1
-rw-r--r--ui/app/components/pages/confirm-send-ether/confirm-send-ether.component.js39
-rw-r--r--ui/app/components/pages/confirm-send-ether/confirm-send-ether.container.js45
-rw-r--r--ui/app/components/pages/confirm-send-ether/index.js1
-rw-r--r--ui/app/components/pages/confirm-send-token/confirm-send-token.component.js29
-rw-r--r--ui/app/components/pages/confirm-send-token/confirm-send-token.container.js52
-rw-r--r--ui/app/components/pages/confirm-send-token/index.js1
-rw-r--r--ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js85
-rw-r--r--ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js34
-rw-r--r--ui/app/components/pages/confirm-token-transaction-base/index.js2
-rw-r--r--ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js330
-rw-r--r--ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js181
-rw-r--r--ui/app/components/pages/confirm-transaction-base/index.js1
-rw-r--r--ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js85
-rw-r--r--ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js3
-rw-r--r--ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js20
-rw-r--r--ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.util.js4
-rw-r--r--ui/app/components/pages/confirm-transaction-switch/index.js2
-rw-r--r--ui/app/components/pages/confirm-transaction/confirm-transaction.component.js157
-rw-r--r--ui/app/components/pages/confirm-transaction/confirm-transaction.container.js33
-rw-r--r--ui/app/components/pages/confirm-transaction/index.js2
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/account-list.js143
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/connect-screen.js149
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/index.js241
-rw-r--r--ui/app/components/pages/create-account/index.js25
-rw-r--r--ui/app/components/pages/create-account/new-account.js2
-rw-r--r--ui/app/components/pages/home.js108
-rw-r--r--ui/app/components/pages/keychains/restore-vault.js313
-rw-r--r--ui/app/components/pages/unlock-page/unlock-page.component.js72
-rw-r--r--ui/app/components/pending-tx/confirm-deploy-contract.js358
-rw-r--r--ui/app/components/pending-tx/confirm-send-ether.js692
-rw-r--r--ui/app/components/pending-tx/confirm-send-token.js696
-rw-r--r--ui/app/components/pending-tx/index.js165
-rw-r--r--ui/app/components/selected-account/selected-account.component.js11
-rw-r--r--ui/app/components/send/README.md (renamed from ui/app/components/send_/README.md)0
-rw-r--r--ui/app/components/send/account-list-item/account-list-item-README.md (renamed from ui/app/components/send_/account-list-item/account-list-item-README.md)0
-rw-r--r--ui/app/components/send/account-list-item/account-list-item.component.js (renamed from ui/app/components/send_/account-list-item/account-list-item.component.js)11
-rw-r--r--ui/app/components/send/account-list-item/account-list-item.container.js (renamed from ui/app/components/send_/account-list-item/account-list-item.container.js)0
-rw-r--r--ui/app/components/send/account-list-item/account-list-item.scss (renamed from ui/app/components/send_/account-list-item/account-list-item.scss)0
-rw-r--r--ui/app/components/send/account-list-item/index.js (renamed from ui/app/components/send_/account-list-item/index.js)0
-rw-r--r--ui/app/components/send/account-list-item/tests/account-list-item-component.test.js (renamed from ui/app/components/send_/account-list-item/tests/account-list-item-component.test.js)2
-rw-r--r--ui/app/components/send/account-list-item/tests/account-list-item-container.test.js (renamed from ui/app/components/send_/account-list-item/tests/account-list-item-container.test.js)0
-rw-r--r--ui/app/components/send/currency-display/currency-display.js (renamed from ui/app/components/send/currency-display.js)43
-rw-r--r--ui/app/components/send/currency-display/index.js1
-rw-r--r--ui/app/components/send/index.js (renamed from ui/app/components/send_/index.js)0
-rw-r--r--ui/app/components/send/send-content/index.js (renamed from ui/app/components/send_/send-content/index.js)0
-rw-r--r--ui/app/components/send/send-content/send-amount-row/README.md (renamed from ui/app/components/send_/send-content/send-amount-row/README.md)0
-rw-r--r--ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js (renamed from ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.component.js)8
-rw-r--r--ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js (renamed from ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.container.js)0
-rw-r--r--ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js (renamed from ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js)0
-rw-r--r--ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js (renamed from ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js)0
-rw-r--r--ui/app/components/send/send-content/send-amount-row/amount-max-button/index.js (renamed from ui/app/components/send_/send-content/send-amount-row/amount-max-button/index.js)0
-rw-r--r--ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js (renamed from ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js (renamed from ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js (renamed from ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js (renamed from ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-amount-row/index.js (renamed from ui/app/components/send_/send-content/send-amount-row/index.js)0
-rw-r--r--ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js (renamed from ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js)28
-rw-r--r--ui/app/components/send/send-content/send-amount-row/send-amount-row.container.js (renamed from ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js)5
-rw-r--r--ui/app/components/send/send-content/send-amount-row/send-amount-row.scss (renamed from ui/app/components/send_/send-content/send-amount-row/send-amount-row.scss)0
-rw-r--r--ui/app/components/send/send-content/send-amount-row/send-amount-row.selectors.js (renamed from ui/app/components/send_/send-content/send-amount-row/send-amount-row.selectors.js)0
-rw-r--r--ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js (renamed from ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js)31
-rw-r--r--ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-container.test.js (renamed from ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-container.test.js)18
-rw-r--r--ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js (renamed from ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-selectors.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-content-README.md (renamed from ui/app/components/send_/send-content/send-content-README.md)0
-rw-r--r--ui/app/components/send/send-content/send-content.component.js (renamed from ui/app/components/send_/send-content/send-content.component.js)2
-rw-r--r--ui/app/components/send/send-content/send-content.scss (renamed from ui/app/components/send_/send-content/send-content.scss)0
-rw-r--r--ui/app/components/send/send-content/send-dropdown-list/index.js (renamed from ui/app/components/send_/send-content/send-dropdown-list/index.js)0
-rw-r--r--ui/app/components/send/send-content/send-dropdown-list/send-dropdown-list.component.js (renamed from ui/app/components/send_/send-content/send-dropdown-list/send-dropdown-list.component.js)8
-rw-r--r--ui/app/components/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js (renamed from ui/app/components/send_/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown-README.md (renamed from ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown-README.md)0
-rw-r--r--ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.component.js (renamed from ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.component.js)8
-rw-r--r--ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.scss (renamed from ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.scss)0
-rw-r--r--ui/app/components/send/send-content/send-from-row/from-dropdown/index.js (renamed from ui/app/components/send_/send-content/send-from-row/from-dropdown/index.js)0
-rw-r--r--ui/app/components/send/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js (renamed from ui/app/components/send_/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-from-row/index.js (renamed from ui/app/components/send_/send-content/send-from-row/index.js)0
-rw-r--r--ui/app/components/send/send-content/send-from-row/send-from-row-README.md (renamed from ui/app/components/send_/send-content/send-from-row/send-from-row-README.md)0
-rw-r--r--ui/app/components/send/send-content/send-from-row/send-from-row.component.js (renamed from ui/app/components/send_/send-content/send-from-row/send-from-row.component.js)8
-rw-r--r--ui/app/components/send/send-content/send-from-row/send-from-row.container.js (renamed from ui/app/components/send_/send-content/send-from-row/send-from-row.container.js)0
-rw-r--r--ui/app/components/send/send-content/send-from-row/send-from-row.selectors.js (renamed from ui/app/components/send_/send-content/send-from-row/send-from-row.selectors.js)0
-rw-r--r--ui/app/components/send/send-content/send-from-row/tests/send-from-row-component.test.js (renamed from ui/app/components/send_/send-content/send-from-row/tests/send-from-row-component.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-from-row/tests/send-from-row-container.test.js (renamed from ui/app/components/send_/send-content/send-from-row/tests/send-from-row-container.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-from-row/tests/send-from-row-selectors.test.js (renamed from ui/app/components/send_/send-content/send-from-row/tests/send-from-row-selectors.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-gas-row/README.md (renamed from ui/app/components/send_/send-content/send-gas-row/README.md)0
-rw-r--r--ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js (renamed from ui/app/components/send_/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js)8
-rw-r--r--ui/app/components/send/send-content/send-gas-row/gas-fee-display/index.js (renamed from ui/app/components/send_/send-content/send-gas-row/gas-fee-display/index.js)0
-rw-r--r--ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js (renamed from ui/app/components/send_/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-gas-row/index.js (renamed from ui/app/components/send_/send-content/send-gas-row/index.js)0
-rw-r--r--ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js (renamed from ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js)16
-rw-r--r--ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js (renamed from ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js)5
-rw-r--r--ui/app/components/send/send-content/send-gas-row/send-gas-row.scss (renamed from ui/app/components/send_/send-content/send-gas-row/send-gas-row.scss)0
-rw-r--r--ui/app/components/send/send-content/send-gas-row/send-gas-row.selectors.js14
-rw-r--r--ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js (renamed from ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-component.test.js)5
-rw-r--r--ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js (renamed from ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-container.test.js)6
-rw-r--r--ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js49
-rw-r--r--ui/app/components/send/send-content/send-hex-data-row/index.js1
-rw-r--r--ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js40
-rw-r--r--ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.container.js21
-rw-r--r--ui/app/components/send/send-content/send-row-wrapper/index.js (renamed from ui/app/components/send_/send-content/send-row-wrapper/index.js)0
-rw-r--r--ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/index.js (renamed from ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/index.js)0
-rw-r--r--ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md (renamed from ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md)0
-rw-r--r--ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js (renamed from ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js)8
-rw-r--r--ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js (renamed from ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js)0
-rw-r--r--ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss (renamed from ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss)0
-rw-r--r--ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js (renamed from ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js (renamed from ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper-README.md (renamed from ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper-README.md)0
-rw-r--r--ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.component.js (renamed from ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.component.js)8
-rw-r--r--ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.scss (renamed from ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.scss)0
-rw-r--r--ui/app/components/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js (renamed from ui/app/components/send_/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-to-row/index.js (renamed from ui/app/components/send_/send-content/send-to-row/index.js)0
-rw-r--r--ui/app/components/send/send-content/send-to-row/send-to-row-README.md (renamed from ui/app/components/send_/send-content/send-to-row/send-to-row-README.md)0
-rw-r--r--ui/app/components/send/send-content/send-to-row/send-to-row.component.js (renamed from ui/app/components/send_/send-content/send-to-row/send-to-row.component.js)9
-rw-r--r--ui/app/components/send/send-content/send-to-row/send-to-row.container.js (renamed from ui/app/components/send_/send-content/send-to-row/send-to-row.container.js)0
-rw-r--r--ui/app/components/send/send-content/send-to-row/send-to-row.selectors.js (renamed from ui/app/components/send_/send-content/send-to-row/send-to-row.selectors.js)0
-rw-r--r--ui/app/components/send/send-content/send-to-row/send-to-row.utils.js (renamed from ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js)0
-rw-r--r--ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js (renamed from ui/app/components/send_/send-content/send-to-row/tests/send-to-row-component.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-to-row/tests/send-to-row-container.test.js (renamed from ui/app/components/send_/send-content/send-to-row/tests/send-to-row-container.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-to-row/tests/send-to-row-selectors.test.js (renamed from ui/app/components/send_/send-content/send-to-row/tests/send-to-row-selectors.test.js)0
-rw-r--r--ui/app/components/send/send-content/send-to-row/tests/send-to-row-utils.test.js (renamed from ui/app/components/send_/send-content/send-to-row/tests/send-to-row-utils.test.js)0
-rw-r--r--ui/app/components/send/send-content/tests/send-content-component.test.js (renamed from ui/app/components/send_/send-content/tests/send-content-component.test.js)0
-rw-r--r--ui/app/components/send/send-footer/README.md (renamed from ui/app/components/send_/send-footer/README.md)0
-rw-r--r--ui/app/components/send/send-footer/index.js (renamed from ui/app/components/send_/send-footer/index.js)0
-rw-r--r--ui/app/components/send/send-footer/send-footer.component.js (renamed from ui/app/components/send_/send-footer/send-footer.component.js)19
-rw-r--r--ui/app/components/send/send-footer/send-footer.container.js (renamed from ui/app/components/send_/send-footer/send-footer.container.js)9
-rw-r--r--ui/app/components/send/send-footer/send-footer.scss (renamed from ui/app/components/send_/send-footer/send-footer.scss)0
-rw-r--r--ui/app/components/send/send-footer/send-footer.selectors.js (renamed from ui/app/components/send_/send-footer/send-footer.selectors.js)0
-rw-r--r--ui/app/components/send/send-footer/send-footer.utils.js (renamed from ui/app/components/send_/send-footer/send-footer.utils.js)38
-rw-r--r--ui/app/components/send/send-footer/tests/send-footer-component.test.js (renamed from ui/app/components/send_/send-footer/tests/send-footer-component.test.js)13
-rw-r--r--ui/app/components/send/send-footer/tests/send-footer-container.test.js (renamed from ui/app/components/send_/send-footer/tests/send-footer-container.test.js)5
-rw-r--r--ui/app/components/send/send-footer/tests/send-footer-selectors.test.js (renamed from ui/app/components/send_/send-footer/tests/send-footer-selectors.test.js)0
-rw-r--r--ui/app/components/send/send-footer/tests/send-footer-utils.test.js (renamed from ui/app/components/send_/send-footer/tests/send-footer-utils.test.js)24
-rw-r--r--ui/app/components/send/send-header/README.md (renamed from ui/app/components/send_/send-header/README.md)0
-rw-r--r--ui/app/components/send/send-header/index.js (renamed from ui/app/components/send_/send-header/index.js)0
-rw-r--r--ui/app/components/send/send-header/send-header.component.js (renamed from ui/app/components/send_/send-header/send-header.component.js)8
-rw-r--r--ui/app/components/send/send-header/send-header.container.js (renamed from ui/app/components/send_/send-header/send-header.container.js)0
-rw-r--r--ui/app/components/send/send-header/send-header.selectors.js (renamed from ui/app/components/send_/send-header/send-header.selectors.js)0
-rw-r--r--ui/app/components/send/send-header/tests/send-header-component.test.js (renamed from ui/app/components/send_/send-header/tests/send-header-component.test.js)0
-rw-r--r--ui/app/components/send/send-header/tests/send-header-container.test.js (renamed from ui/app/components/send_/send-header/tests/send-header-container.test.js)0
-rw-r--r--ui/app/components/send/send-header/tests/send-header-selectors.test.js (renamed from ui/app/components/send_/send-header/tests/send-header-selectors.test.js)0
-rw-r--r--ui/app/components/send/send.component.js (renamed from ui/app/components/send_/send.component.js)27
-rw-r--r--ui/app/components/send/send.constants.js (renamed from ui/app/components/send_/send.constants.js)0
-rw-r--r--ui/app/components/send/send.container.js (renamed from ui/app/components/send_/send.container.js)2
-rw-r--r--ui/app/components/send/send.scss (renamed from ui/app/components/send_/send.scss)0
-rw-r--r--ui/app/components/send/send.selectors.js (renamed from ui/app/components/send_/send.selectors.js)5
-rw-r--r--ui/app/components/send/send.utils.js (renamed from ui/app/components/send_/send.utils.js)41
-rw-r--r--ui/app/components/send/tests/send-component.test.js (renamed from ui/app/components/send_/tests/send-component.test.js)75
-rw-r--r--ui/app/components/send/tests/send-container.test.js (renamed from ui/app/components/send_/tests/send-container.test.js)12
-rw-r--r--ui/app/components/send/tests/send-selectors-test-data.js (renamed from ui/app/components/send_/tests/send-selectors-test-data.js)0
-rw-r--r--ui/app/components/send/tests/send-selectors.test.js (renamed from ui/app/components/send_/tests/send-selectors.test.js)0
-rw-r--r--ui/app/components/send/tests/send-utils.test.js (renamed from ui/app/components/send_/tests/send-utils.test.js)72
-rw-r--r--ui/app/components/send/to-autocomplete.component.js141
-rw-r--r--ui/app/components/send/to-autocomplete/index.js1
-rw-r--r--ui/app/components/send/to-autocomplete/to-autocomplete.js (renamed from ui/app/components/send/to-autocomplete.js)2
-rw-r--r--ui/app/components/send_/send-content/send-gas-row/send-gas-row.selectors.js9
-rw-r--r--ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-selectors.test.js22
-rw-r--r--ui/app/components/send_/send.utils.test.js30
-rw-r--r--ui/app/components/sender-to-recipient.js72
-rw-r--r--ui/app/components/sender-to-recipient/index.js1
-rw-r--r--ui/app/components/sender-to-recipient/index.scss (renamed from ui/app/css/itcss/components/sender-to-recipient.scss)26
-rw-r--r--ui/app/components/sender-to-recipient/sender-to-recipient.component.js117
-rw-r--r--ui/app/components/signature-request.js13
-rw-r--r--ui/app/components/tabs/index.js3
-rw-r--r--ui/app/components/tabs/index.scss11
-rw-r--r--ui/app/components/tabs/tab/index.js2
-rw-r--r--ui/app/components/tabs/tab/index.scss15
-rw-r--r--ui/app/components/tabs/tab/tab.component.js31
-rw-r--r--ui/app/components/tabs/tabs.component.js62
-rw-r--r--ui/app/components/token-balance.js6
-rw-r--r--ui/app/components/token-list.js5
-rw-r--r--ui/app/components/tooltip-v2.js4
-rw-r--r--ui/app/components/tx-list-item.js33
-rw-r--r--ui/app/components/tx-view.js37
-rw-r--r--ui/app/components/wallet-view.js2
-rw-r--r--ui/app/conf-tx.js139
-rw-r--r--ui/app/constants/error-keys.js3
-rw-r--r--ui/app/css/itcss/components/account-menu.scss18
-rw-r--r--ui/app/css/itcss/components/alert.scss57
-rw-r--r--ui/app/css/itcss/components/buttons.scss25
-rw-r--r--ui/app/css/itcss/components/index.scss4
-rw-r--r--ui/app/css/itcss/components/network.scss12
-rw-r--r--ui/app/css/itcss/components/new-account.scss297
-rw-r--r--ui/app/css/itcss/components/newui-sections.scss9
-rw-r--r--ui/app/css/itcss/components/request-signature.scss1
-rw-r--r--ui/app/css/itcss/components/send.scss4
-rw-r--r--ui/app/css/itcss/components/token-list.scss1
-rw-r--r--ui/app/css/itcss/components/transaction-list.scss10
-rw-r--r--ui/app/css/itcss/generic/index.scss189
-rw-r--r--ui/app/css/itcss/settings/variables.scss1
-rw-r--r--ui/app/css/itcss/tools/utilities.scss2
-rw-r--r--ui/app/ducks/confirm-transaction.duck.js386
-rw-r--r--ui/app/ducks/send.duck.js7
-rw-r--r--ui/app/ducks/tests/confirm-transaction.duck.test.js675
-rw-r--r--ui/app/ducks/tests/send-duck.test.js10
-rw-r--r--ui/app/helpers/confirm-transaction/util.js139
-rw-r--r--ui/app/helpers/confirm-transaction/util.test.js137
-rw-r--r--ui/app/helpers/with-token-tracker.js3
-rw-r--r--ui/app/i18n-provider.js5
-rw-r--r--ui/app/main-container.js3
-rw-r--r--ui/app/metamask-connect.js27
-rw-r--r--ui/app/reducers.js3
-rw-r--r--ui/app/reducers/app.js15
-rw-r--r--ui/app/reducers/metamask.js8
-rw-r--r--ui/app/routes.js23
-rw-r--r--ui/app/selectors.js23
-rw-r--r--ui/app/selectors/confirm-transaction.js191
-rw-r--r--ui/app/token-util.js5
-rw-r--r--ui/app/util.js9
-rw-r--r--ui/i18n-helper.js25
355 files changed, 10262 insertions, 3796 deletions
diff --git a/.babelrc b/.babelrc
index 307583ffd..fcabd2d1a 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,4 +1,4 @@
{
- "presets": ["es2015", "stage-0", "react"],
- "plugins": ["transform-runtime", "transform-async-to-generator"]
+ "presets": [["env"], "react", "stage-0"],
+ "plugins": ["transform-runtime", "transform-async-to-generator", "transform-class-properties"]
}
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 0ad5cdf53..6d4bf270c 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -101,29 +101,30 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ keys:
+ - v1.0-dependency-cache-{{ checksum "package-lock.json" }}
+ # fallback to using the latest cache if no exact match is found
+ - v1.0-dependency-cache-
- run:
- name: Install deps via npm
- command: npm install
+ name: Install npm 6 + deps via npm
+ command: |
+ sudo npm install -g npm@6.1.0 && npm install --no-save
- save_cache:
- key: dependency-cache-{{ checksum "package-lock.json" }}
+ key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
paths:
- node_modules
- - save_cache:
- key: dependency-cache-{{ .Revision }}
- paths:
- - node_modules
-
prep-deps-firefox:
docker:
- image: circleci/node:8.11.3-browsers
steps:
- checkout
+ - restore_cache:
+ key: v1.0-dependency-cache-firefox-
- run:
- name: Download Firefox
+ name: Download Firefox If needed
command: ./.circleci/scripts/firefox-download.sh
- save_cache:
- key: dependency-cache-firefox-{{ .Revision }}
+ key: v1.0-dependency-cache-firefox-
paths:
- firefox
@@ -133,7 +134,7 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
- run:
name: build:dist
command: npm run dist
@@ -152,7 +153,7 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
- run:
name: build:dist
command: npm run doc
@@ -167,7 +168,7 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
- run:
name: Get Scss Cache key
# this allows us to checksum against a whole directory
@@ -186,7 +187,7 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
- run:
name: Test
command: npm run lint
@@ -197,7 +198,7 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
- run:
name: Test
command: npx nsp check
@@ -208,7 +209,7 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
- restore_cache:
key: build-cache-{{ .Revision }}
- run:
@@ -224,12 +225,12 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-firefox-{{ .Revision }}
+ key: v1.0-dependency-cache-firefox-
- run:
name: Install firefox
command: ./.circleci/scripts/firefox-install.sh
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
- restore_cache:
key: build-cache-{{ .Revision }}
- run:
@@ -245,7 +246,7 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
- restore_cache:
key: build-cache-{{ .Revision }}
- run:
@@ -261,12 +262,12 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-firefox-{{ .Revision }}
+ key: v1.0-dependency-cache-firefox-
- run:
name: Install firefox
command: ./.circleci/scripts/firefox-install.sh
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
- restore_cache:
key: build-cache-{{ .Revision }}
- run:
@@ -282,7 +283,7 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
- restore_cache:
key: build-cache-{{ .Revision }}
- run:
@@ -299,7 +300,7 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
- restore_cache:
key: build-cache-{{ .Revision }}
- restore_cache:
@@ -326,7 +327,7 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
- restore_cache:
key: build-cache-{{ .Revision }}
- restore_cache:
@@ -349,7 +350,7 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
- run:
name: test:coverage
command: npm run test:coverage
@@ -362,12 +363,12 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-firefox-{{ .Revision }}
+ key: v1.0-dependency-cache-firefox-
- run:
name: Install firefox
command: ./.circleci/scripts/firefox-install.sh
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
- run:
name: Get Scss Cache key
# this allows us to checksum against a whole directory
@@ -386,7 +387,7 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
- run:
name: Get Scss Cache key
# this allows us to checksum against a whole directory
@@ -405,12 +406,12 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-firefox-{{ .Revision }}
+ key: v1.0-dependency-cache-firefox-
- run:
name: Install firefox
command: ./.circleci/scripts/firefox-install.sh
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
- run:
name: Get Scss Cache key
# this allows us to checksum against a whole directory
@@ -429,7 +430,7 @@ jobs:
steps:
- checkout
- restore_cache:
- key: dependency-cache-{{ .Revision }}
+ key: v1.0-dependency-cache-{{ checksum "package-lock.json" }}
- run:
name: Get Scss Cache key
# this allows us to checksum against a whole directory
@@ -446,4 +447,4 @@ jobs:
steps:
- run:
name: All Tests Passed
- command: echo 'weew - everything passed!'
+ command: echo 'weew - everything passed!' \ No newline at end of file
diff --git a/.circleci/scripts/firefox-download.sh b/.circleci/scripts/firefox-download.sh
index c63e8c3df..64f0c74e3 100755
--- a/.circleci/scripts/firefox-download.sh
+++ b/.circleci/scripts/firefox-download.sh
@@ -1,6 +1,13 @@
#!/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"
+echo "Checking if firefox was already downloaded"
+if [ -d "firefox" ]
+then
+ echo "Firefox found. No need to download"
+else
+ FIREFOX_VERSION="61.0.1"
+ FIREFOX_BINARY="firefox-$FIREFOX_VERSION.tar.bz2"
+ echo "Downloading firefox..."
+ wget "https://ftp.mozilla.org/pub/firefox/releases/$FIREFOX_VERSION/linux-x86_64/en-US/$FIREFOX_BINARY" \
+ && tar xjf "$FIREFOX_BINARY"
+ echo "firefox download complete"
+fi
diff --git a/.circleci/scripts/firefox-install.sh b/.circleci/scripts/firefox-install.sh
index 589bcbbb5..1c60f4de9 100755
--- a/.circleci/scripts/firefox-install.sh
+++ b/.circleci/scripts/firefox-install.sh
@@ -2,7 +2,7 @@
echo "Installing firefox..."
sudo rm -r /opt/firefox
-sudo mv firefox /opt/firefox58
+sudo mv firefox /opt/firefox61
sudo mv /usr/bin/firefox /usr/bin/firefox-old
-sudo ln -s /opt/firefox58/firefox /usr/bin/firefox
+sudo ln -s /opt/firefox61/firefox /usr/bin/firefox
echo "Firefox installed."
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 000000000..1cdadda65
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,6 @@
+# Lines starting with '#' are comments.
+# Each line is a file pattern followed by one or more owners.
+
+ui/ @danjm @alextsg @whymarrh
+app/scripts/controllers/transactions @frankiebee
+
diff --git a/.gitignore b/.gitignore
index fcf520641..af86113fb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,10 @@ package
.vscode
.sublime-project
+# VIM
+*.swp
+*.swo
+
temp
.tmp
.sass-cache
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 000000000..b6f27f135
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+engine-strict=true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d86fcd713..7e1e1ff4b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,13 @@
## Current Master
+- Add new tokens auto detection
+- Remove rejected transactions from transaction history
+- Add Trezor Support
+- Allow to remove accounts (Imported and Hardware Wallets)
+- [#4840](https://github.com/MetaMask/metamask-extension/pull/4840): Now shows notifications when transactions are completed.
+- [#4855](https://github.com/MetaMask/metamask-extension/pull/4855): network.js: convert rpc protocol to lower case.
+
## 4.8.0 Thur Jun 14 2018
- [#4513](https://github.com/MetaMask/metamask-extension/pull/4513): Attempting to import an empty private key will now show a clear error.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8517eed70..63d19c633 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,31 +2,26 @@
If you're submitting code to MetaMask, there are some simple things we'd appreciate you doing to help us stay organized!
-## Submitting pull requests
+### Finding the right project
Before taking the time to code and implement something, feel free to open an issue and discuss it! There may even be an issue already open, and together we may come up with a specific strategy before you take your precious time to write code.
-### Tests
+There are also plenty of open issues we'd love help with. Search the [`good first issue`](https://github.com/MetaMask/metamask-extension/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) label, or head to Gitcoin and earn ETH for completing projects we've posted bounties on.
-For any new programmatic functionality, we like unit tests when possible, so if you can keep your code cleanly isolated, please do add a test file to the `tests` folder.
+If you're picking up a bounty or an existing issue, feel free to ask clarifying questions on the issue as you go about your work.
-### PR Format
+### Submitting a pull request
+When you're done with your project / bugfix / feature and ready to submit a PR, there are a couple guidelines we ask you to follow:
-If this PR closes the issue, add the line `Fixes #$ISSUE_NUMBER`. Ex. For closing issue 418, include the line `Fixes #418`.
+- [ ] **Test it**: For any new programmatic functionality, we like unit tests when possible, so if you can keep your code cleanly isolated, please do add a test file to the `tests` folder.
+- [ ] **Add to the CHANGELOG**: Help us keep track of all the moving pieces by adding an entry to the [`CHANGELOG.md`](https://github.com/MetaMask/metamask-extension/blob/develop/CHANGELOG.md) with a link to your PR.
+- [ ] **Meet the spec**: Make sure the PR adds functionality that matches the issue you're closing. This is especially important for bounties: sometimes design or implementation details are included in the conversation, so read carefully!
+- [ ] **Close the issue**: If this PR closes an open issue, add the line `Fixes #$ISSUE_NUMBER`. Ex. For closing issue 418, include the line `Fixes #418`. If it doesn't close the issue but addresses it partially, just include a reference to the issue number, like `#418`.
+- [ ] **Keep it simple**: Try not to include multiple features in a single PR, and don't make extraneous changes outside the scope of your contribution. All those touched files make things harder to review ;)
+- [ ] **PR against `develop`**: Submit your PR against the `develop` branch. This is where we merge new features so they get some time to receive extra testing before being pushed to `master` for production. If your PR is a hot-fix that needs to be published urgently, you may submit a PR against the `master` branch, but this PR will receive tighter scrutiny before merging.
+- [ ] **Get reviewed by a core contributor**: Make sure you get a `:thumbsup`, `:+1`, or `LGTM` from a user with a `Member` badge before merging.
-If it doesn't close the issue but addresses it partially, just include a reference to the issue number, like `#418`.
-
-Submit your PR against the `develop` branch. This is where we merge new features so they get some time to receive extra testing before being pushed to `master` for production.
-
-If your PR is a hot-fix that needs to be published urgently, you may submit a PR against the `master` branch, but this PR will receive tighter scrutiny before merging.
-
-## Before Merging
-
-Make sure you get a `:thumbsup`, `:+1`, or `LGTM` from another collaborator before merging.
-
-## Before Closing Issues
-
-Make sure the relevant code has been reviewed and merged.
+And that's it! Thanks for helping out.
### Developing inside a node_modules folder
diff --git a/README.md b/README.md
index 70faa8856..86d515377 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,8 @@
# MetaMask Browser Extension
[![Build Status](https://circleci.com/gh/MetaMask/metamask-extension.svg?style=shield&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-extension) [![Coverage Status](https://coveralls.io/repos/github/MetaMask/metamask-extension/badge.svg?branch=master)](https://coveralls.io/github/MetaMask/metamask-extension?branch=master) [![Greenkeeper badge](https://badges.greenkeeper.io/MetaMask/metamask-extension.svg)](https://greenkeeper.io/) [![Stories in Ready](https://badge.waffle.io/MetaMask/metamask-extension.png?label=in%20progress&title=waffle.io)](https://waffle.io/MetaMask/metamask-extension)
+🚨 As of 7/25/18, the MetaMask extension has been removed from the Chrome Web Store. In the meantime, you can download the latest version of MetaMask on our [Releases](https://github.com/MetaMask/metamask-extension/releases) page and load it in Chrome by visiting `chrome://extensions`. For more detailed steps, see our [help center](https://consensys.zendesk.com/hc/en-us/articles/360004134152-How-to-Install-MetaMask-Manually). Follow [@metamask_io](https://twitter.com/metamask_io) on Twitter for updates. 🚨
+
## Support
If you're a user seeking support, [here is our support site](https://metamask.helpscoutdocs.com/).
@@ -9,7 +11,7 @@ If you're a user seeking support, [here is our support site](https://metamask.he
[Mission Statement](./MISSION.md)
-[Internal documentation](./docs/jsdocs)
+[Internal documentation](./docs#documentation)
## Developing Compatible Dapps
@@ -26,10 +28,9 @@ If you're a web dapp developer, we've got two types of guides for you:
## Building locally
- - Install [Node.js](https://nodejs.org/en/) version 6.3.1 or later.
+ - Install [Node.js](https://nodejs.org/en/) version 8.11.3 and npm version 6.1.0
- Install dependencies:
- - For node versions up to and including 9, install local dependencies with `npm install`.
- - For node versions 10 and later, install [Yarn](https://yarnpkg.com/lang/en/docs/install/) and use `yarn install`.
+ - If you are using nvm (recommended) running `nvm use` will automatically choose the right node version for you.
- Install gulp globally with `npm install -g gulp-cli`.
- Build the project to the `./dist/` folder with `gulp build`.
- Optionally, to rebuild on file changes, run `gulp dev`.
@@ -81,6 +82,7 @@ To write tests that will be run in the browser using QUnit, add your test files
- [How to add new networks to the Provider Menu](./docs/adding-new-networks.md)
- [How to manage notices that appear when the app starts up](./docs/notices.md)
- [How to port MetaMask to a new platform](./docs/porting_to_new_environment.md)
+- [How to use the TREZOR emulator](./docs/trezor-emulator.md)
- [How to generate a visualization of this repository's development](./docs/development-visualization.md)
[1]: http://www.nomnoml.com/#view/%5B%3Cactor%3Euser%5D%0A%0A%5Bmetamask-ui%7C%0A%20%20%20%5Btools%7C%0A%20%20%20%20%20react%0A%20%20%20%20%20redux%0A%20%20%20%20%20thunk%0A%20%20%20%20%20ethUtils%0A%20%20%20%20%20jazzicon%0A%20%20%20%5D%0A%20%20%20%5Bcomponents%7C%0A%20%20%20%20%20app%0A%20%20%20%20%20account-detail%0A%20%20%20%20%20accounts%0A%20%20%20%20%20locked-screen%0A%20%20%20%20%20restore-vault%0A%20%20%20%20%20identicon%0A%20%20%20%20%20config%0A%20%20%20%20%20info%0A%20%20%20%5D%0A%20%20%20%5Breducers%7C%0A%20%20%20%20%20app%0A%20%20%20%20%20metamask%0A%20%20%20%20%20identities%0A%20%20%20%5D%0A%20%20%20%5Bactions%7C%0A%20%20%20%20%20%5BaccountManager%5D%0A%20%20%20%5D%0A%20%20%20%5Bcomponents%5D%3A-%3E%5Bactions%5D%0A%20%20%20%5Bactions%5D%3A-%3E%5Breducers%5D%0A%20%20%20%5Breducers%5D%3A-%3E%5Bcomponents%5D%0A%5D%0A%0A%5Bweb%20dapp%7C%0A%20%20%5Bui%20code%5D%0A%20%20%5Bweb3%5D%0A%20%20%5Bmetamask-inpage%5D%0A%20%20%0A%20%20%5B%3Cactor%3Eui%20developer%5D%0A%20%20%5Bui%20developer%5D-%3E%5Bui%20code%5D%0A%20%20%5Bui%20code%5D%3C-%3E%5Bweb3%5D%0A%20%20%5Bweb3%5D%3C-%3E%5Bmetamask-inpage%5D%0A%5D%0A%0A%5Bmetamask-background%7C%0A%20%20%5Bprovider-engine%5D%0A%20%20%5Bhooked%20wallet%20subprovider%5D%0A%20%20%5Bid%20store%5D%0A%20%20%0A%20%20%5Bprovider-engine%5D%3C-%3E%5Bhooked%20wallet%20subprovider%5D%0A%20%20%5Bhooked%20wallet%20subprovider%5D%3C-%3E%5Bid%20store%5D%0A%20%20%5Bconfig%20manager%7C%0A%20%20%20%20%5Brpc%20configuration%5D%0A%20%20%20%20%5Bencrypted%20keys%5D%0A%20%20%20%20%5Bwallet%20nicknames%5D%0A%20%20%5D%0A%20%20%0A%20%20%5Bprovider-engine%5D%3C-%5Bconfig%20manager%5D%0A%20%20%5Bid%20store%5D%3C-%3E%5Bconfig%20manager%5D%0A%5D%0A%0A%5Buser%5D%3C-%3E%5Bmetamask-ui%5D%0A%0A%5Buser%5D%3C%3A--%3A%3E%5Bweb%20dapp%5D%0A%0A%5Bmetamask-contentscript%7C%0A%20%20%5Bplugin%20restart%20detector%5D%0A%20%20%5Brpc%20passthrough%5D%0A%5D%0A%0A%5Brpc%20%7C%0A%20%20%5Bethereum%20blockchain%20%7C%0A%20%20%20%20%5Bcontracts%5D%0A%20%20%20%20%5Baccounts%5D%0A%20%20%5D%0A%5D%0A%0A%5Bweb%20dapp%5D%3C%3A--%3A%3E%5Bmetamask-contentscript%5D%0A%5Bmetamask-contentscript%5D%3C-%3E%5Bmetamask-background%5D%0A%5Bmetamask-background%5D%3C-%3E%5Bmetamask-ui%5D%0A%5Bmetamask-background%5D%3C-%3E%5Brpc%5D%0A
diff --git a/USER_AGREEMENT.md b/USER_AGREEMENT.md
index 8c4c41e58..c485edd1d 100644
--- a/USER_AGREEMENT.md
+++ b/USER_AGREEMENT.md
@@ -138,7 +138,7 @@ Notwithstanding the parties' decision to resolve all disputes through arbitratio
### 13.6 30-Day Right to Opt Out ###
-You have the right to opt-out and not be bound by the arbitration and class action waiver provisions set forth above by sending written notice of your decision to opt-out to the following address: MetaMask ℅ ConsenSys, 49 Bogart Street, Brooklyn NY 11206 and via email at legal-opt@metamask.io. The notice must be sent within 30 days of September 6, 2016 or your first use of the Service, whichever is later, otherwise you shall be bound to arbitrate disputes in accordance with the terms of those paragraphs. If you opt-out of these arbitration provisions, MetaMask also will not be bound by them.
+You have the right to opt-out and not be bound by the arbitration and class action waiver provisions set forth above by sending written notice of your decision to opt-out to the following address: MetaMask ℅ ConsenSys, 49 Bogart Street, Brooklyn NY 11206 and via email at support@metamask.io. The notice must be sent within 30 days of September 6, 2016 or your first use of the Service, whichever is later, otherwise you shall be bound to arbitrate disputes in accordance with the terms of those paragraphs. If you opt-out of these arbitration provisions, MetaMask also will not be bound by them.
### 13.7 Changes to This Section ###
diff --git a/app/404.html b/app/404.html
new file mode 100644
index 000000000..8a6df9d7a
--- /dev/null
+++ b/app/404.html
@@ -0,0 +1,52 @@
+<html>
+<head>
+ <title>MetaMask</title>
+ <style>
+ *{
+ padding: 0;
+ margin: 0;
+ box-sizing: border-box;
+ }
+ img{
+ display: block;
+ }
+ html, body{
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ }
+ .app{
+ position: relative;
+ width: 100%;
+ height: auto;
+ overflow: hidden;
+ }
+ img{
+ display: block;
+ width: 100%;
+ height: auto;
+ }
+ h2{
+ display: block;
+ width: 100%;
+ overflow: hidden;
+ position: absolute;
+ bottom: 20%;
+ left: 0;
+ color: #1b243d;
+ text-align: center;
+ }
+ h2 > a{
+ color: #1b243d;
+ }
+ </style>
+</head>
+<body>
+ <div class="app">
+ <img src="./images/404.png" alt="">
+ <h2>Powered by <a href="https://www.portal.network/">Portal Network</a></h2>
+ </div>
+</body>
+</html>
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 46fbdc1a7..8d65bc596 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -11,6 +11,9 @@
"accountName": {
"message": "Account Name"
},
+ "accountSelectionRequired": {
+ "message": "You need to select an account!"
+ },
"address": {
"message": "Address"
},
@@ -40,6 +43,9 @@
"message": "MetaMask",
"description": "The name of the application"
},
+ "approve": {
+ "message": "Approve"
+ },
"approved": {
"message": "Approved"
},
@@ -77,6 +83,9 @@
"borrowDharma": {
"message": "Borrow With Dharma (Beta)"
},
+ "browserNotSupported": {
+ "message": "Your Browser is not supported..."
+ },
"builtInCalifornia": {
"message": "MetaMask is designed and built in California."
},
@@ -89,6 +98,9 @@
"buyCoinbaseExplainer": {
"message": "Coinbase is the world’s most popular way to buy and sell bitcoin, ethereum, and litecoin."
},
+ "bytes": {
+ "message": "Bytes"
+ },
"ok": {
"message": "Ok"
},
@@ -104,6 +116,9 @@
"close": {
"message": "Close"
},
+ "chromeRequiredForTrezor":{
+ "message": "You need to use Metamask on Google Chrome in order to connect to your TREZOR device."
+ },
"confirm": {
"message": "Confirm"
},
@@ -119,6 +134,24 @@
"confirmTransaction": {
"message": "Confirm Transaction"
},
+ "connectHardwareWallet": {
+ "message": "Connect Hardware Wallet"
+ },
+ "connect": {
+ "message": "Connect"
+ },
+ "connecting": {
+ "message": "Connecting..."
+ },
+ "connectToTrezor": {
+ "message": "Connect to Trezor"
+ },
+ "connectToTrezorHelp": {
+ "message": "Metamask is able to access your TREZOR ethereum accounts. First make sure your device is connected and unlocked."
+ },
+ "connectToTrezorTrouble": {
+ "message": "If you are having trouble, please make sure you are using the latest version of the TREZOR firmware."
+ },
"continue": {
"message": "Continue"
},
@@ -149,6 +182,9 @@
"copyContractAddress": {
"message": "Copy Contract Address"
},
+ "copyAddress": {
+ "message": "Copy address to clipboard"
+ },
"copyToClipboard": {
"message": "Copy to clipboard"
},
@@ -244,9 +280,15 @@
"done": {
"message": "Done"
},
+ "downloadGoogleChrome": {
+ "message": "Download Google Chrome"
+ },
"downloadStateLogs": {
"message": "Download State Logs"
},
+ "dontHaveATrezorWallet": {
+ "message": "Don't have a TREZOR hardware wallet?"
+ },
"dropped": {
"message": "Dropped"
},
@@ -277,6 +319,9 @@
"enterPasswordContinue": {
"message": "Enter password to continue"
},
+ "parameters": {
+ "message": "Parameters"
+ },
"passwordNotLongEnough": {
"message": "Password not long enough"
},
@@ -309,6 +354,9 @@
"followTwitter": {
"message": "Follow us on Twitter"
},
+ "forgetDevice": {
+ "message": "Forget this device"
+ },
"from": {
"message": "From"
},
@@ -318,6 +366,9 @@
"fromShapeShift": {
"message": "From ShapeShift"
},
+ "functionType": {
+ "message": "Function Type"
+ },
"gas": {
"message": "Gas",
"description": "Short indication of gas cost"
@@ -359,10 +410,28 @@
"message": "Get Ether from a faucet for the $1",
"description": "Displays network name for Ether faucet"
},
+ "getHelp": {
+ "message": "Get Help."
+ },
"greaterThanMin": {
"message": "must be greater than or equal to $1.",
"description": "helper for inputting hex as decimal input"
},
+ "hardware": {
+ "message": "hardware"
+ },
+ "hardwareWalletConnected": {
+ "message": "Hardware wallet connected"
+ },
+ "hardwareSupport": {
+ "message": "Hardware Support"
+ },
+ "hardwareSupportMsg": {
+ "message": "You can now view your Hardware accounts in MetaMask! Scroll down and read how it works."
+ },
+ "havingTroubleConnecting": {
+ "message": "Having trouble connecting?"
+ },
"here": {
"message": "here",
"description": "as in -click here- for more information (goes with troubleTokenBalances)"
@@ -370,6 +439,9 @@
"hereList": {
"message": "Here's a list!!!!"
},
+ "hexData": {
+ "message": "Hex Data"
+ },
"hide": {
"message": "Hide"
},
@@ -458,7 +530,7 @@
"message": "Max"
},
"learnMore": {
- "message": "Learn more."
+ "message": "Learn more"
},
"lessThanMax": {
"message": "must be less than or equal to $1.",
@@ -497,6 +569,9 @@
"mainnet": {
"message": "Main Ethereum Network"
},
+ "menu": {
+ "message": "Menu"
+ },
"message": {
"message": "Message"
},
@@ -563,12 +638,18 @@
"noDeposits": {
"message": "No deposits received"
},
+ "noConversionRateAvailable":{
+ "message": "No Conversion Rate Available"
+ },
"noTransactionHistory": {
"message": "No transaction history."
},
"noTransactions": {
"message": "No Transactions"
},
+ "notFound": {
+ "message": "Not Found"
+ },
"notStarted": {
"message": "Not Started"
},
@@ -578,10 +659,16 @@
"oldUIMessage": {
"message": "You have returned to the old UI. You can switch back to the New UI through the option in the top right dropdown menu."
},
+ "openInTab": {
+ "message": "Open in tab"
+ },
"or": {
"message": "or",
"description": "choice between creating or importing a new account"
},
+ "origin": {
+ "message": "Origin"
+ },
"password": {
"message": "Password"
},
@@ -612,6 +699,9 @@
"popularTokens": {
"message": "Popular Tokens"
},
+ "prev": {
+ "message": "Prev"
+ },
"privacyMsg": {
"message": "Privacy Policy"
},
@@ -664,6 +754,9 @@
"restoreVault": {
"message": "Restore Vault"
},
+ "restoreAccountWithSeed": {
+ "message": "Restore your Account with Seed Phrase"
+ },
"required": {
"message": "Required"
},
@@ -673,6 +766,9 @@
"walletSeed": {
"message": "Wallet Seed"
},
+ "restore": {
+ "message": "Restore"
+ },
"revealSeedWords": {
"message": "Reveal Seed Words"
},
@@ -691,6 +787,18 @@
"revert": {
"message": "Revert"
},
+ "remove": {
+ "message": "remove"
+ },
+ "removeAccount": {
+ "message": "Remove account"
+ },
+ "removeAccountDescription": {
+ "message": "This account will be removed from your wallet. Please make sure you have the original seed phrase or private key for this imported account before continuing. You can import or create accounts again from the account drop-down. "
+ },
+ "readyToConnect": {
+ "message": "Ready to Connect?"
+ },
"rinkeby": {
"message": "Rinkeby Test Network"
},
@@ -777,6 +885,9 @@
"sendTokens": {
"message": "Send Tokens"
},
+ "separateEachWord": {
+ "message": "Separate each word with a single space"
+ },
"onlySendToEtherAddress": {
"message": "Only send ETH to an Ethereum address."
},
@@ -784,15 +895,45 @@
"message": "Only send $1 to an Ethereum account address.",
"description": "displays token symbol"
},
+ "orderOneHere": {
+ "message": "Order one here."
+ },
"searchTokens": {
"message": "Search Tokens"
},
+ "selectAnAddress": {
+ "message": "Select an Address"
+ },
+ "selectAnAccount": {
+ "message": "Select an Account"
+ },
+ "selectAnAccountHelp": {
+ "message": "These are the accounts available in your hardware wallet. Select the one you’d like to use in MetaMask."
+ },
"sendTokensAnywhere": {
"message": "Send Tokens to anyone with an Ethereum account"
},
"settings": {
"message": "Settings"
},
+ "step1HardwareWallet": {
+ "message": "1. Connect Hardware Wallet"
+ },
+ "step1HardwareWalletMsg": {
+ "message": "Connect your hardware wallet directly to your computer."
+ },
+ "step2HardwareWallet": {
+ "message": "2. Select an Account"
+ },
+ "step2HardwareWalletMsg": {
+ "message": "Select the account you want to view. You can only choose one at a time."
+ },
+ "step3HardwareWallet": {
+ "message": "3. Start using dApps and more!"
+ },
+ "step3HardwareWalletMsg": {
+ "message": "Use your hardware account like you would with any Ethereum account. Log in to dApps, send Eth, buy and store ERC20 tokens and Non-Fungible tokens like CryptoKitties."
+ },
"info": {
"message": "Info"
},
@@ -902,9 +1043,15 @@
"transactionNumber": {
"message": "Transaction Number"
},
+ "transfer": {
+ "message": "Transfer"
+ },
"transfers": {
"message": "Transfers"
},
+ "trezorHardwareWallet": {
+ "message": "TREZOR Hardware Wallet"
+ },
"troubleTokenBalances": {
"message": "We had trouble loading your token balances. You can view them ",
"description": "Followed by a link (here) to view token balances"
@@ -930,12 +1077,18 @@
"unknown": {
"message": "Unknown"
},
+ "unknownFunction": {
+ "message": "Unknown Function"
+ },
"unknownNetwork": {
"message": "Unknown Private Network"
},
"unknownNetworkId": {
"message": "Unknown network ID"
},
+ "unlock": {
+ "message": "Unlock"
+ },
"unlockMessage": {
"message": "The decentralized web awaits"
},
diff --git a/app/error.html b/app/error.html
new file mode 100644
index 000000000..366b3d94a
--- /dev/null
+++ b/app/error.html
@@ -0,0 +1,79 @@
+<html>
+<head>
+ <title>MetaMask Error</title>
+ <link href="https://fonts.googleapis.com/css?family=Rokkitt" rel="stylesheet">
+ <style>
+ *{
+ padding: 0;
+ margin: 0;
+ box-sizing: border-box;
+ }
+ img{
+ display: block;
+ }
+ html, body{
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ }
+ @keyframes logoAmin{
+ from {transform: scale(1);}
+ 50%{transform: scale(1.1);}
+ to {transform: scale(1);}
+ }
+ .errorBox{
+ width: 70%;
+ height: auto;
+ overflow: hidden;
+ background-image: url("./images/deadface.png");
+ background-repeat: no-repeat;
+ background-position: 100% 50%;
+ background-size: auto 90%;
+ padding: 5px;
+ }
+ .errorBox > img{
+ width: 100px;
+ height: auto;
+ margin-bottom: 25px;
+ animation: logoAmin 1s infinite linear;
+ }
+ .errorBox > h1, .errorBox > h2{
+ letter-spacing: 2px;
+ }
+ .errorBox > h1{
+ color: #9b9b9b;
+ font-size: 40px;
+ }
+ .errorBox > h2{
+ color: #1b243d;
+ font-size: 20px;
+ padding-top: 5px;
+ }
+ .errorBox > h2 >a{
+ color: #1b243d;
+ }
+ .errorBox > h2 >a:hover{
+ color: #44588e;
+ }
+
+ .errorBox > h1 > span{
+ color: #33559f;
+ }
+
+ </style>
+</head>
+<body>
+ <div class="errorBox">
+ <img src="./images/logo.png" alt="">
+ <h1><span id="name"></span> not found</h1>
+ <h2>Powered by <a href="https://www.portal.network/">Portal Network</a></h2>
+ </div>
+ <script>
+ let index = location.href.lastIndexOf("?name=")
+ let name = location.href.slice(index + 6)
+ document.getElementById("name").innerHTML = name
+ </script>
+</body>
+</html>
diff --git a/app/images/404.png b/app/images/404.png
new file mode 100644
index 000000000..b1a767dde
--- /dev/null
+++ b/app/images/404.png
Binary files differ
diff --git a/app/images/alert-red.svg b/app/images/alert-red.svg
new file mode 100644
index 000000000..ac5b30e27
--- /dev/null
+++ b/app/images/alert-red.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
+ <title>Artboard Copy</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Artboard-Copy" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="Group-48">
+ <circle id="Oval" fill="#D0021B" cx="8" cy="8" r="8"></circle>
+ <rect id="Rectangle-41" fill="#FFFFFF" x="7" y="3" width="2" height="7" rx="1"></rect>
+ <rect id="Rectangle-41" fill="#FFFFFF" x="7" y="11" width="2" height="2" rx="1"></rect>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/images/alert.svg b/app/images/alert.svg
new file mode 100644
index 000000000..534eda194
--- /dev/null
+++ b/app/images/alert.svg
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="29px" height="29px" viewBox="0 0 29 29" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: sketchtool 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
+ <title>7414FFD8-B28A-4593-9D7E-19E73D687B50</title>
+ <desc>Created with sketchtool.</desc>
+ <defs></defs>
+ <g id="Action-Screens" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="Approve---insufficient-amount" transform="translate(-69.000000, -166.000000)">
+ <g id="Group-7" transform="translate(53.000000, 51.000000)">
+ <g id="Group-34" transform="translate(0.000000, 91.000000)">
+ <g id="alert" transform="translate(16.000000, 24.000000)">
+ <circle id="Oval" fill="#605A1C" cx="14.5" cy="14.5" r="14.5"></circle>
+ <path d="M16,16.8282967 L14,16.8282967 L14,7 L16,7 L16,16.8282967 Z M16,21 L14,21 L14,19 L16,19 L16,21 Z" id="!" fill="#FFFCDB"></path>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/images/cancel.png b/app/images/cancel.png
new file mode 100644
index 000000000..4e0eb1143
--- /dev/null
+++ b/app/images/cancel.png
Binary files differ
diff --git a/app/images/caret-left.svg b/app/images/caret-left.svg
new file mode 100644
index 000000000..0ea266161
--- /dev/null
+++ b/app/images/caret-left.svg
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="9px" height="15px" viewBox="0 0 9 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: sketchtool 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
+ <title>8439120D-5704-4273-B416-FEE134322584</title>
+ <desc>Created with sketchtool.</desc>
+ <defs></defs>
+ <g id="Action-Screens" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="Approve---insufficient-amount" transform="translate(-75.000000, -69.000000)" stroke="#3099F2" stroke-width="2">
+ <g id="Group-7" transform="translate(53.000000, 51.000000)">
+ <g id="cancel" transform="translate(24.000000, 14.000000)">
+ <g id="Group">
+ <polyline id="Path-8" points="6.1263881 18.0633906 0 11.6306831 6.31493631 5"></polyline>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/images/connect-icon.svg b/app/images/connect-icon.svg
new file mode 100644
index 000000000..84540999a
--- /dev/null
+++ b/app/images/connect-icon.svg
@@ -0,0 +1,11 @@
+<svg width="288" height="288" xmlns="http://www.w3.org/2000/svg">
+
+ <g>
+ <title>background</title>
+ <rect fill="none" id="canvas_background" height="402" width="582" y="-1" x="-1"/>
+ </g>
+ <g>
+ <title>Layer 1</title>
+ <path fill="#ffffff" id="svg_1" d="m122,25l15,-21c4,-5 10,-5 14,0l16,22c4,5 2,10 -5,10l-12,0l0,118c0,3 3,3 5,1l25,-25c4,-4 6,-10 6,-16l0,-24c-7,0 -12,-5 -12,-12l0,-12c0,-6 5,-12 12,-12l12,0c7,0 12,5 12,12l0,12c0,7 -5,12 -12,12l0,24c0,10 -3,18 -10,25l-31,31c-4,4 -7,6 -7,16l0,49c12,3 21,13 21,26c0,15 -12,27 -27,27s-27,-12 -27,-27c0,-13 9,-23 21,-26l0,-13c0,-10 -3,-13 -7,-17l-31,-31c-6,-6 -10,-14 -10,-24l0,-25c-7,-2 -12,-9 -12,-17c0,-10 8,-18 18,-18s18,8 18,18c0,8 -5,15 -12,17l0,25c0,7 3,12 7,16l25,25c2,2 4,2 4,-1l0,-154l-12,0c-7,0 -9,-5 -4,-11z"/>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/images/deadface.png b/app/images/deadface.png
new file mode 100644
index 000000000..e12476c03
--- /dev/null
+++ b/app/images/deadface.png
Binary files differ
diff --git a/app/images/hardware-wallet-step-1.svg b/app/images/hardware-wallet-step-1.svg
new file mode 100644
index 000000000..2b6596a43
--- /dev/null
+++ b/app/images/hardware-wallet-step-1.svg
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="213px" height="78px" viewBox="0 0 213 78" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: sketchtool 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
+ <title>2981A924-C7CB-4957-87AD-8C680802DAD7</title>
+ <desc>Created with sketchtool.</desc>
+ <defs></defs>
+ <g id="Import-account" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="hardware-connect" transform="translate(-406.000000, -602.000000)">
+ <g id="Group-9" transform="translate(356.000000, 522.000000)">
+ <g id="connect-hardware" transform="translate(51.000000, 81.000000)">
+ <path d="M4,9 L70,9 L70,17 L4,17 C1.790861,17 2.705415e-16,15.209139 0,13 L0,13 C-2.705415e-16,10.790861 1.790861,9 4,9 Z" id="Rectangle" fill="#D9F0FF"></path>
+ <g id="Group-4-Copy">
+ <g id="Group-2" transform="translate(91.000000, 31.000000)" stroke="#3098DC">
+ <polyline id="Stroke-10" points="7.33333333 0 13.4253333 5.29042105 7.33333333 10.5802632"></polyline>
+ <path d="M0,5.21052632 L13.5,5.21052632" id="Stroke-11"></path>
+ </g>
+ <g id="Group-3" transform="translate(109.000000, 0.000000)">
+ <path d="M92.535988,75.1897419 L9.16167665,75.1897419 C7.13816766,75.1897419 5.49700599,73.5434839 5.49700599,71.5123226 L5.49700599,3.67741935 C5.49700599,1.64625806 7.13816766,0 9.16167665,0 L92.535988,0 C94.5601078,0 96.2006587,1.64625806 96.2006587,3.67741935 L96.2006587,71.5123226 C96.2006587,73.5434839 94.5601078,75.1897419 92.535988,75.1897419" id="Fill-12" fill="#FEFEFE"></path>
+ <path d="M92.535988,75.1897419 L9.16167665,75.1897419 C7.13816766,75.1897419 5.49700599,73.5434839 5.49700599,71.5123226 L5.49700599,3.67741935 C5.49700599,1.64625806 7.13816766,0 9.16167665,0 L92.535988,0 C94.5601078,0 96.2006587,1.64625806 96.2006587,3.67741935 L96.2006587,71.5123226 C96.2006587,73.5434839 94.5601078,75.1897419 92.535988,75.1897419 Z" id="Stroke-13" stroke="#3098DC"></path>
+ <path d="M100.70576,75.6298065 L1.22155689,75.6298065 C0.546646707,75.6298065 0,75.0812581 0,74.404 L0,69.8709677 C0,69.1937097 0.546646707,68.6451613 1.22155689,68.6451613 L100.70576,68.6451613 C101.38006,68.6451613 101.927317,69.1937097 101.927317,69.8709677 L101.927317,74.404 C101.927317,75.0812581 101.38006,75.6298065 100.70576,75.6298065" id="Fill-14" fill="#D9F0FF"></path>
+ <path d="M100.70576,75.6298065 L1.22155689,75.6298065 C0.546646707,75.6298065 0,75.0812581 0,74.404 L0,69.8709677 C0,69.1937097 0.546646707,68.6451613 1.22155689,68.6451613 L100.70576,68.6451613 C101.38006,68.6451613 101.927317,69.1937097 101.927317,69.8709677 L101.927317,74.404 C101.927317,75.0812581 101.38006,75.6298065 100.70576,75.6298065 Z" id="Stroke-15" stroke="#3098DC"></path>
+ <polygon id="Fill-16" fill="#FEFEFE" points="9.77245509 63.6953548 92.1554731 63.6953548 92.1554731 4.90322581 9.77245509 4.90322581"></polygon>
+ <polygon id="Stroke-17" stroke="#3098DC" points="9.77245509 63.6953548 92.1554731 63.6953548 92.1554731 4.90322581 9.77245509 4.90322581"></polygon>
+ <path d="M53.4327111,51.4764454 L48.3815734,51.4764454 C48.2930105,51.4764454 48.2081123,51.4494776 48.1354296,51.3986067 L45.1462799,49.3073809 L43.8318847,48.2207035 L36.536747,50.2488002 C36.3101482,50.3113164 36.0719446,50.1770906 36.0059805,49.9490906 L33.8908548,42.6077357 C33.8664237,42.523768 33.8676452,42.4361228 33.8939087,42.352768 L36.1409626,35.2823164 L34.7923638,33.6826389 C34.7141841,33.5900906 34.6775374,33.4675099 34.6921961,33.346768 C34.7068548,33.226026 34.7709865,33.1157035 34.8693219,33.0446067 L35.1105793,32.8693164 L34.4368907,32.2484454 C34.3428308,32.1608002 34.2915255,32.0376067 34.2958009,31.9095099 C34.3006871,31.7808002 34.3611542,31.6612841 34.4607111,31.5822196 L34.8406153,31.2806712 L34.2218967,30.8044454 C34.1150105,30.7223164 34.0508787,30.5905422 34.0521003,30.4544776 C34.0533219,30.3178002 34.1180644,30.1872518 34.2273937,30.1057357 L34.6818129,29.7655744 L33.6019566,24.5289293 C33.5860763,24.4541551 33.5909626,24.3744776 33.6160045,24.3003164 L35.2620524,19.299026 C35.2993099,19.1862518 35.3805434,19.0930906 35.4862081,19.0428325 C35.5918728,18.9919615 35.7140284,18.9864454 35.8233578,19.0275099 L46.4826632,23.0506067 L55.3316213,23.0506067 L65.9903159,19.0275099 C66.0996452,18.9858325 66.2224117,18.9919615 66.3280763,19.0428325 C66.4343518,19.0937035 66.5155853,19.1868647 66.552232,19.299026 L68.1988907,24.3021551 C68.2227111,24.3732518 68.2275973,24.4517035 68.2123278,24.5277035 L67.1440763,29.7674131 L67.5911662,30.1081873 C67.6998847,30.1909293 67.7621841,30.3178002 67.7627949,30.4550906 C67.7634057,30.5905422 67.6998847,30.7210906 67.5936093,30.8038325 L66.9742799,31.2806712 L67.3535734,31.5822196 C67.453741,31.661897 67.5135973,31.7814131 67.5178728,31.9095099 C67.522759,32.0382196 67.4714536,32.1614131 67.3773937,32.2478325 L66.7037051,32.8693164 L66.9455734,33.0452196 C67.0432979,33.1157035 67.1074296,33.226026 67.1220883,33.3473809 C67.136747,33.4681228 67.0994895,33.5907035 67.0213099,33.6832518 L65.6739326,35.2817035 L67.9332021,42.3515422 C67.9600763,42.4336712 67.9612979,42.5225422 67.9374775,42.6071228 L65.8076931,49.9497035 C65.7423398,50.1770906 65.5035255,50.3100906 65.2775374,50.2488002 L57.9823997,48.2207035 L56.6283039,49.3368002 L53.6788548,51.3986067 C53.6061722,51.4494776 53.5206632,51.4764454 53.4327111,51.4764454" id="Fill-18" fill="#FEFEFE"></path>
+ <path d="M53.4327111,51.4764454 L48.3815734,51.4764454 C48.2930105,51.4764454 48.2081123,51.4494776 48.1354296,51.3986067 L45.1462799,49.3073809 L43.8318847,48.2207035 L36.536747,50.2488002 C36.3101482,50.3113164 36.0719446,50.1770906 36.0059805,49.9490906 L33.8908548,42.6077357 C33.8664237,42.523768 33.8676452,42.4361228 33.8939087,42.352768 L36.1409626,35.2823164 L34.7923638,33.6826389 C34.7141841,33.5900906 34.6775374,33.4675099 34.6921961,33.346768 C34.7068548,33.226026 34.7709865,33.1157035 34.8693219,33.0446067 L35.1105793,32.8693164 L34.4368907,32.2484454 C34.3428308,32.1608002 34.2915255,32.0376067 34.2958009,31.9095099 C34.3006871,31.7808002 34.3611542,31.6612841 34.4607111,31.5822196 L34.8406153,31.2806712 L34.2218967,30.8044454 C34.1150105,30.7223164 34.0508787,30.5905422 34.0521003,30.4544776 C34.0533219,30.3178002 34.1180644,30.1872518 34.2273937,30.1057357 L34.6818129,29.7655744 L33.6019566,24.5289293 C33.5860763,24.4541551 33.5909626,24.3744776 33.6160045,24.3003164 L35.2620524,19.299026 C35.2993099,19.1862518 35.3805434,19.0930906 35.4862081,19.0428325 C35.5918728,18.9919615 35.7140284,18.9864454 35.8233578,19.0275099 L46.4826632,23.0506067 L55.3316213,23.0506067 L65.9903159,19.0275099 C66.0996452,18.9858325 66.2224117,18.9919615 66.3280763,19.0428325 C66.4343518,19.0937035 66.5155853,19.1868647 66.552232,19.299026 L68.1988907,24.3021551 C68.2227111,24.3732518 68.2275973,24.4517035 68.2123278,24.5277035 L67.1440763,29.7674131 L67.5911662,30.1081873 C67.6998847,30.1909293 67.7621841,30.3178002 67.7627949,30.4550906 C67.7634057,30.5905422 67.6998847,30.7210906 67.5936093,30.8038325 L66.9742799,31.2806712 L67.3535734,31.5822196 C67.453741,31.661897 67.5135973,31.7814131 67.5178728,31.9095099 C67.522759,32.0382196 67.4714536,32.1614131 67.3773937,32.2478325 L66.7037051,32.8693164 L66.9455734,33.0452196 C67.0432979,33.1157035 67.1074296,33.226026 67.1220883,33.3473809 C67.136747,33.4681228 67.0994895,33.5907035 67.0213099,33.6832518 L65.6739326,35.2817035 L67.9332021,42.3515422 C67.9600763,42.4336712 67.9612979,42.5225422 67.9374775,42.6071228 L65.8076931,49.9497035 C65.7423398,50.1770906 65.5035255,50.3100906 65.2775374,50.2488002 L57.9823997,48.2207035 L56.6283039,49.3368002 L53.6788548,51.3986067 C53.6061722,51.4494776 53.5206632,51.4764454 53.4327111,51.4764454 Z" id="Stroke-19" stroke="#3098DC"></path>
+ <polygon id="Fill-20" fill="#3098DC" points="52.0480958 46.5806452 49.4779401 46.5806452 49.0320719 46.9355161 48.8622754 49.0745484 52.6637605 49.0745484 52.4933533 46.9355161"></polygon>
+ <path d="M54.8341371,38.1394234 L53.7713826,40.485617 C53.6852628,40.675617 53.8562808,40.8809396 54.0505083,40.8208751 L57.6742569,39.7017138 C57.8807,39.6379718 57.9014664,39.3425525 57.7054066,39.2493912 L55.1444125,38.0223589 C55.0277539,37.9665847 54.8891072,38.0186815 54.8341371,38.1394234" id="Fill-21" fill="#3098DC"></path>
+ <path d="M46.6690624,38.0223713 L44.1117331,39.2487906 C43.916284,39.3425648 43.9364397,39.6373713 44.1434936,39.7017261 L47.7666313,40.8208874 C47.9608589,40.8809519 48.1318768,40.6750164 48.0457571,40.4850164 L46.9793379,38.1388229 C46.9243678,38.0186939 46.7857211,37.9665971 46.6690624,38.0223713" id="Fill-22" fill="#3098DC"></path>
+ </g>
+ <g id="Group" transform="translate(0.000000, 9.000000)">
+ <path d="M66.9571429,59 L3.68571429,59 C1.65058571,59 0,57.3318526 0,55.2736842 L0,3.72631579 C0,1.66876842 1.65058571,0 3.68571429,0 L66.9571429,0 C68.9922714,0 70.6428571,1.66876842 70.6428571,3.72631579 L70.6428571,55.2736842 C70.6428571,57.3318526 68.9922714,59 66.9571429,59 Z" id="Stroke-1" stroke="#3098DC"></path>
+ <path d="M66.9571429,7.45263158 L3.68571429,7.45263158 C1.65058571,7.45263158 0,5.78448421 0,3.72631579 C0,1.66876842 1.65058571,0 3.68571429,0 L66.9571429,0 C68.9922714,0 70.6428571,1.66876842 70.6428571,3.72631579" id="Stroke-3" stroke="#3098DC"></path>
+ <path d="M66.9571429,7.45263158 C68.9922714,7.45263158 70.6428571,9.1214 70.6428571,11.1789474" id="Stroke-5" stroke="#3098DC"></path>
+ <polygon id="Stroke-7" stroke="#3098DC" points="70.6428571 23.5987579 85.5319143 23.5987579 85.5319143 19.8736842 70.6428571 19.8736842"></polygon>
+ <polygon id="Stroke-9" stroke="#3098DC" points="70.6428571 38.6530737 85.5319143 38.6530737 85.5319143 23.6 70.6428571 23.6"></polygon>
+ <path d="M35.2784286,22.3578947 L28.9666429,32.8356737 L35.2784286,36.5682 L41.5902143,32.8387789 L35.2784286,22.3578947 Z M35.1832143,37.7631053 L28.8714286,34.0336842 L35.1832143,42.9321263 L41.4986857,34.0336842 L35.1819857,37.7631053 L35.1832143,37.7631053 Z" id="Fill-23" fill="#3098DC"></path>
+ <path d="M50.4672571,32.9642316 C50.4672571,24.3626526 43.5700571,17.3894737 35.0622,17.3894737 C26.5543429,17.3894737 19.6571429,24.3626526 19.6571429,32.9642316 C19.6571429,41.5664316 26.5543429,48.5389895 35.0622,48.5389895 C43.5700571,48.5389895 50.4672571,41.5664316 50.4672571,32.9642316 Z" id="Stroke-24" stroke="#3098DC"></path>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/images/hardware-wallet-step-2.svg b/app/images/hardware-wallet-step-2.svg
new file mode 100644
index 000000000..9fff05b7e
--- /dev/null
+++ b/app/images/hardware-wallet-step-2.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="302px" height="98px" viewBox="0 0 302 98" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: sketchtool 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
+ <title>27B850D0-B3BA-4F98-8BB4-B542D8BFDE3B</title>
+ <desc>Created with sketchtool.</desc>
+ <defs>
+ <rect id="path-1" x="0" y="0" width="294" height="32"></rect>
+ <filter x="-2.4%" y="-15.6%" width="104.8%" height="143.8%" filterUnits="objectBoundingBox" id="filter-2">
+ <feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+ <feGaussianBlur stdDeviation="2" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+ <feComposite in="shadowBlurOuter1" in2="SourceAlpha" operator="out" result="shadowBlurOuter1"></feComposite>
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.123075181 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+ </filter>
+ </defs>
+ <g id="Import-account" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="hardware-connect" transform="translate(-361.000000, -797.000000)">
+ <g id="Group-10" transform="translate(356.000000, 717.000000)">
+ <g id="accounts" transform="translate(9.000000, 80.000000)">
+ <g id="Group-5" transform="translate(21.000000, 0.000000)">
+ <g id="Group-7" transform="translate(0.000000, 45.000000)">
+ <rect id="Rectangle-6-Copy" stroke="#AFDFFF" fill="#FFFFFF" x="0.5" y="0.5" width="250" height="26"></rect>
+ <text id="3" font-family="Roboto-Regular, Roboto" font-size="12" font-weight="normal" fill="#ABB4BB">
+ <tspan x="31" y="19">3</tspan>
+ </text>
+ <text id="OXz3…T3A4" font-family="Roboto-Regular, Roboto" font-size="12" font-weight="normal" fill="#ABB4BB">
+ <tspan x="50" y="19">OXz3…T3A4</tspan>
+ </text>
+ <text id="0.020000-ETH" font-family="Roboto-Regular, Roboto" font-size="12" font-weight="normal" fill="#ABB4BB">
+ <tspan x="142" y="19">0.020000 ETH</tspan>
+ </text>
+ <circle id="Oval-Copy" stroke="#AFDFFF" stroke-width="2" cx="16.5" cy="14.5" r="6.5"></circle>
+ </g>
+ <g id="Group-7-Copy">
+ <rect id="Rectangle-6-Copy" stroke="#AFDFFF" fill="#FFFFFF" x="0.5" y="0.5" width="250" height="26"></rect>
+ <text id="1" font-family="Roboto-Regular, Roboto" font-size="12" font-weight="normal" fill="#ABB4BB">
+ <tspan x="31" y="19">1</tspan>
+ </text>
+ <text id="OXa4…s0a2" font-family="Roboto-Regular, Roboto" font-size="12" font-weight="normal" fill="#ABB4BB">
+ <tspan x="50" y="19">OXa4…s0a2</tspan>
+ </text>
+ <text id="0.01500-ETH" font-family="Roboto-Regular, Roboto" font-size="12" font-weight="normal" fill="#ABB4BB">
+ <tspan x="142" y="19">0.01500 ETH</tspan>
+ </text>
+ <circle id="Oval-Copy" stroke="#AFDFFF" stroke-width="2" cx="16.5" cy="14.5" r="6.5"></circle>
+ </g>
+ <g id="Group-8" transform="translate(0.000000, 71.000000)">
+ <rect id="Rectangle-6-Copy-2" stroke="#AFDFFF" fill="#FFFFFF" x="0.5" y="0.5" width="250" height="26"></rect>
+ <text id="4" font-family="Roboto-Regular, Roboto" font-size="12" font-weight="normal" fill="#ABB4BB">
+ <tspan x="31" y="18">4</tspan>
+ </text>
+ <text id="OXd2…D0V4" font-family="Roboto-Regular, Roboto" font-size="12" font-weight="normal" fill="#ABB4BB">
+ <tspan x="50" y="18">OXd2…D0V4</tspan>
+ </text>
+ <text id="0.030000-ETH" font-family="Roboto-Regular, Roboto" font-size="12" font-weight="normal" fill="#ABB4BB">
+ <tspan x="142" y="18">0.030000 ETH</tspan>
+ </text>
+ <circle id="Oval-Copy-2" stroke="#AFDFFF" stroke-width="2" cx="16.5" cy="13.5" r="6.5"></circle>
+ </g>
+ </g>
+ <g id="Group-4" transform="translate(0.000000, 21.000000)">
+ <g id="Rectangle-6">
+ <use fill="black" fill-opacity="1" filter="url(#filter-2)" xlink:href="#path-1"></use>
+ <rect stroke="#3098DC" stroke-width="1" stroke-linejoin="square" fill="#D9F0FF" fill-rule="evenodd" x="0.5" y="0.5" width="293" height="31"></rect>
+ </g>
+ <text id="2" font-family="Roboto-Regular, Roboto" font-size="16" font-weight="normal" fill="#4A4A4A">
+ <tspan x="36" y="22">2</tspan>
+ </text>
+ <text id="OXe7…B0a1" font-family="Roboto-Regular, Roboto" font-size="16" font-weight="normal" fill="#4A4A4A">
+ <tspan x="58" y="22">OXe7…B0a1</tspan>
+ </text>
+ <text id="0.041000-ETH" font-family="Roboto-Regular, Roboto" font-size="16" font-weight="normal" fill="#4A4A4A">
+ <tspan x="166" y="22">0.041000 ETH</tspan>
+ </text>
+ <circle id="Oval" stroke="#3098DC" stroke-width="2" cx="19.5" cy="16.5" r="7.5"></circle>
+ <polyline id="Path-5" stroke="#3098DC" stroke-width="2" points="15 17 17.5495098 19.5495098 24 13.9042921"></polyline>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/images/hardware-wallet-step-3.svg b/app/images/hardware-wallet-step-3.svg
new file mode 100644
index 000000000..4a7655b3b
--- /dev/null
+++ b/app/images/hardware-wallet-step-3.svg
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="108px" height="85px" viewBox="0 0 108 85" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: sketchtool 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
+ <title>CEB55C41-7BCE-405E-83CD-834B388B495F</title>
+ <desc>Created with sketchtool.</desc>
+ <defs></defs>
+ <g id="Import-account" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="hardware-connect" transform="translate(-457.000000, -1057.000000)">
+ <g id="Group-11" transform="translate(356.000000, 929.000000)">
+ <g id="use-dapps" transform="translate(102.000000, 129.000000)">
+ <g id="Group-3">
+ <path d="M96.2021481,83 L9.79785192,83 C7.70080469,83 6,81.2922121 6,79.1851351 L6,8.81486493 C6,6.70778787 7.70080469,5 9.79785192,5 L96.2021481,5 C98.2998283,5 100,6.70778787 100,8.81486493 L100,79.1851351 C100,81.2922121 98.2998283,83 96.2021481,83" id="Fill-12" fill="#FEFEFE"></path>
+ <path d="M96.2021481,83 L9.79785192,83 C7.70080469,83 6,81.2922121 6,79.1851351 L6,8.81486493 C6,6.70778787 7.70080469,5 9.79785192,5 L96.2021481,5 C98.2998283,5 100,6.70778787 100,8.81486493 L100,79.1851351 C100,81.2922121 98.2998283,83 96.2021481,83 Z" id="Stroke-13" stroke="#3098DC"></path>
+ <path d="M104.729634,83 L1.27036631,83 C0.568488923,83 0,82.4502457 0,81.7714988 L0,77.2285012 C0,76.5497543 0.568488923,76 1.27036631,76 L104.729634,76 C105.430876,76 106,76.5497543 106,77.2285012 L106,81.7714988 C106,82.4502457 105.430876,83 104.729634,83" id="Fill-14" fill="#D9F0FF"></path>
+ <path d="M104.729634,83 L1.27036631,83 C0.568488923,83 0,82.4502457 0,81.7714988 L0,77.2285012 C0,76.5497543 0.568488923,76 1.27036631,76 L104.729634,76 C105.430876,76 106,76.5497543 106,77.2285012 L106,81.7714988 C106,82.4502457 105.430876,83 104.729634,83 Z" id="Stroke-15" stroke="#3098DC"></path>
+ <polygon id="Fill-16" fill="#FEFEFE" points="10 71 96 71 96 10 10 10"></polygon>
+ <polygon id="Stroke-17" stroke="#3098DC" points="10 71 96 71 96 10 10 10"></polygon>
+ <rect id="Rectangle-2" stroke="#3098DC" fill="#FFFFFF" x="14.5" y="14.5" width="77" height="53" rx="2"></rect>
+ <path d="M14.5,21.5 L91.5,21.5 L91.5,16 C91.5,15.1715729 90.8284271,14.5 90,14.5 L16,14.5 C15.1715729,14.5 14.5,15.1715729 14.5,16 L14.5,21.5 Z" id="Rectangle-3" stroke="#3098DC" fill="#FFFFFF"></path>
+ <g id="Group-6" transform="translate(17.000000, 17.000000)" fill="#D9F0FF" stroke="#3098DC">
+ <circle id="Oval-2" cx="1" cy="1" r="1"></circle>
+ <circle id="Oval-2" cx="5.76923077" cy="1" r="1"></circle>
+ <circle id="Oval-2" cx="10.5384615" cy="1" r="1"></circle>
+ </g>
+ <g id="metamask-outline" transform="translate(73.000000, 0.000000)">
+ <g id="Group-6">
+ <path d="M19.2986136,29.0578722 L14.6471457,29.0578722 C14.5655903,29.0578722 14.4874097,29.0337432 14.420478,28.988227 L11.6678439,27.1171303 L10.4574498,26.1448399 L3.73953772,27.9594528 C3.53086848,28.0153883 3.31151268,27.8952915 3.250768,27.6912915 L1.30300094,21.1227109 C1.28050291,21.0475819 1.28162782,20.9691625 1.3058132,20.8945819 L3.37506962,14.5683883 L2.1331783,13.137098 C2.0611846,13.0542915 2.02743755,12.9446141 2.04093637,12.8365819 C2.05443519,12.7285496 2.11349252,12.6298399 2.20404709,12.566227 L2.42621515,12.4093883 L1.80583194,11.8538722 C1.71921452,11.7754528 1.67196866,11.665227 1.67590581,11.5506141 C1.68040542,11.4354528 1.73608805,11.3285174 1.82776752,11.2577754 L2.17761191,10.987969 L1.60784927,10.5618722 C1.50942038,10.4883883 1.45036305,10.3704851 1.45148795,10.2487432 C1.45261285,10.1264528 1.51223264,10.0096464 1.61291132,9.9367109 L2.0313747,9.63235606 L1.03696173,4.94693671 C1.02233801,4.88003348 1.02683761,4.80874316 1.04989809,4.74238832 L2.56570295,0.267549611 C2.60001244,0.166646385 2.6748184,0.0832915464 2.77212238,0.0383238044 C2.86942637,-0.00719232461 2.98191652,-0.0121278085 3.08259521,0.024614127 L12.8984862,3.62422703 L21.0472731,3.62422703 L30.8626017,0.024614127 C30.9632804,-0.0126761956 31.076333,-0.00719232461 31.173637,0.0383238044 C31.2715034,0.0838399335 31.3463093,0.167194772 31.3800564,0.267549611 L32.8964237,4.74403348 C32.9183593,4.80764639 32.9228589,4.87783993 32.9087976,4.94583993 L31.9250712,9.63400122 L32.3367852,9.93890445 C32.4369014,10.0129367 32.4942714,10.1264528 32.4948338,10.2492915 C32.4953963,10.3704851 32.4369014,10.4872915 32.339035,10.5613238 L31.7687099,10.987969 L32.1179918,11.2577754 C32.2102337,11.3290657 32.2653539,11.4360012 32.2692911,11.5506141 C32.2737907,11.6657754 32.2265448,11.7760012 32.1399274,11.8533238 L31.5195442,12.4093883 L31.7422747,12.5667754 C31.8322668,12.6298399 31.8913241,12.7285496 31.904823,12.8371303 C31.9183218,12.9451625 31.8840123,13.0548399 31.8120186,13.1376464 L30.5712522,14.5678399 L32.6517576,20.8934851 C32.6765054,20.966969 32.6776303,21.0464851 32.6556948,21.1221625 L30.6944289,27.6918399 C30.6342467,27.8952915 30.4143284,28.0142915 30.2062216,27.9594528 L23.4883095,26.1448399 L22.2413561,27.1434528 L19.5252813,28.988227 C19.4583497,29.0337432 19.3796066,29.0578722 19.2986136,29.0578722 Z" id="Stroke-19" stroke="#3098DC" fill="#FFFFFF"></path>
+ <polygon id="Fill-20" fill="#3098DC" points="18.0235556 24.6774194 15.6567628 24.6774194 15.2461737 24.9949355 15.0898124 26.9088065 18.590506 26.9088065 18.4335823 24.9949355"></polygon>
+ <path d="M20.5891522,17.1247473 L19.6104879,19.2239731 C19.5311823,19.3939731 19.6886685,19.5776828 19.8675279,19.5239408 L23.2045484,18.522586 C23.3946567,18.4655537 23.4137801,18.2012312 23.2332334,18.1178763 L20.8748772,17.0200054 C20.7674491,16.9701021 20.6397728,17.016715 20.5891522,17.1247473" id="Fill-21" fill="#3098DC"></path>
+ <path d="M13.0701367,17.0200164 L10.7151553,18.117339 C10.535171,18.2012422 10.5537319,18.4650164 10.7444027,18.5225971 L14.0808608,19.5239519 C14.2597201,19.5776938 14.4172063,19.3934358 14.3379008,19.2234358 L13.3558617,17.12421 C13.3052411,17.0167261 13.1775648,16.9701132 13.0701367,17.0200164" id="Fill-22" fill="#3098DC"></path>
+ </g>
+ </g>
+ </g>
+ <text id="LOGIN-WITH-METAMASK" font-family="RobotoMono-Bold, Roboto Mono" font-size="10" font-weight="bold" fill="#3098DC">
+ <tspan x="24.4951172" y="43">LOGIN WITH </tspan>
+ <tspan x="30.4960938" y="56">METAMASK</tspan>
+ </text>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/images/loginglogo.svg b/app/images/loginglogo.svg
new file mode 100644
index 000000000..ca8b0a2ee
--- /dev/null
+++ b/app/images/loginglogo.svg
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="128px" height="128px" viewBox="0 0 128 128" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 49.3 (51167) - http://www.bohemiancoding.com/sketch -->
+ <title>logo2</title>
+ <desc>Created with Sketch.</desc>
+ <defs>
+ <polygon id="path-1" points="0 82.118 126.527 82.118 126.527 0 0 0"></polygon>
+ </defs>
+ <g id="logo2" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="Page-1" transform="translate(1.000000, 23.000000)">
+ <path d="M6.8811,74.2729 C6.6901,74.0489 6.4661,73.8719 6.2111,73.7389 C5.9551,73.6069 5.6861,73.5409 5.4031,73.5409 L0.0001,73.5409 L0.0001,82.0769 L0.9581,82.0769 L0.9581,74.4029 L5.2261,74.4029 C5.4261,74.4029 5.6071,74.4549 5.7661,74.5599 C5.9251,74.6649 6.0601,74.7969 6.1701,74.9569 C6.2791,75.1159 6.3631,75.2939 6.4231,75.4899 C6.4821,75.6859 6.5111,75.8759 6.5111,76.0579 C6.5111,76.2219 6.4891,76.3949 6.4431,76.5779 C6.3981,76.7599 6.3221,76.9289 6.2171,77.0839 C6.1121,77.2389 5.9831,77.3669 5.8281,77.4669 C5.6731,77.5669 5.4851,77.6179 5.2671,77.6179 L3.9941,77.6179 L3.9941,78.4789 L5.4311,78.4789 C5.7141,78.4789 5.9801,78.4129 6.2311,78.2809 C6.4821,78.1489 6.7031,77.9709 6.8951,77.7479 C7.0861,77.5239 7.2361,77.2679 7.3461,76.9809 C7.4561,76.6939 7.5101,76.3909 7.5101,76.0719 C7.5101,75.7149 7.4531,75.3829 7.3391,75.0729 C7.2251,74.7629 7.0721,74.4959 6.8811,74.2729" id="Fill-1" fill="#1B243D"></path>
+ <path d="M16.43,79.1157 C16.247,79.5207 15.999,79.8797 15.684,80.1887 C15.369,80.4997 15.005,80.7457 14.59,80.9277 C14.175,81.1107 13.735,81.2017 13.27,81.2017 C12.805,81.2017 12.365,81.1107 11.95,80.9277 C11.534,80.7457 11.172,80.4997 10.862,80.1887 C10.552,79.8797 10.305,79.5207 10.123,79.1157 C9.941,78.7097 9.85,78.2787 9.85,77.8227 C9.85,77.3757 9.941,76.9447 10.123,76.5297 C10.305,76.1147 10.552,75.7527 10.862,75.4417 C11.172,75.1327 11.537,74.8837 11.956,74.6967 C12.376,74.5097 12.818,74.4167 13.284,74.4167 C13.748,74.4167 14.188,74.5097 14.603,74.6967 C15.018,74.8837 15.381,75.1327 15.691,75.4417 C16.001,75.7527 16.247,76.1147 16.43,76.5297 C16.612,76.9447 16.703,77.3757 16.703,77.8227 C16.703,78.2787 16.612,78.7097 16.43,79.1157 M16.341,74.7647 C15.953,74.3777 15.495,74.0697 14.966,73.8417 C14.437,73.6137 13.872,73.4997 13.27,73.4997 C12.668,73.4997 12.102,73.6137 11.574,73.8417 C11.044,74.0697 10.586,74.3777 10.199,74.7647 C9.811,75.1527 9.503,75.6087 9.275,76.1337 C9.047,76.6577 8.933,77.2167 8.933,77.8087 C8.933,78.4017 9.047,78.9607 9.275,79.4847 C9.503,80.0097 9.811,80.4657 10.199,80.8527 C10.586,81.2407 11.044,81.5487 11.574,81.7757 C12.102,82.0047 12.668,82.1177 13.27,82.1177 C13.872,82.1177 14.437,82.0047 14.966,81.7757 C15.495,81.5487 15.953,81.2407 16.341,80.8527 C16.728,80.4657 17.036,80.0097 17.264,79.4847 C17.492,78.9607 17.606,78.4017 17.606,77.8087 C17.606,77.2167 17.492,76.6577 17.264,76.1337 C17.036,75.6087 16.728,75.1527 16.341,74.7647" id="Fill-3" fill="#1B243D"></path>
+ <path d="M26.2249,74.2729 C26.0329,74.0489 25.8089,73.8719 25.5549,73.7389 C25.2989,73.6069 25.0299,73.5409 24.7469,73.5409 L19.3439,73.5409 L19.3439,82.0769 L20.3009,82.0769 L20.3009,74.4029 L24.5699,74.4029 C24.7699,74.4029 24.9499,74.4549 25.1099,74.5599 C25.2689,74.6649 25.4039,74.7969 25.5139,74.9569 C25.6229,75.1159 25.7069,75.2939 25.7669,75.4899 C25.8259,75.6859 25.8549,75.8759 25.8549,76.0579 C25.8549,76.2219 25.8319,76.3949 25.7869,76.5779 C25.7409,76.7599 25.6659,76.9289 25.5609,77.0839 C25.4559,77.2389 25.3269,77.3669 25.1709,77.4669 C25.0169,77.5669 24.8289,77.6179 24.6109,77.6179 L23.2969,77.6179 L25.5819,82.0769 L26.6079,82.0769 L24.7879,78.4789 C25.0709,78.4789 25.3379,78.4129 25.5889,78.2809 C25.8389,78.1489 26.0579,77.9709 26.2459,77.7479 C26.4319,77.5239 26.5799,77.2679 26.6899,76.9809 C26.7989,76.6939 26.8539,76.3909 26.8539,76.0719 C26.8539,75.7149 26.7969,75.3829 26.6829,75.0729 C26.5689,74.7629 26.4159,74.4959 26.2249,74.2729" id="Fill-5" fill="#1B243D"></path>
+ <polygon id="Fill-7" fill="#1B243D" points="35.8694 73.5405 28.4134 73.5405 28.4134 74.4025 31.6554 74.4025 31.6554 82.0765 32.6134 82.0765 32.6134 74.4025 35.8694 74.4025"></polygon>
+ <polygon id="Fill-9" fill="#1B243D" points="35.3084 82.0766 41.4234 82.0766 40.8344 81.2156 36.8134 81.2156 39.7814 75.1406 43.2294 82.0766 44.2964 82.0766 39.7814 73.0896"></polygon>
+ <polygon id="Fill-11" fill="#1B243D" points="46.7859 73.5405 45.8279 73.5405 45.8279 82.0765 52.3269 82.0765 52.3269 81.2155 46.7859 81.2155"></polygon>
+ <polygon id="Fill-13" fill="#1B243D" points="66.0471 81.27 60.6441 73.541 59.0431 73.541 59.0431 82.077 60.0011 82.077 60.0011 74.334 65.5001 82.077 67.0051 82.077 67.0051 73.541 66.0471 73.541"></polygon>
+ <polygon id="Fill-15" fill="#1B243D" points="68.9202 82.0766 75.4182 82.0766 75.4182 81.2156 69.8782 81.2156 69.8782 74.4026 75.4182 74.4026 75.4182 73.5406 68.9202 73.5406"></polygon>
+ <mask id="mask-2" fill="white">
+ <use xlink:href="#path-1"></use>
+ </mask>
+ <g id="Clip-18"></g>
+ <polygon id="Fill-17" fill="#1B243D" mask="url(#mask-2)" points="71.218 78.479 75.309 78.479 75.309 77.618 71.218 77.618"></polygon>
+ <polygon id="Fill-19" fill="#1B243D" mask="url(#mask-2)" points="77.2923 74.4028 80.5343 74.4028 80.5343 82.0768 81.4923 82.0768 81.4923 74.4028 84.7483 74.4028 84.7483 73.5408 77.2923 73.5408"></polygon>
+ <polygon id="Fill-20" fill="#1B243D" mask="url(#mask-2)" points="95.8284 81.3657 91.6694 73.0897 87.5794 81.2977 87.5794 73.5407 86.6214 73.5407 86.6214 82.0767 88.2634 82.0767 91.6694 75.0997 95.1174 82.0767 96.7864 82.0767 96.7864 73.5407 95.8284 73.5407"></polygon>
+ <path d="M106.0203,79.1157 C105.8373,79.5207 105.5893,79.8797 105.2743,80.1887 C104.9603,80.4997 104.5953,80.7457 104.1803,80.9277 C103.7653,81.1107 103.3253,81.2017 102.8603,81.2017 C102.3953,81.2017 101.9553,81.1107 101.5403,80.9277 C101.1243,80.7457 100.7623,80.4997 100.4523,80.1887 C100.1423,79.8797 99.8953,79.5207 99.7133,79.1157 C99.5313,78.7097 99.4403,78.2787 99.4403,77.8227 C99.4403,77.3757 99.5313,76.9447 99.7133,76.5297 C99.8953,76.1147 100.1423,75.7527 100.4523,75.4417 C100.7623,75.1327 101.1273,74.8837 101.5463,74.6967 C101.9663,74.5097 102.4093,74.4167 102.8743,74.4167 C103.3393,74.4167 103.7793,74.5097 104.1943,74.6967 C104.6083,74.8837 104.9713,75.1327 105.2813,75.4417 C105.5913,75.7527 105.8373,76.1147 106.0203,76.5297 C106.2023,76.9447 106.2933,77.3757 106.2933,77.8227 C106.2933,78.2787 106.2023,78.7097 106.0203,79.1157 M105.9313,74.7647 C105.5433,74.3777 105.0853,74.0697 104.5563,73.8417 C104.0273,73.6137 103.4623,73.4997 102.8603,73.4997 C102.2583,73.4997 101.6923,73.6137 101.1643,73.8417 C100.6343,74.0697 100.1763,74.3777 99.7893,74.7647 C99.4013,75.1527 99.0933,75.6087 98.8653,76.1337 C98.6373,76.6577 98.5233,77.2167 98.5233,77.8087 C98.5233,78.4017 98.6373,78.9607 98.8653,79.4847 C99.0933,80.0097 99.4013,80.4657 99.7893,80.8527 C100.1763,81.2407 100.6343,81.5487 101.1643,81.7757 C101.6923,82.0047 102.2583,82.1177 102.8603,82.1177 C103.4623,82.1177 104.0273,82.0047 104.5563,81.7757 C105.0853,81.5487 105.5433,81.2407 105.9313,80.8527 C106.3193,80.4657 106.6263,80.0097 106.8543,79.4847 C107.0823,78.9607 107.1963,78.4017 107.1963,77.8087 C107.1963,77.2167 107.0823,76.6577 106.8543,76.1337 C106.6263,75.6087 106.3193,75.1527 105.9313,74.7647" id="Fill-21" fill="#1B243D" mask="url(#mask-2)"></path>
+ <path d="M115.8152,74.2729 C115.6232,74.0489 115.4002,73.8719 115.1452,73.7389 C114.8892,73.6069 114.6202,73.5409 114.3372,73.5409 L108.9342,73.5409 L108.9342,82.0769 L109.8912,82.0769 L109.8912,74.4029 L114.1602,74.4029 C114.3602,74.4029 114.5402,74.4549 114.7002,74.5599 C114.8592,74.6649 114.9942,74.7969 115.1042,74.9569 C115.2132,75.1159 115.2972,75.2939 115.3572,75.4899 C115.4162,75.6859 115.4452,75.8759 115.4452,76.0579 C115.4452,76.2219 115.4222,76.3949 115.3772,76.5779 C115.3312,76.7599 115.2562,76.9289 115.1512,77.0839 C115.0462,77.2389 114.9172,77.3669 114.7612,77.4669 C114.6072,77.5669 114.4192,77.6179 114.2012,77.6179 L112.8872,77.6179 L115.1722,82.0769 L116.1982,82.0769 L114.3782,78.4789 C114.6612,78.4789 114.9282,78.4129 115.1792,78.2809 C115.4292,78.1489 115.6482,77.9709 115.8362,77.7479 C116.0222,77.5239 116.1702,77.2679 116.2802,76.9809 C116.3892,76.6939 116.4442,76.3909 116.4442,76.0719 C116.4442,75.7149 116.3872,75.3829 116.2732,75.0729 C116.1592,74.7629 116.0062,74.4959 115.8152,74.2729" id="Fill-22" fill="#1B243D" mask="url(#mask-2)"></path>
+ <polygon id="Fill-23" fill="#1B243D" mask="url(#mask-2)" points="126.5266 73.5405 125.4866 73.5405 123.3666 77.8505 125.4866 82.0765 126.5266 82.0765 124.4336 77.8505"></polygon>
+ <polygon id="Fill-24" fill="#1B243D" mask="url(#mask-2)" points="119.2078 73.5405 118.2498 73.5405 118.2498 82.0765 119.2078 82.0765 119.2078 78.2735 122.1078 78.2735 122.1078 77.4115 119.2078 77.4115"></polygon>
+ <path d="M72.4627,48.06 C67.3487,48.06 61.5897,46.102 56.2467,42.545 C55.7657,42.225 55.2427,42.234 55.0787,42.564 C54.9137,42.894 55.1697,43.421 55.6507,43.741 C61.4477,47.6 67.6967,49.725 73.2447,49.725 C77.8417,49.725 81.5387,48.281 83.9357,45.548 C86.2857,42.87 87.1877,39.202 86.5457,34.942 C86.4797,34.504 86.0177,34.036 85.5137,33.896 C85.0107,33.756 84.6557,33.998 84.7217,34.436 C85.3137,38.362 84.4817,41.743 82.3157,44.211 C80.1077,46.73 76.6997,48.06 72.4627,48.06" id="Fill-25" fill="#1B243D" mask="url(#mask-2)"></path>
+ <path d="M84.5081,28.4746 C84.2921,28.0146 83.7441,27.6416 83.2851,27.6416 C82.8251,27.6416 82.6281,28.0146 82.8431,28.4746 C83.2451,29.3286 83.5941,30.1886 83.8801,31.0306 C84.0341,31.4866 84.5521,31.9036 85.0361,31.9626 C85.0741,31.9676 85.1121,31.9706 85.1481,31.9706 C85.5591,31.9706 85.7741,31.6646 85.6311,31.2456 C85.3211,30.3326 84.9441,29.4006 84.5081,28.4746" id="Fill-26" fill="#1B243D" mask="url(#mask-2)"></path>
+ <path d="M71.5769,13.6919 C65.5769,9.5209 59.0759,7.2239 53.2699,7.2239 C48.6269,7.2239 44.9069,8.6949 42.5129,11.4779 C40.1969,14.1689 39.3079,17.9979 40.0099,22.2589 C40.0819,22.6979 40.5479,23.1639 41.0509,23.2979 C41.1429,23.3219 41.2309,23.3339 41.3109,23.3339 C41.6679,23.3339 41.8889,23.1039 41.8299,22.7449 C41.1839,18.8179 42.0039,15.2889 44.1379,12.8079 C46.3439,10.2439 49.7719,8.8879 54.0529,8.8879 C59.4039,8.8879 65.3959,11.0049 70.9259,14.8499 C71.4009,15.1799 71.9309,15.1889 72.1109,14.8689 C72.2909,14.5489 72.0519,14.0219 71.5769,13.6919" id="Fill-27" fill="#1B243D" mask="url(#mask-2)"></path>
+ <path d="M42.6355,25.9184 C42.4805,25.4624 41.9635,25.0444 41.4795,24.9854 C40.9955,24.9264 40.7285,25.2474 40.8845,25.7034 C41.1935,26.6164 41.5715,27.5484 42.0075,28.4744 C42.2225,28.9344 42.7705,29.3064 43.2305,29.3064 C43.6905,29.3064 43.8875,28.9344 43.6715,28.4744 C43.2695,27.6194 42.9215,26.7594 42.6355,25.9184" id="Fill-28" fill="#1B243D" mask="url(#mask-2)"></path>
+ <path d="M48.9544,5.5986 C48.9954,5.5986 49.0354,5.5956 49.0734,5.5896 C50.1214,5.4216 51.2354,5.3376 52.3844,5.3376 C52.8434,5.3376 53.0404,4.9646 52.8254,4.5046 C52.6094,4.0456 52.0614,3.6726 51.6014,3.6726 C50.3714,3.6726 49.1784,3.7636 48.0554,3.9426 C47.6324,4.0106 47.5174,4.4336 47.7984,4.8886 C48.0544,5.3016 48.5494,5.5986 48.9544,5.5986" id="Fill-29" fill="#1B243D" mask="url(#mask-2)"></path>
+ <path d="M38.4558,28.4746 C41.5898,35.1436 47.1408,41.4016 54.0858,46.0956 C54.3238,46.2566 54.5728,46.3366 54.7848,46.3366 C54.9988,46.3366 55.1738,46.2546 55.2598,46.0896 C55.4298,45.7636 55.1798,45.2356 54.7008,44.9126 C48.2228,40.5336 43.0448,34.6966 40.1208,28.4746 C37.7008,23.3246 37.0648,18.4506 38.2838,14.3796 C39.4638,10.4356 42.3918,7.5286 46.5268,6.1936 C46.9118,6.0696 46.9538,5.6106 46.6218,5.1676 C46.2888,4.7246 45.7088,4.4676 45.3248,4.5906 C40.8908,6.0226 37.7518,9.1386 36.4868,13.3646 C35.1808,17.7296 35.8618,22.9536 38.4558,28.4746" id="Fill-30" fill="#1B243D" mask="url(#mask-2)"></path>
+ <path d="M77.4417,51.3588 C76.3937,51.5268 75.2797,51.6118 74.1307,51.6118 C73.6717,51.6118 73.4737,51.9838 73.6907,52.4438 C73.9057,52.9028 74.4537,53.2758 74.9137,53.2758 C76.1437,53.2758 77.3367,53.1848 78.4607,53.0048 C78.8827,52.9378 78.9977,52.5148 78.7167,52.0598 C78.4357,51.6058 77.8647,51.2918 77.4417,51.3588" id="Fill-31" fill="#1B243D" mask="url(#mask-2)"></path>
+ <path d="M88.0589,28.4741 C84.9249,21.8061 79.3749,15.5481 72.4319,10.8551 C71.9539,10.5311 71.4279,10.5341 71.2579,10.8601 C71.0879,11.1871 71.3379,11.7141 71.8159,12.0371 C78.2939,16.4161 83.4709,22.2531 86.3949,28.4741 C88.7639,33.5141 89.4229,38.3051 88.3009,42.3281 C87.2169,46.2181 84.4469,49.1491 80.5019,50.5781 C80.1259,50.7151 80.0999,51.1811 80.4419,51.6201 C80.7209,51.9771 81.1569,52.2061 81.5129,52.2061 C81.5939,52.2061 81.6709,52.1951 81.7419,52.1691 C85.9709,50.6351 88.9399,47.4951 90.1029,43.3251 C91.3049,39.0121 90.5989,33.8771 88.0589,28.4741" id="Fill-32" fill="#1B243D" mask="url(#mask-2)"></path>
+ <path d="M36.5716,28.4746 C36.3556,28.0146 35.8076,27.6416 35.3476,27.6416 C34.8886,27.6416 34.6906,28.0146 34.9066,28.4746 C37.7026,34.4226 42.1996,40.1156 47.9136,44.9396 C48.1826,45.1666 48.4956,45.2876 48.7556,45.2876 C48.9226,45.2876 49.0686,45.2376 49.1656,45.1336 C49.4136,44.8656 49.2566,44.3466 48.8136,43.9726 C43.4356,39.4326 39.2026,34.0736 36.5716,28.4746" id="Fill-33" fill="#1B243D" mask="url(#mask-2)"></path>
+ <path d="M56.8172,49.5649 C55.1282,48.6039 53.4612,47.5269 51.8632,46.3639 C51.3942,46.0229 50.8562,45.9969 50.6592,46.3039 C50.4632,46.6129 50.6842,47.1389 51.1522,47.4799 C52.8492,48.7149 54.6202,49.8589 56.4142,50.8789 C56.6212,50.9979 56.8282,51.0549 57.0062,51.0549 C57.2542,51.0549 57.4482,50.9439 57.5132,50.7329 C57.6242,50.3699 57.3132,49.8469 56.8172,49.5649" id="Fill-34" fill="#1B243D" mask="url(#mask-2)"></path>
+ <path d="M75.7991,55.1606 C70.9321,55.1606 65.5491,53.8386 60.2301,51.3366 C59.7241,51.0986 59.2721,51.2246 59.2191,51.6186 C59.1671,52.0126 59.5341,52.5236 60.0391,52.7616 C65.6911,55.4206 71.4111,56.8246 76.5821,56.8246 C77.0411,56.8246 77.2381,56.4526 77.0231,55.9926 C76.8061,55.5326 76.2591,55.1606 75.7991,55.1606" id="Fill-35" fill="#1B243D" mask="url(#mask-2)"></path>
+ <path d="M71.843,8.6733 C75.749,11.1523 79.3,14.1083 82.396,17.4603 C85.599,20.9283 88.139,24.6343 89.944,28.4743 C90.16,28.9343 90.707,29.3063 91.167,29.3063 C91.627,29.3063 91.824,28.9343 91.608,28.4743 C89.691,24.3953 86.993,20.4583 83.588,16.7723 C80.3,13.2113 76.528,10.0713 72.379,7.4383 C71.893,7.1303 71.379,7.1563 71.231,7.4983 C71.083,7.8393 71.357,8.3653 71.843,8.6733" id="Fill-36" fill="#1B243D" mask="url(#mask-2)"></path>
+ <path d="M64.1145,4.6572 C65.5935,5.2632 67.0835,5.9662 68.5425,6.7462 C68.7385,6.8512 68.9315,6.9012 69.0985,6.9012 C69.3585,6.9012 69.5575,6.7802 69.6135,6.5532 C69.7045,6.1782 69.3735,5.6592 68.8735,5.3912 C67.3235,4.5622 65.7405,3.8162 64.1695,3.1722 C63.6625,2.9652 63.2385,3.1282 63.2225,3.5382 C63.2075,3.9492 63.6075,4.4492 64.1145,4.6572" id="Fill-37" fill="#1B243D" mask="url(#mask-2)"></path>
+ <path d="M50.7161,1.788 C53.6641,1.788 56.7851,2.266 59.9921,3.207 C60.0951,3.237 60.1921,3.252 60.2811,3.252 C60.6281,3.252 60.8491,3.034 60.8041,2.687 C60.7471,2.252 60.2931,1.78 59.7881,1.631 C56.3801,0.631 53.0651,0.123 49.9331,0.123 C49.4741,0.123 49.2761,0.496 49.4921,0.956 C49.7081,1.415 50.2561,1.788 50.7161,1.788" id="Fill-38" fill="#1B243D" mask="url(#mask-2)"></path>
+ <path d="M36.7913,1.5722 C37.1013,1.3622 37.0253,0.8602 36.6233,0.4512 C36.2213,0.0432 35.6443,-0.1178 35.3333,0.0932 C32.3243,2.1362 30.1993,4.9382 29.0183,8.4202 C28.8973,8.7782 29.1993,9.3012 29.6923,9.5912 C29.9053,9.7152 30.1183,9.7762 30.3023,9.7762 C30.5443,9.7762 30.7353,9.6702 30.8043,9.4672 C31.9243,6.1662 33.9383,3.5102 36.7913,1.5722" id="Fill-39" fill="#1B243D" mask="url(#mask-2)"></path>
+ <path d="M29.3557,10.9082 C28.8527,10.6582 28.3887,10.7682 28.3197,11.1542 C28.1767,11.9612 28.0807,12.8002 28.0327,13.6492 C28.0107,14.0562 28.4037,14.5592 28.9107,14.7732 C29.0647,14.8382 29.2117,14.8692 29.3417,14.8692 C29.6417,14.8692 29.8547,14.7072 29.8707,14.4242 C29.9157,13.6192 30.0067,12.8242 30.1427,12.0592 C30.2107,11.6732 29.8587,11.1582 29.3557,10.9082" id="Fill-40" fill="#1B243D" mask="url(#mask-2)"></path>
+ <path d="M96.823,47.3579 C96.33,47.0689 95.832,47.1249 95.711,47.4819 C94.591,50.7829 92.576,53.4389 89.723,55.3769 C89.414,55.5869 89.489,56.0889 89.891,56.4979 C90.174,56.7849 90.542,56.9499 90.844,56.9499 C90.972,56.9499 91.089,56.9199 91.181,56.8569 C94.191,54.8119 96.316,52.0099 97.497,48.5289 C97.617,48.1709 97.316,47.6469 96.823,47.3579" id="Fill-41" fill="#1B243D" mask="url(#mask-2)"></path>
+ <path d="M97.6082,41.9389 C97.1012,41.7279 96.6742,41.8889 96.6562,42.2969 C96.6162,43.1809 96.5212,44.0529 96.3722,44.8889 C96.3042,45.2749 96.6562,45.7909 97.1592,46.0409 C97.3412,46.1299 97.5192,46.1739 97.6742,46.1739 C97.9462,46.1739 98.1512,46.0409 98.1942,45.7939 C98.3512,44.9129 98.4522,43.9929 98.4942,43.0589 C98.5122,42.6509 98.1162,42.1489 97.6082,41.9389" id="Fill-42" fill="#1B243D" mask="url(#mask-2)"></path>
+ <path d="M70.7937,44.5092 C61.9517,44.5092 51.3777,37.3162 47.2227,28.4742 C43.0667,19.6332 46.8797,12.4392 55.7217,12.4392 C64.5637,12.4392 75.1377,19.6332 79.2927,28.4742 C83.4477,37.3162 79.6357,44.5092 70.7937,44.5092 M54.9397,10.7752 C45.1797,10.7752 40.9717,18.7152 45.5577,28.4742 C50.1447,38.2342 61.8157,46.1742 71.5757,46.1742 C81.3357,46.1742 85.5437,38.2342 80.9577,28.4742 C76.3707,18.7152 64.6987,10.7752 54.9397,10.7752" id="Fill-43" fill="#1B243D" mask="url(#mask-2)"></path>
+ <path d="M36.9939,5.7876 C36.5479,5.4176 35.9899,5.3376 35.7469,5.6096 C34.2189,7.3236 33.1309,9.3756 32.5139,11.7096 C32.4159,12.0796 32.7409,12.6006 33.2389,12.8726 C33.4379,12.9816 33.6359,13.0346 33.8059,13.0346 C34.0619,13.0346 34.2599,12.9166 34.3179,12.6946 C34.8999,10.4966 35.9239,8.5646 37.3629,6.9516 C37.6049,6.6786 37.4399,6.1586 36.9939,5.7876" id="Fill-44" fill="#1B243D" mask="url(#mask-2)"></path>
+ <path d="M90.5696,51.5581 C92.2026,49.8011 93.3576,47.6751 94.0016,45.2401 C94.0996,44.8691 93.7756,44.3481 93.2766,44.0761 C92.7776,43.8041 92.2946,43.8831 92.1966,44.2541 C91.5906,46.5471 90.5026,48.5491 88.9656,50.2041 C88.7166,50.4701 88.8746,50.9911 89.3176,51.3641 C89.5866,51.5911 89.8996,51.7121 90.1596,51.7121 C90.3266,51.7121 90.4716,51.6621 90.5696,51.5581" id="Fill-45" fill="#1B243D" mask="url(#mask-2)"></path>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/images/logo.png b/app/images/logo.png
new file mode 100644
index 000000000..db6ba7d6c
--- /dev/null
+++ b/app/images/logo.png
Binary files differ
diff --git a/app/images/pw-128x128.png b/app/images/pw-128x128.png
new file mode 100644
index 000000000..a0eb1b730
--- /dev/null
+++ b/app/images/pw-128x128.png
Binary files differ
diff --git a/app/images/pw-48x48.png b/app/images/pw-48x48.png
new file mode 100644
index 000000000..a96c59e15
--- /dev/null
+++ b/app/images/pw-48x48.png
Binary files differ
diff --git a/app/images/pw128x128.png b/app/images/pw128x128.png
new file mode 100644
index 000000000..a0eb1b730
--- /dev/null
+++ b/app/images/pw128x128.png
Binary files differ
diff --git a/app/loading.html b/app/loading.html
new file mode 100644
index 000000000..aef5d9607
--- /dev/null
+++ b/app/loading.html
@@ -0,0 +1,35 @@
+<html>
+<head>
+ <title>MetaMask Loading</title>
+ <style>
+ #div-logo {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 256px;
+ }
+ #logo {
+ width: 100%;
+ animation: pulse 1s ease-in-out infinite;
+ }
+ @keyframes pulse {
+ 0% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ transform: scale(0.95, 0.95);
+ }
+ 100% {
+ opacity: 1;
+ }
+ }
+ </style>
+</head>
+<body>
+ <div id="div-logo">
+ <img id="logo" src="./images/loginglogo.svg">
+ </div>
+</body>
+</html>
diff --git a/app/manifest.json b/app/manifest.json
index 50b7e3c53..52256c5b7 100644
--- a/app/manifest.json
+++ b/app/manifest.json
@@ -59,7 +59,12 @@
"unlimitedStorage",
"clipboardWrite",
"http://localhost:8545/",
- "https://*.infura.io/"
+ "https://*.infura.io/",
+ "activeTab",
+ "webRequest",
+ "*://*.eth/",
+ "*://*.test/",
+ "notifications"
],
"web_accessible_resources": [
"inpage.js"
@@ -72,4 +77,4 @@
"*"
]
}
-} \ No newline at end of file
+}
diff --git a/app/scripts/background.js b/app/scripts/background.js
index e61d2f6ea..4cd6dac4c 100644
--- a/app/scripts/background.js
+++ b/app/scripts/background.js
@@ -26,6 +26,8 @@ const setupMetamaskMeshMetrics = require('./lib/setupMetamaskMeshMetrics')
const EdgeEncryptor = require('./edge-encryptor')
const getFirstPreferredLangCode = require('./lib/get-first-preferred-lang-code')
const getObjStructure = require('./lib/getObjStructure')
+const ipfsContent = require('./lib/ipfsContent.js')
+
const {
ENVIRONMENT_TYPE_POPUP,
ENVIRONMENT_TYPE_NOTIFICATION,
@@ -45,8 +47,8 @@ const notificationManager = new NotificationManager()
global.METAMASK_NOTIFIER = notificationManager
// setup sentry error reporting
-const release = platform.getVersion()
-const raven = setupRaven({ release })
+const releaseVersion = platform.getVersion()
+const raven = setupRaven({ releaseVersion })
// browser check if it is Edge - https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
// Internet Explorer 6-11
@@ -54,6 +56,7 @@ const isIE = !!document.documentMode
// Edge 20+
const isEdge = !isIE && !!window.StyleMedia
+let ipfsHandle
let popupIsOpen = false
let notificationIsOpen = false
const openMetamaskTabsIDs = {}
@@ -69,6 +72,7 @@ initialize().catch(log.error)
// setup metamask mesh testing container
setupMetamaskMeshMetrics()
+
/**
* An object representing a transaction, in whatever state it is in.
* @typedef TransactionMeta
@@ -158,6 +162,7 @@ async function initialize () {
const initLangCode = await getFirstPreferredLangCode()
await setupController(initState, initLangCode)
log.debug('MetaMask initialization complete.')
+ ipfsHandle = ipfsContent(initState.NetworkController.provider)
}
//
@@ -261,6 +266,11 @@ function setupController (initState, initLangCode) {
})
global.metamaskController = controller
+ controller.networkController.on('networkDidChange', () => {
+ ipfsHandle && ipfsHandle.remove()
+ ipfsHandle = ipfsContent(controller.networkController.providerStore.getState())
+ })
+
// report failed transactions to Sentry
controller.txController.on(`tx:status-update`, (txId, status) => {
if (status !== 'failed') return
diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js
index b35a70dd2..7b7114c35 100644
--- a/app/scripts/contentscript.js
+++ b/app/scripts/contentscript.js
@@ -177,6 +177,9 @@ function blacklistedDomainCheck () {
'cdn.shopify.com/s/javascripts/tricorder/xtld-read-only-frame.html',
'adyen.com',
'gravityforms.com',
+ 'harbourair.com',
+ 'ani.gamer.com.tw',
+ 'blueskybooking.com',
]
var currentUrl = window.location.href
var currentRegex
diff --git a/app/scripts/controllers/detect-tokens.js b/app/scripts/controllers/detect-tokens.js
new file mode 100644
index 000000000..195ec918a
--- /dev/null
+++ b/app/scripts/controllers/detect-tokens.js
@@ -0,0 +1,130 @@
+const Web3 = require('web3')
+const contracts = require('eth-contract-metadata')
+const { warn } = require('loglevel')
+const { MAINNET } = require('./network/enums')
+// By default, poll every 3 minutes
+const DEFAULT_INTERVAL = 180 * 1000
+const ERC20_ABI = [{'constant': true, 'inputs': [{'name': '_owner', 'type': 'address'}], 'name': 'balanceOf', 'outputs': [{'name': 'balance', 'type': 'uint256'}], 'payable': false, 'type': 'function'}]
+
+/**
+ * A controller that polls for token exchange
+ * rates based on a user's current token list
+ */
+class DetectTokensController {
+ /**
+ * Creates a DetectTokensController
+ *
+ * @param {Object} [config] - Options to configure controller
+ */
+ constructor ({ interval = DEFAULT_INTERVAL, preferences, network, keyringMemStore } = {}) {
+ this.preferences = preferences
+ this.interval = interval
+ this.network = network
+ this.keyringMemStore = keyringMemStore
+ }
+
+ /**
+ * For each token in eth-contract-metada, find check selectedAddress balance.
+ *
+ */
+ async detectNewTokens () {
+ if (!this.isActive) { return }
+ if (this._network.store.getState().provider.type !== MAINNET) { return }
+ this.web3.setProvider(this._network._provider)
+ for (const contractAddress in contracts) {
+ if (contracts[contractAddress].erc20 && !(this.tokenAddresses.includes(contractAddress.toLowerCase()))) {
+ this.detectTokenBalance(contractAddress)
+ }
+ }
+ }
+
+ /**
+ * Find if selectedAddress has tokens with contract in contractAddress.
+ *
+ * @param {string} contractAddress Hex address of the token contract to explore.
+ * @returns {boolean} If balance is detected, token is added.
+ *
+ */
+ async detectTokenBalance (contractAddress) {
+ const ethContract = this.web3.eth.contract(ERC20_ABI).at(contractAddress)
+ ethContract.balanceOf(this.selectedAddress, (error, result) => {
+ if (!error) {
+ if (!result.isZero()) {
+ this._preferences.addToken(contractAddress, contracts[contractAddress].symbol, contracts[contractAddress].decimals)
+ }
+ } else {
+ warn(`MetaMask - DetectTokensController balance fetch failed for ${contractAddress}.`, error)
+ }
+ })
+ }
+
+ /**
+ * Restart token detection polling period and call detectNewTokens
+ * in case of address change or user session initialization.
+ *
+ */
+ restartTokenDetection () {
+ if (!(this.isActive && this.selectedAddress)) { return }
+ this.detectNewTokens()
+ this.interval = DEFAULT_INTERVAL
+ }
+
+ /**
+ * @type {Number}
+ */
+ set interval (interval) {
+ this._handle && clearInterval(this._handle)
+ if (!interval) { return }
+ this._handle = setInterval(() => { this.detectNewTokens() }, interval)
+ }
+
+ /**
+ * In setter when selectedAddress is changed, detectNewTokens and restart polling
+ * @type {Object}
+ */
+ set preferences (preferences) {
+ if (!preferences) { return }
+ this._preferences = preferences
+ preferences.store.subscribe(({ tokens }) => { this.tokenAddresses = tokens.map((obj) => { return obj.address }) })
+ preferences.store.subscribe(({ selectedAddress }) => {
+ if (this.selectedAddress !== selectedAddress) {
+ this.selectedAddress = selectedAddress
+ this.restartTokenDetection()
+ }
+ })
+ }
+
+ /**
+ * @type {Object}
+ */
+ set network (network) {
+ if (!network) { return }
+ this._network = network
+ this.web3 = new Web3(network._provider)
+ }
+
+ /**
+ * In setter when isUnlocked is updated to true, detectNewTokens and restart polling
+ * @type {Object}
+ */
+ set keyringMemStore (keyringMemStore) {
+ if (!keyringMemStore) { return }
+ this._keyringMemStore = keyringMemStore
+ this._keyringMemStore.subscribe(({ isUnlocked }) => {
+ if (this.isUnlocked !== isUnlocked) {
+ this.isUnlocked = isUnlocked
+ if (isUnlocked) { this.restartTokenDetection() }
+ }
+ })
+ }
+
+ /**
+ * Internal isActive state
+ * @type {Object}
+ */
+ get isActive () {
+ return this.isOpen && this.isUnlocked
+ }
+}
+
+module.exports = DetectTokensController
diff --git a/app/scripts/controllers/network/enums.js b/app/scripts/controllers/network/enums.js
index 9da7f309c..3190eb37c 100644
--- a/app/scripts/controllers/network/enums.js
+++ b/app/scripts/controllers/network/enums.js
@@ -4,6 +4,7 @@ const KOVAN = 'kovan'
const MAINNET = 'mainnet'
const LOCALHOST = 'localhost'
+const MAINNET_CODE = 1
const ROPSTEN_CODE = 3
const RINKEYBY_CODE = 4
const KOVAN_CODE = 42
@@ -13,13 +14,13 @@ const RINKEBY_DISPLAY_NAME = 'Rinkeby'
const KOVAN_DISPLAY_NAME = 'Kovan'
const MAINNET_DISPLAY_NAME = 'Main Ethereum Network'
-
module.exports = {
ROPSTEN,
RINKEBY,
KOVAN,
MAINNET,
LOCALHOST,
+ MAINNET_CODE,
ROPSTEN_CODE,
RINKEYBY_CODE,
KOVAN_CODE,
diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js
index b314745f5..f6250dc16 100644
--- a/app/scripts/controllers/preferences.js
+++ b/app/scripts/controllers/preferences.js
@@ -86,6 +86,30 @@ class PreferencesController {
}
/**
+ * Removes an address from state
+ *
+ * @param {string} address A hex address
+ * @returns {string} the address that was removed
+ */
+ removeAddress (address) {
+ const identities = this.store.getState().identities
+ if (!identities[address]) {
+ throw new Error(`${address} can't be deleted cause it was not found`)
+ }
+ delete identities[address]
+ this.store.updateState({ identities })
+
+ // If the selected account is no longer valid,
+ // select an arbitrary other account:
+ if (address === this.getSelectedAddress()) {
+ const selected = Object.keys(identities)[0]
+ this.setSelectedAddress(selected)
+ }
+ return address
+ }
+
+
+ /**
* Adds addresses to the identities object without removing identities
*
* @param {string[]} addresses An array of hex addresses
diff --git a/app/scripts/controllers/transactions/nonce-tracker.js b/app/scripts/controllers/transactions/nonce-tracker.js
index a27568843..421036368 100644
--- a/app/scripts/controllers/transactions/nonce-tracker.js
+++ b/app/scripts/controllers/transactions/nonce-tracker.js
@@ -120,19 +120,6 @@ class NonceTracker {
return Number.isInteger(highest) ? highest + 1 : 0
}
- _reduceTxListToUniqueNonces (txList) {
- const reducedTxList = txList.reduce((reducedList, txMeta, index) => {
- if (!index) return [txMeta]
- const nonceMatches = txList.filter((txData) => {
- return txMeta.txParams.nonce === txData.txParams.nonce
- })
- if (nonceMatches.length > 1) return reducedList
- reducedList.push(txMeta)
- return reducedList
- }, [])
- return reducedTxList
- }
-
_getHighestNonce (txList) {
const nonces = txList.map((txMeta) => {
const nonce = txMeta.txParams.nonce
diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js
index 0aae4774b..28a18ca2e 100644
--- a/app/scripts/controllers/transactions/tx-state-manager.js
+++ b/app/scripts/controllers/transactions/tx-state-manager.js
@@ -288,6 +288,7 @@ class TransactionStateManager extends EventEmitter {
*/
setTxStatusRejected (txId) {
this._setTxStatus(txId, 'rejected')
+ this._removeTx(txId)
}
/**
@@ -422,6 +423,11 @@ class TransactionStateManager extends EventEmitter {
_saveTxList (transactions) {
this.store.updateState({ transactions })
}
+
+ _removeTx (txId) {
+ const transactionList = this.getFullTxList()
+ this._saveTxList(transactionList.filter((txMeta) => txMeta.id !== txId))
+ }
}
module.exports = TransactionStateManager
diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js
index 1a2354463..b7e2c7cbe 100644
--- a/app/scripts/lib/account-tracker.js
+++ b/app/scripts/lib/account-tracker.js
@@ -79,7 +79,7 @@ class AccountTracker {
})
this.addAccounts(accountsToAdd)
- this.removeAccounts(accountsToRemove)
+ this.removeAccount(accountsToRemove)
}
/**
@@ -108,7 +108,7 @@ class AccountTracker {
* @param {array} an array of hex addresses to stop tracking
*
*/
- removeAccounts (addresses) {
+ removeAccount (addresses) {
const accounts = this.store.getState().accounts
// remove each state object
addresses.forEach(address => {
diff --git a/app/scripts/lib/contracts/registrar.js b/app/scripts/lib/contracts/registrar.js
new file mode 100644
index 000000000..99ca24458
--- /dev/null
+++ b/app/scripts/lib/contracts/registrar.js
@@ -0,0 +1 @@
+module.exports = [{'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'resolver', 'outputs': [{'name': '', 'type': 'address'}], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'owner', 'outputs': [{'name': '', 'type': 'address'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'label', 'type': 'bytes32'}, {'name': 'owner', 'type': 'address'}], 'name': 'setSubnodeOwner', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'ttl', 'type': 'uint64'}], 'name': 'setTTL', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'ttl', 'outputs': [{'name': '', 'type': 'uint64'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'resolver', 'type': 'address'}], 'name': 'setResolver', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'owner', 'type': 'address'}], 'name': 'setOwner', 'outputs': [], 'payable': false, 'type': 'function'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'owner', 'type': 'address'}], 'name': 'Transfer', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': true, 'name': 'label', 'type': 'bytes32'}, {'indexed': false, 'name': 'owner', 'type': 'address'}], 'name': 'NewOwner', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'resolver', 'type': 'address'}], 'name': 'NewResolver', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'ttl', 'type': 'uint64'}], 'name': 'NewTTL', 'type': 'event'}]
diff --git a/app/scripts/lib/contracts/resolver.js b/app/scripts/lib/contracts/resolver.js
new file mode 100644
index 000000000..1bf3f90ce
--- /dev/null
+++ b/app/scripts/lib/contracts/resolver.js
@@ -0,0 +1,2 @@
+module.exports =
+[{'constant': true, 'inputs': [{'name': 'interfaceID', 'type': 'bytes4'}], 'name': 'supportsInterface', 'outputs': [{'name': '', 'type': 'bool'}], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'contentTypes', 'type': 'uint256'}], 'name': 'ABI', 'outputs': [{'name': 'contentType', 'type': 'uint256'}, {'name': 'data', 'type': 'bytes'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'x', 'type': 'bytes32'}, {'name': 'y', 'type': 'bytes32'}], 'name': 'setPubkey', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'content', 'outputs': [{'name': 'ret', 'type': 'bytes32'}], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'addr', 'outputs': [{'name': 'ret', 'type': 'address'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'contentType', 'type': 'uint256'}, {'name': 'data', 'type': 'bytes'}], 'name': 'setABI', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'name', 'outputs': [{'name': 'ret', 'type': 'string'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'name', 'type': 'string'}], 'name': 'setName', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'hash', 'type': 'bytes32'}], 'name': 'setContent', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'pubkey', 'outputs': [{'name': 'x', 'type': 'bytes32'}, {'name': 'y', 'type': 'bytes32'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'addr', 'type': 'address'}], 'name': 'setAddr', 'outputs': [], 'payable': false, 'type': 'function'}, {'inputs': [{'name': 'ensAddr', 'type': 'address'}], 'payable': false, 'type': 'constructor'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'a', 'type': 'address'}], 'name': 'AddrChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'hash', 'type': 'bytes32'}], 'name': 'ContentChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'name', 'type': 'string'}], 'name': 'NameChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': true, 'name': 'contentType', 'type': 'uint256'}], 'name': 'ABIChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'x', 'type': 'bytes32'}, {'indexed': false, 'name': 'y', 'type': 'bytes32'}], 'name': 'PubkeyChanged', 'type': 'event'}]
diff --git a/app/scripts/lib/ipfsContent.js b/app/scripts/lib/ipfsContent.js
new file mode 100644
index 000000000..5222151ea
--- /dev/null
+++ b/app/scripts/lib/ipfsContent.js
@@ -0,0 +1,44 @@
+const extension = require('extensionizer')
+const resolver = require('./resolver.js')
+
+module.exports = function (provider) {
+ function ipfsContent (details) {
+ const name = details.url.substring(7, details.url.length - 1)
+ let clearTime = null
+ extension.tabs.getSelected(null, tab => {
+ extension.tabs.update(tab.id, { url: 'loading.html' })
+
+ clearTime = setTimeout(() => {
+ return extension.tabs.update(tab.id, { url: '404.html' })
+ }, 60000)
+
+ resolver.resolve(name, provider).then(ipfsHash => {
+ clearTimeout(clearTime)
+ let url = 'https://ipfs.infura.io/ipfs/' + ipfsHash
+ return fetch(url, { method: 'HEAD' }).then(response => response.status).then(statusCode => {
+ if (statusCode !== 200) return extension.tabs.update(tab.id, { url: '404.html' })
+ extension.tabs.update(tab.id, { url: url })
+ })
+ .catch(err => {
+ url = 'https://ipfs.infura.io/ipfs/' + ipfsHash
+ extension.tabs.update(tab.id, {url: url})
+ return err
+ })
+ })
+ .catch(err => {
+ clearTimeout(clearTime)
+ const url = err === 'unsupport' ? 'unsupport' : 'error'
+ extension.tabs.update(tab.id, {url: `${url}.html?name=${name}`})
+ })
+ })
+ return { cancel: true }
+ }
+
+ extension.webRequest.onBeforeRequest.addListener(ipfsContent, {urls: ['*://*.eth/', '*://*.test/']})
+
+ return {
+ remove () {
+ extension.webRequest.onBeforeRequest.removeListener(ipfsContent)
+ },
+ }
+}
diff --git a/app/scripts/lib/resolver.js b/app/scripts/lib/resolver.js
new file mode 100644
index 000000000..ff0fed161
--- /dev/null
+++ b/app/scripts/lib/resolver.js
@@ -0,0 +1,71 @@
+const namehash = require('eth-ens-namehash')
+const multihash = require('multihashes')
+const HttpProvider = require('ethjs-provider-http')
+const Eth = require('ethjs-query')
+const EthContract = require('ethjs-contract')
+const registrarAbi = require('./contracts/registrar')
+const resolverAbi = require('./contracts/resolver')
+
+function ens (name, provider) {
+ const eth = new Eth(new HttpProvider(getProvider(provider.type)))
+ const hash = namehash.hash(name)
+ const contract = new EthContract(eth)
+ const Registrar = contract(registrarAbi).at(getRegistrar(provider.type))
+ return new Promise((resolve, reject) => {
+ if (provider.type === 'mainnet' || provider.type === 'ropsten') {
+ Registrar.resolver(hash).then((address) => {
+ if (address === '0x0000000000000000000000000000000000000000') {
+ reject(null)
+ } else {
+ const Resolver = contract(resolverAbi).at(address['0'])
+ return Resolver.content(hash)
+ }
+ }).then((contentHash) => {
+ if (contentHash['0'] === '0x0000000000000000000000000000000000000000000000000000000000000000') reject(null)
+ if (contentHash.ret !== '0x') {
+ const hex = contentHash['0'].substring(2)
+ const buf = multihash.fromHexString(hex)
+ resolve(multihash.toB58String(multihash.encode(buf, 'sha2-256')))
+ } else {
+ reject(null)
+ }
+ })
+ } else {
+ return reject('unsupport')
+ }
+ })
+}
+
+function getProvider (type) {
+ switch (type) {
+ case 'mainnet':
+ return 'https://mainnet.infura.io/'
+ case 'ropsten':
+ return 'https://ropsten.infura.io/'
+ default:
+ return 'http://localhost:8545/'
+ }
+}
+
+function getRegistrar (type) {
+ switch (type) {
+ case 'mainnet':
+ return '0x314159265dd8dbb310642f98f50c066173c1259b'
+ case 'ropsten':
+ return '0x112234455c3a32fd11230c42e7bccd4a84e02010'
+ default:
+ return '0x0000000000000000000000000000000000000000'
+ }
+}
+
+module.exports.resolve = function (name, provider) {
+ const path = name.split('.')
+ const topLevelDomain = path[path.length - 1]
+ if (topLevelDomain === 'eth' || topLevelDomain === 'test') {
+ return ens(name, provider)
+ } else {
+ return new Promise((resolve, reject) => {
+ reject(null)
+ })
+ }
+}
diff --git a/app/scripts/lib/setupRaven.js b/app/scripts/lib/setupRaven.js
index 85a078bf3..9aa2dec0a 100644
--- a/app/scripts/lib/setupRaven.js
+++ b/app/scripts/lib/setupRaven.js
@@ -8,8 +8,10 @@ module.exports = setupRaven
// Setup raven / sentry remote error reporting
function setupRaven (opts) {
- const { release } = opts
+ const { releaseVersion } = opts
let ravenTarget
+ // detect brave
+ const isBrave = Boolean(window.chrome.ipcRenderer)
if (METAMASK_DEBUG) {
console.log('Setting up Sentry Remote Error Reporting: DEV')
@@ -20,9 +22,11 @@ function setupRaven (opts) {
}
const client = Raven.config(ravenTarget, {
- release,
+ releaseVersion,
transport: function (opts) {
+ opts.data.extra.isBrave = isBrave
const report = opts.data
+
try {
// handle error-like non-error exceptions
rewriteErrorLikeExceptions(report)
diff --git a/app/scripts/lib/util.js b/app/scripts/lib/util.js
index 7ceb9da3c..2b3fe3d6e 100644
--- a/app/scripts/lib/util.js
+++ b/app/scripts/lib/util.js
@@ -28,7 +28,7 @@ function getStack () {
*
*/
const getEnvironmentType = (url = window.location.href) => {
- if (url.match(/popup.html(?:\?.+)*$/)) {
+ if (url.match(/popup.html(?:#.*)*$/)) {
return ENVIRONMENT_TYPE_POPUP
} else if (url.match(/home.html(?:\?.+)*$/) || url.match(/home.html(?:#.*)*$/)) {
return ENVIRONMENT_TYPE_FULLSCREEN
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index 3d7268395..12daf1603 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -35,6 +35,7 @@ const TypedMessageManager = require('./lib/typed-message-manager')
const TransactionController = require('./controllers/transactions')
const BalancesController = require('./controllers/computed-balances')
const TokenRatesController = require('./controllers/token-rates')
+const DetectTokensController = require('./controllers/detect-tokens')
const ConfigManager = require('./lib/config-manager')
const nodeify = require('./lib/nodeify')
const accountImporter = require('./account-import-strategies')
@@ -46,6 +47,7 @@ const GWEI_BN = new BN('1000000000')
const percentile = require('percentile')
const seedPhraseVerifier = require('./lib/seed-phrase-verifier')
const log = require('loglevel')
+const TrezorKeyring = require('eth-trezor-keyring')
module.exports = class MetamaskController extends EventEmitter {
@@ -124,7 +126,9 @@ module.exports = class MetamaskController extends EventEmitter {
})
// key mgmt
+ const additionalKeyrings = [TrezorKeyring]
this.keyringController = new KeyringController({
+ keyringTypes: additionalKeyrings,
initState: initState.KeyringController,
getNetwork: this.networkController.getNetworkState.bind(this.networkController),
encryptor: opts.encryptor || undefined,
@@ -144,6 +148,13 @@ module.exports = class MetamaskController extends EventEmitter {
this.accountTracker.syncWithAddresses(addresses)
})
+ // detect tokens controller
+ this.detectTokensController = new DetectTokensController({
+ preferences: this.preferencesController,
+ network: this.networkController,
+ keyringMemStore: this.keyringController.memStore,
+ })
+
// address book controller
this.addressBookController = new AddressBookController({
initState: initState.AddressBookController,
@@ -164,6 +175,13 @@ module.exports = class MetamaskController extends EventEmitter {
})
this.txController.on('newUnapprovedTx', opts.showUnapprovedTx.bind(opts))
+ this.txController.on(`tx:status-update`, (txId, status) => {
+ if (status === 'confirmed' || status === 'failed') {
+ const txMeta = this.txController.txStateManager.getTx(txId)
+ this.platform.showTransactionNotification(txMeta)
+ }
+ })
+
// computed balances (accounting for pending transactions)
this.balancesController = new BalancesController({
accountTracker: this.accountTracker,
@@ -332,6 +350,7 @@ module.exports = class MetamaskController extends EventEmitter {
markAccountsFound: this.markAccountsFound.bind(this),
markPasswordForgotten: this.markPasswordForgotten.bind(this),
unMarkPasswordForgotten: this.unMarkPasswordForgotten.bind(this),
+ getGasPrice: (cb) => cb(null, this.getGasPrice()),
// coinbase
buyEth: this.buyEth.bind(this),
@@ -344,8 +363,17 @@ module.exports = class MetamaskController extends EventEmitter {
verifySeedPhrase: nodeify(this.verifySeedPhrase, this),
clearSeedWordCache: this.clearSeedWordCache.bind(this),
resetAccount: nodeify(this.resetAccount, this),
+ removeAccount: nodeify(this.removeAccount, this),
importAccountWithStrategy: nodeify(this.importAccountWithStrategy, this),
+ // hardware wallets
+ connectHardware: nodeify(this.connectHardware, this),
+ forgetDevice: nodeify(this.forgetDevice, this),
+ checkHardwareStatus: nodeify(this.checkHardwareStatus, this),
+
+ // TREZOR
+ unlockTrezorAccount: nodeify(this.unlockTrezorAccount, this),
+
// vault management
submitPassword: nodeify(this.submitPassword, this),
@@ -502,6 +530,127 @@ module.exports = class MetamaskController extends EventEmitter {
}
//
+ // Hardware
+ //
+
+ /**
+ * Fetch account list from a trezor device.
+ *
+ * @returns [] accounts
+ */
+ async connectHardware (deviceName, page) {
+
+ switch (deviceName) {
+ case 'trezor':
+ const keyringController = this.keyringController
+ const oldAccounts = await keyringController.getAccounts()
+ let keyring = await keyringController.getKeyringsByType(
+ 'Trezor Hardware'
+ )[0]
+ if (!keyring) {
+ keyring = await this.keyringController.addNewKeyring('Trezor Hardware')
+ }
+ let accounts = []
+
+ switch (page) {
+ case -1:
+ accounts = await keyring.getPreviousPage()
+ break
+ case 1:
+ accounts = await keyring.getNextPage()
+ break
+ default:
+ accounts = await keyring.getFirstPage()
+ }
+
+ // Merge with existing accounts
+ // and make sure addresses are not repeated
+ const accountsToTrack = [...new Set(oldAccounts.concat(accounts.map(a => a.address.toLowerCase())))]
+ this.accountTracker.syncWithAddresses(accountsToTrack)
+ return accounts
+
+ default:
+ throw new Error('MetamaskController:connectHardware - Unknown device')
+ }
+ }
+
+ /**
+ * Check if the device is unlocked
+ *
+ * @returns {Promise<boolean>}
+ */
+ async checkHardwareStatus (deviceName) {
+
+ switch (deviceName) {
+ case 'trezor':
+ const keyringController = this.keyringController
+ const keyring = await keyringController.getKeyringsByType(
+ 'Trezor Hardware'
+ )[0]
+ if (!keyring) {
+ return false
+ }
+ return keyring.isUnlocked()
+ default:
+ throw new Error('MetamaskController:checkHardwareStatus - Unknown device')
+ }
+ }
+
+ /**
+ * Clear
+ *
+ * @returns {Promise<boolean>}
+ */
+ async forgetDevice (deviceName) {
+
+ switch (deviceName) {
+ case 'trezor':
+ const keyringController = this.keyringController
+ const keyring = await keyringController.getKeyringsByType(
+ 'Trezor Hardware'
+ )[0]
+ if (!keyring) {
+ throw new Error('MetamaskController:forgetDevice - Trezor Hardware keyring not found')
+ }
+ keyring.forgetDevice()
+ return true
+ default:
+ throw new Error('MetamaskController:forgetDevice - Unknown device')
+ }
+ }
+
+ /**
+ * Imports an account from a trezor device.
+ *
+ * @returns {} keyState
+ */
+ async unlockTrezorAccount (index) {
+ const keyringController = this.keyringController
+ const keyring = await keyringController.getKeyringsByType(
+ 'Trezor Hardware'
+ )[0]
+ if (!keyring) {
+ throw new Error('MetamaskController - No Trezor Hardware Keyring found')
+ }
+
+ keyring.setAccountToUnlock(index)
+ const oldAccounts = await keyringController.getAccounts()
+ const keyState = await keyringController.addNewAccount(keyring)
+ const newAccounts = await keyringController.getAccounts()
+ this.preferencesController.setAddresses(newAccounts)
+ newAccounts.forEach(address => {
+ if (!oldAccounts.includes(address)) {
+ this.preferencesController.setAccountLabel(address, `TREZOR #${parseInt(index, 10) + 1}`)
+ this.preferencesController.setSelectedAddress(address)
+ }
+ })
+
+ const { identities } = this.preferencesController.store.getState()
+ return { ...keyState, identities }
+ }
+
+
+ //
// Account Management
//
@@ -614,6 +763,23 @@ module.exports = class MetamaskController extends EventEmitter {
}
/**
+ * Removes an account from state / storage.
+ *
+ * @param {string[]} address A hex address
+ *
+ */
+ async removeAccount (address) {
+ // Remove account from the preferences controller
+ this.preferencesController.removeAddress(address)
+ // Remove account from the account tracker controller
+ this.accountTracker.removeAccount(address)
+ // Remove account from the keyring
+ await this.keyringController.removeAccount(address)
+ return address
+ }
+
+
+ /**
* Imports an account with the specified import strategy.
* These are defined in app/scripts/account-import-strategies
* Each strategy represents a different way of serializing an Ethereum key pair.
@@ -1235,10 +1401,12 @@ module.exports = class MetamaskController extends EventEmitter {
set isClientOpen (open) {
this._isClientOpen = open
this.isClientOpenAndUnlocked = this.getState().isUnlocked && open
+ this.detectTokensController.isOpen = open
}
/**
- * A method for activating the retrieval of price data, which should only be fetched when the UI is visible.
+ * A method for activating the retrieval of price data and auto detect tokens,
+ * which should only be fetched when the UI is visible.
* @private
* @param {boolean} active - True if price data should be getting fetched.
*/
diff --git a/app/scripts/migrations/027.js b/app/scripts/migrations/027.js
new file mode 100644
index 000000000..d6ebef580
--- /dev/null
+++ b/app/scripts/migrations/027.js
@@ -0,0 +1,35 @@
+// next version number
+const version = 27
+
+/*
+
+normalizes txParams on unconfirmed txs
+
+*/
+const clone = require('clone')
+
+module.exports = {
+ version,
+
+ migrate: async function (originalVersionedData) {
+ const versionedData = clone(originalVersionedData)
+ versionedData.meta.version = version
+ const state = versionedData.data
+ const newState = transformState(state)
+ versionedData.data = newState
+ return versionedData
+ },
+}
+
+function transformState (state) {
+ const newState = state
+
+ if (newState.TransactionController) {
+ if (newState.TransactionController.transactions) {
+ const transactions = newState.TransactionController.transactions
+ newState.TransactionController.transactions = transactions.filter((txMeta) => txMeta.status !== 'rejected')
+ }
+ }
+
+ return newState
+}
diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js
index 04d90bfff..bd0005221 100644
--- a/app/scripts/migrations/index.js
+++ b/app/scripts/migrations/index.js
@@ -37,4 +37,5 @@ module.exports = [
require('./024'),
require('./025'),
require('./026'),
+ require('./027'),
]
diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js
index f5cc255d1..0803164e8 100644
--- a/app/scripts/platforms/extension.js
+++ b/app/scripts/platforms/extension.js
@@ -1,4 +1,5 @@
const extension = require('extensionizer')
+const explorerLink = require('etherscan-link').createExplorerLink
class ExtensionPlatform {
@@ -13,12 +14,21 @@ class ExtensionPlatform {
extension.tabs.create({ url })
}
+ closeCurrentWindow () {
+ return extension.windows.getCurrent((windowDetails) => {
+ return extension.windows.remove(windowDetails.id)
+ })
+ }
+
getVersion () {
return extension.runtime.getManifest().version
}
- openExtensionInBrowser () {
- const extensionURL = extension.runtime.getURL('home.html')
+ openExtensionInBrowser (route = null) {
+ let extensionURL = extension.runtime.getURL('home.html')
+ if (route) {
+ extensionURL += `#${route}`
+ }
this.openWindow({ url: extensionURL })
}
@@ -31,6 +41,59 @@ class ExtensionPlatform {
cb(e)
}
}
+
+ showTransactionNotification (txMeta) {
+
+ const status = txMeta.status
+ if (status === 'confirmed') {
+ this._showConfirmedTransaction(txMeta)
+ } else if (status === 'failed') {
+ this._showFailedTransaction(txMeta)
+ }
+ }
+
+ _showConfirmedTransaction (txMeta) {
+
+ this._subscribeToNotificationClicked()
+
+ const url = explorerLink(txMeta.hash, parseInt(txMeta.metamaskNetworkId))
+ const nonce = parseInt(txMeta.txParams.nonce, 16)
+
+ const title = 'Confirmed transaction'
+ const message = `Transaction ${nonce} confirmed! View on EtherScan`
+ this._showNotification(title, message, url)
+ }
+
+ _showFailedTransaction (txMeta) {
+
+ const nonce = parseInt(txMeta.txParams.nonce, 16)
+ const title = 'Failed transaction'
+ const message = `Transaction ${nonce} failed! ${txMeta.err.message}`
+ this._showNotification(title, message)
+ }
+
+ _showNotification (title, message, url) {
+ extension.notifications.create(
+ url,
+ {
+ 'type': 'basic',
+ 'title': title,
+ 'iconUrl': extension.extension.getURL('../../images/icon-64.png'),
+ 'message': message,
+ })
+ }
+
+ _subscribeToNotificationClicked () {
+ if (!extension.notifications.onClicked.hasListener(this._viewOnEtherScan)) {
+ extension.notifications.onClicked.addListener(this._viewOnEtherScan)
+ }
+ }
+
+ _viewOnEtherScan (txId) {
+ if (txId.startsWith('http://')) {
+ global.metamaskController.platform.openWindow({ url: txId })
+ }
+ }
}
module.exports = ExtensionPlatform
diff --git a/app/scripts/ui.js b/app/scripts/ui.js
index 9bf97be87..da100f928 100644
--- a/app/scripts/ui.js
+++ b/app/scripts/ui.js
@@ -64,7 +64,6 @@ async function start () {
css = betaUIState ? NewMetaMaskUiCss() : OldMetaMaskUiCss()
deleteInjectedCss = injectCss(css)
}
- if (state.appState.shouldClose) notificationManager.closePopup()
})
})
diff --git a/app/unsupport.html b/app/unsupport.html
new file mode 100644
index 000000000..6f514eb17
--- /dev/null
+++ b/app/unsupport.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <title>MetaMask</title>
+ </head>
+ <style>
+ *{
+ padding: 0;
+ margin: 0;
+ box-sizing: border-box;
+ }
+ img{
+ display: block;
+ }
+ html, body{
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ }
+ @keyframes logoAmin{
+ from {transform: scale(1);}
+ 50%{transform: scale(1.1);}
+ to {transform: scale(1);}
+ }
+ .unsupport{
+ width: 80%;
+ height: auto;
+ overflow: hidden;
+ padding: 10px;
+ }
+ .unsupport > img{
+ margin: 0 auto 31px auto;
+ width: 136px;
+ height: auto;
+ animation: logoAmin 1s infinite linear;
+ }
+ .unsupport > h1{
+ text-align: center;
+ font-family: Gotham;
+ font-size: 18px;
+ font-weight: 500;
+ font-style: normal;
+ font-stretch: normal;
+ line-height: normal;
+ letter-spacing: 1.3px;
+ color: #33559f;
+ }
+
+ </style>
+ <body>
+ <div class="unsupport">
+ <img src="./images/cancel.png" alt="">
+ <h1>ENS resolver only support on Ethereum mainnet</h1>
+ </div>
+ </body>
+</html> \ No newline at end of file
diff --git a/development/backGroundConnectionModifiers.js b/development/backGroundConnectionModifiers.js
index 665f72898..aee68854b 100644
--- a/development/backGroundConnectionModifiers.js
+++ b/development/backGroundConnectionModifiers.js
@@ -10,7 +10,7 @@ module.exports = {
signPersonalMessage: (msgData, cb) => {
const stateUpdate = {
unapprovedPersonalMsgs: {},
- unapprovedPersonalMsgsCount: 0,
+ unapprovedPersonalMsgCount: 0,
}
return cb(null, stateUpdate)
},
diff --git a/development/states/conf-tx.json b/development/states/conf-tx.json
index 57f667cb9..d47b26fd4 100644
--- a/development/states/conf-tx.json
+++ b/development/states/conf-tx.json
@@ -56,7 +56,7 @@
"read": true,
"date": "Thu Feb 09 2017",
"title": "Terms of Use",
- "body": "# Terms of Use #\n\n**THIS AGREEMENT IS SUBJECT TO BINDING ARBITRATION AND A WAIVER OF CLASS ACTION RIGHTS AS DETAILED IN SECTION 13. PLEASE READ THE AGREEMENT CAREFULLY.**\n\n_Our Terms of Use have been updated as of September 5, 2016_\n\n## 1. Acceptance of Terms ##\n\nMetaMask provides a platform for managing Ethereum (or \"ETH\") accounts, and allowing ordinary websites to interact with the Ethereum blockchain, while keeping the user in control over what transactions they approve, through our website located at[ ](http://metamask.io)[https://metamask.io/](https://metamask.io/) and browser plugin (the \"Site\") — which includes text, images, audio, code and other materials (collectively, the “Content”) and all of the features, and services provided. The Site, and any other features, tools, materials, or other services offered from time to time by MetaMask are referred to here as the “Service.” Please read these Terms of Use (the “Terms” or “Terms of Use”) carefully before using the Service. By using or otherwise accessing the Services, or clicking to accept or agree to these Terms where that option is made available, you (1) accept and agree to these Terms (2) consent to the collection, use, disclosure and other handling of information as described in our Privacy Policy and (3) any additional terms, rules and conditions of participation issued by MetaMask from time to time. If you do not agree to the Terms, then you may not access or use the Content or Services.\n\n## 2. Modification of Terms of Use ##\n\nExcept for Section 13, providing for binding arbitration and waiver of class action rights, MetaMask reserves the right, at its sole discretion, to modify or replace the Terms of Use at any time. The most current version of these Terms will be posted on our Site. You shall be responsible for reviewing and becoming familiar with any such modifications. Use of the Services by you after any modification to the Terms constitutes your acceptance of the Terms of Use as modified.\n\n\n\n## 3. Eligibility ##\n\nYou hereby represent and warrant that you are fully able and competent to enter into the terms, conditions, obligations, affirmations, representations and warranties set forth in these Terms and to abide by and comply with these Terms.\n\nMetaMask is a global platform and by accessing the Content or Services, you are representing and warranting that, you are of the legal age of majority in your jurisdiction as is required to access such Services and Content and enter into arrangements as provided by the Service. You further represent that you are otherwise legally permitted to use the service in your jurisdiction including owning cryptographic tokens of value, and interacting with the Services or Content in any way. You further represent you are responsible for ensuring compliance with the laws of your jurisdiction and acknowledge that MetaMask is not liable for your compliance with such laws.\n\n## 4 Account Password and Security ##\n\nWhen setting up an account within MetaMask, you will be responsible for keeping your own account secrets, which may be a twelve-word seed phrase, an account file, or other locally stored secret information. MetaMask encrypts this information locally with a password you provide, that we never send to our servers. You agree to (a) never use the same password for MetaMask that you have ever used outside of this service; (b) keep your secret information and password confidential and do not share them with anyone else; (c) immediately notify MetaMask of any unauthorized use of your account or breach of security. MetaMask cannot and will not be liable for any loss or damage arising from your failure to comply with this section.\n\n## 5. Representations, Warranties, and Risks ##\n\n### 5.1. Warranty Disclaimer ###\n\nYou expressly understand and agree that your use of the Service is at your sole risk. The Service (including the Service and the Content) are provided on an \"AS IS\" and \"as available\" basis, without warranties of any kind, either express or implied, including, without limitation, implied warranties of merchantability, fitness for a particular purpose or non-infringement. You acknowledge that MetaMask has no control over, and no duty to take any action regarding: which users gain access to or use the Service; what effects the Content may have on you; how you may interpret or use the Content; or what actions you may take as a result of having been exposed to the Content. You release MetaMask from all liability for you having acquired or not acquired Content through the Service. MetaMask makes no representations concerning any Content contained in or accessed through the Service, and MetaMask will not be responsible or liable for the accuracy, copyright compliance, legality or decency of material contained in or accessed through the Service.\n\n### 5.2 Sophistication and Risk of Cryptographic Systems ###\n\nBy utilizing the Service or interacting with the Content or platform in any way, you represent that you understand the inherent risks associated with cryptographic systems; and warrant that you have an understanding of the usage and intricacies of native cryptographic tokens, like Ether (ETH) and Bitcoin (BTC), smart contract based tokens such as those that follow the Ethereum Token Standard (https://github.com/ethereum/EIPs/issues/20), and blockchain-based software systems.\n\n### 5.3 Risk of Regulatory Actions in One or More Jurisdictions ###\n\nMetaMask and ETH could be impacted by one or more regulatory inquiries or regulatory action, which could impede or limit the ability of MetaMask to continue to develop, or which could impede or limit your ability to access or use the Service or Ethereum blockchain.\n\n### 5.4 Risk of Weaknesses or Exploits in the Field of Cryptography ###\n\nYou acknowledge and understand that Cryptography is a progressing field. Advances in code cracking or technical advances such as the development of quantum computers may present risks to cryptocurrencies and Services of Content, which could result in the theft or loss of your cryptographic tokens or property. To the extent possible, MetaMask intends to update the protocol underlying Services to account for any advances in cryptography and to incorporate additional security measures, but does not guarantee or otherwise represent full security of the system. By using the Service or accessing Content, you acknowledge these inherent risks.\n\n### 5.5 Volatility of Crypto Currencies ###\n\nYou understand that Ethereum and other blockchain technologies and associated currencies or tokens are highly volatile due to many factors including but not limited to adoption, speculation, technology and security risks. You also acknowledge that the cost of transacting on such technologies is variable and may increase at any time causing impact to any activities taking place on the Ethereum blockchain. You acknowledge these risks and represent that MetaMask cannot be held liable for such fluctuations or increased costs.\n\n### 5.6 Application Security ###\n\nYou acknowledge that Ethereum applications are code subject to flaws and acknowledge that you are solely responsible for evaluating any code provided by the Services or Content and the trustworthiness of any third-party websites, products, smart-contracts, or Content you access or use through the Service. You further expressly acknowledge and represent that Ethereum applications can be written maliciously or negligently, that MetaMask cannot be held liable for your interaction with such applications and that such applications may cause the loss of property or even identity. This warning and others later provided by MetaMask in no way evidence or represent an on-going duty to alert you to all of the potential risks of utilizing the Service or Content.\n\n## 6. Indemnity ##\n\nYou agree to release and to indemnify, defend and hold harmless MetaMask and its parents, subsidiaries, affiliates and agencies, as well as the officers, directors, employees, shareholders and representatives of any of the foregoing entities, from and against any and all losses, liabilities, expenses, damages, costs (including attorneys’ fees and court costs) claims or actions of any kind whatsoever arising or resulting from your use of the Service, your violation of these Terms of Use, and any of your acts or omissions that implicate publicity rights, defamation or invasion of privacy. MetaMask reserves the right, at its own expense, to assume exclusive defense and control of any matter otherwise subject to indemnification by you and, in such case, you agree to cooperate with MetaMask in the defense of such matter.\n\n## 7. Limitation on liability ##\n\nYOU ACKNOWLEDGE AND AGREE THAT YOU ASSUME FULL RESPONSIBILITY FOR YOUR USE OF THE SITE AND SERVICE. YOU ACKNOWLEDGE AND AGREE THAT ANY INFORMATION YOU SEND OR RECEIVE DURING YOUR USE OF THE SITE AND SERVICE MAY NOT BE SECURE AND MAY BE INTERCEPTED OR LATER ACQUIRED BY UNAUTHORIZED PARTIES. YOU ACKNOWLEDGE AND AGREE THAT YOUR USE OF THE SITE AND SERVICE IS AT YOUR OWN RISK. RECOGNIZING SUCH, YOU UNDERSTAND AND AGREE THAT, TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, NEITHER METAMASK NOR ITS SUPPLIERS OR LICENSORS WILL BE LIABLE TO YOU FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY OR OTHER DAMAGES OF ANY KIND, INCLUDING WITHOUT LIMITATION DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA OR OTHER TANGIBLE OR INTANGIBLE LOSSES OR ANY OTHER DAMAGES BASED ON CONTRACT, TORT, STRICT LIABILITY OR ANY OTHER THEORY (EVEN IF METAMASK HAD BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES), RESULTING FROM THE SITE OR SERVICE; THE USE OR THE INABILITY TO USE THE SITE OR SERVICE; UNAUTHORIZED ACCESS TO OR ALTERATION OF YOUR TRANSMISSIONS OR DATA; STATEMENTS OR CONDUCT OF ANY THIRD PARTY ON THE SITE OR SERVICE; ANY ACTIONS WE TAKE OR FAIL TO TAKE AS A RESULT OF COMMUNICATIONS YOU SEND TO US; HUMAN ERRORS; TECHNICAL MALFUNCTIONS; FAILURES, INCLUDING PUBLIC UTILITY OR TELEPHONE OUTAGES; OMISSIONS, INTERRUPTIONS, LATENCY, DELETIONS OR DEFECTS OF ANY DEVICE OR NETWORK, PROVIDERS, OR SOFTWARE (INCLUDING, BUT NOT LIMITED TO, THOSE THAT DO NOT PERMIT PARTICIPATION IN THE SERVICE); ANY INJURY OR DAMAGE TO COMPUTER EQUIPMENT; INABILITY TO FULLY ACCESS THE SITE OR SERVICE OR ANY OTHER WEBSITE; THEFT, TAMPERING, DESTRUCTION, OR UNAUTHORIZED ACCESS TO, IMAGES OR OTHER CONTENT OF ANY KIND; DATA THAT IS PROCESSED LATE OR INCORRECTLY OR IS INCOMPLETE OR LOST; TYPOGRAPHICAL, PRINTING OR OTHER ERRORS, OR ANY COMBINATION THEREOF; OR ANY OTHER MATTER RELATING TO THE SITE OR SERVICE.\n\nSOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF CERTAIN WARRANTIES OR THE LIMITATION OR EXCLUSION OF LIABILITY FOR INCIDENTAL OR CONSEQUENTIAL DAMAGES. ACCORDINGLY, SOME OF THE ABOVE LIMITATIONS MAY NOT APPLY TO YOU.\n\n## 8. Our Proprietary Rights ##\n\nAll title, ownership and intellectual property rights in and to the Service are owned by MetaMask or its licensors. You acknowledge and agree that the Service contains proprietary and confidential information that is protected by applicable intellectual property and other laws. Except as expressly authorized by MetaMask, you agree not to copy, modify, rent, lease, loan, sell, distribute, perform, display or create derivative works based on the Service, in whole or in part. MetaMask issues a license for MetaMask, found [here](https://github.com/MetaMask/metamask-plugin/blob/master/LICENSE). For information on other licenses utilized in the development of MetaMask, please see our attribution page at: [https://metamask.io/attributions.html](https://metamask.io/attributions.html)\n\n## 9. Links ##\n\nThe Service provides, or third parties may provide, links to other World Wide Web or accessible sites, applications or resources. Because MetaMask has no control over such sites, applications and resources, you acknowledge and agree that MetaMask is not responsible for the availability of such external sites, applications or resources, and does not endorse and is not responsible or liable for any content, advertising, products or other materials on or available from such sites or resources. You further acknowledge and agree that MetaMask shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such content, goods or services available on or through any such site or resource.\n\n## 10. Termination and Suspension ##\n\nMetaMask may terminate or suspend all or part of the Service and your MetaMask access immediately, without prior notice or liability, if you breach any of the terms or conditions of the Terms. Upon termination of your access, your right to use the Service will immediately cease.\n\nThe following provisions of the Terms survive any termination of these Terms: INDEMNITY; WARRANTY DISCLAIMERS; LIMITATION ON LIABILITY; OUR PROPRIETARY RIGHTS; LINKS; TERMINATION; NO THIRD PARTY BENEFICIARIES; BINDING ARBITRATION AND CLASS ACTION WAIVER; GENERAL INFORMATION.\n\n## 11. No Third Party Beneficiaries ##\n\nYou agree that, except as otherwise expressly provided in these Terms, there shall be no third party beneficiaries to the Terms.\n\n## 12. Notice and Procedure For Making Claims of Copyright Infringement ##\n\nIf you believe that your copyright or the copyright of a person on whose behalf you are authorized to act has been infringed, please provide MetaMask’s Copyright Agent a written Notice containing the following information:\n\n· an electronic or physical signature of the person authorized to act on behalf of the owner of the copyright or other intellectual property interest;\n\n· a description of the copyrighted work or other intellectual property that you claim has been infringed;\n\n· a description of where the material that you claim is infringing is located on the Service;\n\n· your address, telephone number, and email address;\n\n· a statement by you that you have a good faith belief that the disputed use is not authorized by the copyright owner, its agent, or the law;\n\n· a statement by you, made under penalty of perjury, that the above information in your Notice is accurate and that you are the copyright or intellectual property owner or authorized to act on the copyright or intellectual property owner's behalf.\n\nMetaMask’s Copyright Agent can be reached at:\n\nEmail: copyright [at] metamask [dot] io\n\nMail:\n\nAttention:\n\nMetaMask Copyright ℅ ConsenSys\n\n49 Bogart Street\n\nBrooklyn, NY 11206\n\n## 13. Binding Arbitration and Class Action Waiver ##\n\nPLEASE READ THIS SECTION CAREFULLY – IT MAY SIGNIFICANTLY AFFECT YOUR LEGAL RIGHTS, INCLUDING YOUR RIGHT TO FILE A LAWSUIT IN COURT\n\n### 13.1 Initial Dispute Resolution ###\n\nThe parties shall use their best efforts to engage directly to settle any dispute, claim, question, or disagreement and engage in good faith negotiations which shall be a condition to either party initiating a lawsuit or arbitration.\n\n### 13.2 Binding Arbitration ###\n\nIf the parties do not reach an agreed upon solution within a period of 30 days from the time informal dispute resolution under the Initial Dispute Resolution provision begins, then either party may initiate binding arbitration as the sole means to resolve claims, subject to the terms set forth below. Specifically, all claims arising out of or relating to these Terms (including their formation, performance and breach), the parties’ relationship with each other and/or your use of the Service shall be finally settled by binding arbitration administered by the American Arbitration Association in accordance with the provisions of its Commercial Arbitration Rules and the supplementary procedures for consumer related disputes of the American Arbitration Association (the \"AAA\"), excluding any rules or procedures governing or permitting class actions.\n\nThe arbitrator, and not any federal, state or local court or agency, shall have exclusive authority to resolve all disputes arising out of or relating to the interpretation, applicability, enforceability or formation of these Terms, including, but not limited to any claim that all or any part of these Terms are void or voidable, or whether a claim is subject to arbitration. The arbitrator shall be empowered to grant whatever relief would be available in a court under law or in equity. The arbitrator’s award shall be written, and binding on the parties and may be entered as a judgment in any court of competent jurisdiction.\n\nThe parties understand that, absent this mandatory provision, they would have the right to sue in court and have a jury trial. They further understand that, in some instances, the costs of arbitration could exceed the costs of litigation and the right to discovery may be more limited in arbitration than in court.\n\n### 13.3 Location ###\n\nBinding arbitration shall take place in New York. You agree to submit to the personal jurisdiction of any federal or state court in New York County, New York, in order to compel arbitration, to stay proceedings pending arbitration, or to confirm, modify, vacate or enter judgment on the award entered by the arbitrator.\n\n### 13.4 Class Action Waiver ###\n\nThe parties further agree that any arbitration shall be conducted in their individual capacities only and not as a class action or other representative action, and the parties expressly waive their right to file a class action or seek relief on a class basis. YOU AND METAMASK AGREE THAT EACH MAY BRING CLAIMS AGAINST THE OTHER ONLY IN YOUR OR ITS INDIVIDUAL CAPACITY, AND NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED CLASS OR REPRESENTATIVE PROCEEDING. If any court or arbitrator determines that the class action waiver set forth in this paragraph is void or unenforceable for any reason or that an arbitration can proceed on a class basis, then the arbitration provision set forth above shall be deemed null and void in its entirety and the parties shall be deemed to have not agreed to arbitrate disputes.\n\n### 13.5 Exception - Litigation of Intellectual Property and Small Claims Court Claims ###\n\nNotwithstanding the parties' decision to resolve all disputes through arbitration, either party may bring an action in state or federal court to protect its intellectual property rights (\"intellectual property rights\" means patents, copyrights, moral rights, trademarks, and trade secrets, but not privacy or publicity rights). Either party may also seek relief in a small claims court for disputes or claims within the scope of that court’s jurisdiction.\n\n### 13.6 30-Day Right to Opt Out ###\n\nYou have the right to opt-out and not be bound by the arbitration and class action waiver provisions set forth above by sending written notice of your decision to opt-out to the following address: MetaMask ℅ ConsenSys, 49 Bogart Street, Brooklyn NY 11206 and via email at legal-opt@metamask.io. The notice must be sent within 30 days of September 6, 2016 or your first use of the Service, whichever is later, otherwise you shall be bound to arbitrate disputes in accordance with the terms of those paragraphs. If you opt-out of these arbitration provisions, MetaMask also will not be bound by them.\n\n### 13.7 Changes to This Section ###\n\nMetaMask will provide 60-days’ notice of any changes to this section. Changes will become effective on the 60th day, and will apply prospectively only to any claims arising after the 60th day.\n\nFor any dispute not subject to arbitration you and MetaMask agree to submit to the personal and exclusive jurisdiction of and venue in the federal and state courts located in New York, New York. You further agree to accept service of process by mail, and hereby waive any and all jurisdictional and venue defenses otherwise available.\n\nThe Terms and the relationship between you and MetaMask shall be governed by the laws of the State of New York without regard to conflict of law provisions.\n\n## 14. General Information ##\n\n### 14.1 Entire Agreement ###\n\nThese Terms (and any additional terms, rules and conditions of participation that MetaMask may post on the Service) constitute the entire agreement between you and MetaMask with respect to the Service and supersedes any prior agreements, oral or written, between you and MetaMask. In the event of a conflict between these Terms and the additional terms, rules and conditions of participation, the latter will prevail over the Terms to the extent of the conflict.\n\n### 14.2 Waiver and Severability of Terms ###\n\nThe failure of MetaMask to exercise or enforce any right or provision of the Terms shall not constitute a waiver of such right or provision. If any provision of the Terms is found by an arbitrator or court of competent jurisdiction to be invalid, the parties nevertheless agree that the arbitrator or court should endeavor to give effect to the parties' intentions as reflected in the provision, and the other provisions of the Terms remain in full force and effect.\n\n### 14.3 Statute of Limitations ###\n\nYou agree that regardless of any statute or law to the contrary, any claim or cause of action arising out of or related to the use of the Service or the Terms must be filed within one (1) year after such claim or cause of action arose or be forever barred.\n\n### 14.4 Section Titles ###\n\nThe section titles in the Terms are for convenience only and have no legal or contractual effect.\n\n### 14.5 Communications ###\n\nUsers with questions, complaints or claims with respect to the Service may contact us using the relevant contact information set forth above and at communications@metamask.io.\n\n## 15 Related Links ##\n\n**[Terms of Use](https://metamask.io/terms.html)**\n\n**[Privacy](https://metamask.io/privacy.html)**\n\n**[Attributions](https://metamask.io/attributions.html)**\n\n",
+ "body": "# Terms of Use #\n\n**THIS AGREEMENT IS SUBJECT TO BINDING ARBITRATION AND A WAIVER OF CLASS ACTION RIGHTS AS DETAILED IN SECTION 13. PLEASE READ THE AGREEMENT CAREFULLY.**\n\n_Our Terms of Use have been updated as of September 5, 2016_\n\n## 1. Acceptance of Terms ##\n\nMetaMask provides a platform for managing Ethereum (or \"ETH\") accounts, and allowing ordinary websites to interact with the Ethereum blockchain, while keeping the user in control over what transactions they approve, through our website located at[ ](http://metamask.io)[https://metamask.io/](https://metamask.io/) and browser plugin (the \"Site\") — which includes text, images, audio, code and other materials (collectively, the “Content”) and all of the features, and services provided. The Site, and any other features, tools, materials, or other services offered from time to time by MetaMask are referred to here as the “Service.” Please read these Terms of Use (the “Terms” or “Terms of Use”) carefully before using the Service. By using or otherwise accessing the Services, or clicking to accept or agree to these Terms where that option is made available, you (1) accept and agree to these Terms (2) consent to the collection, use, disclosure and other handling of information as described in our Privacy Policy and (3) any additional terms, rules and conditions of participation issued by MetaMask from time to time. If you do not agree to the Terms, then you may not access or use the Content or Services.\n\n## 2. Modification of Terms of Use ##\n\nExcept for Section 13, providing for binding arbitration and waiver of class action rights, MetaMask reserves the right, at its sole discretion, to modify or replace the Terms of Use at any time. The most current version of these Terms will be posted on our Site. You shall be responsible for reviewing and becoming familiar with any such modifications. Use of the Services by you after any modification to the Terms constitutes your acceptance of the Terms of Use as modified.\n\n\n\n## 3. Eligibility ##\n\nYou hereby represent and warrant that you are fully able and competent to enter into the terms, conditions, obligations, affirmations, representations and warranties set forth in these Terms and to abide by and comply with these Terms.\n\nMetaMask is a global platform and by accessing the Content or Services, you are representing and warranting that, you are of the legal age of majority in your jurisdiction as is required to access such Services and Content and enter into arrangements as provided by the Service. You further represent that you are otherwise legally permitted to use the service in your jurisdiction including owning cryptographic tokens of value, and interacting with the Services or Content in any way. You further represent you are responsible for ensuring compliance with the laws of your jurisdiction and acknowledge that MetaMask is not liable for your compliance with such laws.\n\n## 4 Account Password and Security ##\n\nWhen setting up an account within MetaMask, you will be responsible for keeping your own account secrets, which may be a twelve-word seed phrase, an account file, or other locally stored secret information. MetaMask encrypts this information locally with a password you provide, that we never send to our servers. You agree to (a) never use the same password for MetaMask that you have ever used outside of this service; (b) keep your secret information and password confidential and do not share them with anyone else; (c) immediately notify MetaMask of any unauthorized use of your account or breach of security. MetaMask cannot and will not be liable for any loss or damage arising from your failure to comply with this section.\n\n## 5. Representations, Warranties, and Risks ##\n\n### 5.1. Warranty Disclaimer ###\n\nYou expressly understand and agree that your use of the Service is at your sole risk. The Service (including the Service and the Content) are provided on an \"AS IS\" and \"as available\" basis, without warranties of any kind, either express or implied, including, without limitation, implied warranties of merchantability, fitness for a particular purpose or non-infringement. You acknowledge that MetaMask has no control over, and no duty to take any action regarding: which users gain access to or use the Service; what effects the Content may have on you; how you may interpret or use the Content; or what actions you may take as a result of having been exposed to the Content. You release MetaMask from all liability for you having acquired or not acquired Content through the Service. MetaMask makes no representations concerning any Content contained in or accessed through the Service, and MetaMask will not be responsible or liable for the accuracy, copyright compliance, legality or decency of material contained in or accessed through the Service.\n\n### 5.2 Sophistication and Risk of Cryptographic Systems ###\n\nBy utilizing the Service or interacting with the Content or platform in any way, you represent that you understand the inherent risks associated with cryptographic systems; and warrant that you have an understanding of the usage and intricacies of native cryptographic tokens, like Ether (ETH) and Bitcoin (BTC), smart contract based tokens such as those that follow the Ethereum Token Standard (https://github.com/ethereum/EIPs/issues/20), and blockchain-based software systems.\n\n### 5.3 Risk of Regulatory Actions in One or More Jurisdictions ###\n\nMetaMask and ETH could be impacted by one or more regulatory inquiries or regulatory action, which could impede or limit the ability of MetaMask to continue to develop, or which could impede or limit your ability to access or use the Service or Ethereum blockchain.\n\n### 5.4 Risk of Weaknesses or Exploits in the Field of Cryptography ###\n\nYou acknowledge and understand that Cryptography is a progressing field. Advances in code cracking or technical advances such as the development of quantum computers may present risks to cryptocurrencies and Services of Content, which could result in the theft or loss of your cryptographic tokens or property. To the extent possible, MetaMask intends to update the protocol underlying Services to account for any advances in cryptography and to incorporate additional security measures, but does not guarantee or otherwise represent full security of the system. By using the Service or accessing Content, you acknowledge these inherent risks.\n\n### 5.5 Volatility of Crypto Currencies ###\n\nYou understand that Ethereum and other blockchain technologies and associated currencies or tokens are highly volatile due to many factors including but not limited to adoption, speculation, technology and security risks. You also acknowledge that the cost of transacting on such technologies is variable and may increase at any time causing impact to any activities taking place on the Ethereum blockchain. You acknowledge these risks and represent that MetaMask cannot be held liable for such fluctuations or increased costs.\n\n### 5.6 Application Security ###\n\nYou acknowledge that Ethereum applications are code subject to flaws and acknowledge that you are solely responsible for evaluating any code provided by the Services or Content and the trustworthiness of any third-party websites, products, smart-contracts, or Content you access or use through the Service. You further expressly acknowledge and represent that Ethereum applications can be written maliciously or negligently, that MetaMask cannot be held liable for your interaction with such applications and that such applications may cause the loss of property or even identity. This warning and others later provided by MetaMask in no way evidence or represent an on-going duty to alert you to all of the potential risks of utilizing the Service or Content.\n\n## 6. Indemnity ##\n\nYou agree to release and to indemnify, defend and hold harmless MetaMask and its parents, subsidiaries, affiliates and agencies, as well as the officers, directors, employees, shareholders and representatives of any of the foregoing entities, from and against any and all losses, liabilities, expenses, damages, costs (including attorneys’ fees and court costs) claims or actions of any kind whatsoever arising or resulting from your use of the Service, your violation of these Terms of Use, and any of your acts or omissions that implicate publicity rights, defamation or invasion of privacy. MetaMask reserves the right, at its own expense, to assume exclusive defense and control of any matter otherwise subject to indemnification by you and, in such case, you agree to cooperate with MetaMask in the defense of such matter.\n\n## 7. Limitation on liability ##\n\nYOU ACKNOWLEDGE AND AGREE THAT YOU ASSUME FULL RESPONSIBILITY FOR YOUR USE OF THE SITE AND SERVICE. YOU ACKNOWLEDGE AND AGREE THAT ANY INFORMATION YOU SEND OR RECEIVE DURING YOUR USE OF THE SITE AND SERVICE MAY NOT BE SECURE AND MAY BE INTERCEPTED OR LATER ACQUIRED BY UNAUTHORIZED PARTIES. YOU ACKNOWLEDGE AND AGREE THAT YOUR USE OF THE SITE AND SERVICE IS AT YOUR OWN RISK. RECOGNIZING SUCH, YOU UNDERSTAND AND AGREE THAT, TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, NEITHER METAMASK NOR ITS SUPPLIERS OR LICENSORS WILL BE LIABLE TO YOU FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY OR OTHER DAMAGES OF ANY KIND, INCLUDING WITHOUT LIMITATION DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA OR OTHER TANGIBLE OR INTANGIBLE LOSSES OR ANY OTHER DAMAGES BASED ON CONTRACT, TORT, STRICT LIABILITY OR ANY OTHER THEORY (EVEN IF METAMASK HAD BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES), RESULTING FROM THE SITE OR SERVICE; THE USE OR THE INABILITY TO USE THE SITE OR SERVICE; UNAUTHORIZED ACCESS TO OR ALTERATION OF YOUR TRANSMISSIONS OR DATA; STATEMENTS OR CONDUCT OF ANY THIRD PARTY ON THE SITE OR SERVICE; ANY ACTIONS WE TAKE OR FAIL TO TAKE AS A RESULT OF COMMUNICATIONS YOU SEND TO US; HUMAN ERRORS; TECHNICAL MALFUNCTIONS; FAILURES, INCLUDING PUBLIC UTILITY OR TELEPHONE OUTAGES; OMISSIONS, INTERRUPTIONS, LATENCY, DELETIONS OR DEFECTS OF ANY DEVICE OR NETWORK, PROVIDERS, OR SOFTWARE (INCLUDING, BUT NOT LIMITED TO, THOSE THAT DO NOT PERMIT PARTICIPATION IN THE SERVICE); ANY INJURY OR DAMAGE TO COMPUTER EQUIPMENT; INABILITY TO FULLY ACCESS THE SITE OR SERVICE OR ANY OTHER WEBSITE; THEFT, TAMPERING, DESTRUCTION, OR UNAUTHORIZED ACCESS TO, IMAGES OR OTHER CONTENT OF ANY KIND; DATA THAT IS PROCESSED LATE OR INCORRECTLY OR IS INCOMPLETE OR LOST; TYPOGRAPHICAL, PRINTING OR OTHER ERRORS, OR ANY COMBINATION THEREOF; OR ANY OTHER MATTER RELATING TO THE SITE OR SERVICE.\n\nSOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF CERTAIN WARRANTIES OR THE LIMITATION OR EXCLUSION OF LIABILITY FOR INCIDENTAL OR CONSEQUENTIAL DAMAGES. ACCORDINGLY, SOME OF THE ABOVE LIMITATIONS MAY NOT APPLY TO YOU.\n\n## 8. Our Proprietary Rights ##\n\nAll title, ownership and intellectual property rights in and to the Service are owned by MetaMask or its licensors. You acknowledge and agree that the Service contains proprietary and confidential information that is protected by applicable intellectual property and other laws. Except as expressly authorized by MetaMask, you agree not to copy, modify, rent, lease, loan, sell, distribute, perform, display or create derivative works based on the Service, in whole or in part. MetaMask issues a license for MetaMask, found [here](https://github.com/MetaMask/metamask-plugin/blob/master/LICENSE). For information on other licenses utilized in the development of MetaMask, please see our attribution page at: [https://metamask.io/attributions.html](https://metamask.io/attributions.html)\n\n## 9. Links ##\n\nThe Service provides, or third parties may provide, links to other World Wide Web or accessible sites, applications or resources. Because MetaMask has no control over such sites, applications and resources, you acknowledge and agree that MetaMask is not responsible for the availability of such external sites, applications or resources, and does not endorse and is not responsible or liable for any content, advertising, products or other materials on or available from such sites or resources. You further acknowledge and agree that MetaMask shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such content, goods or services available on or through any such site or resource.\n\n## 10. Termination and Suspension ##\n\nMetaMask may terminate or suspend all or part of the Service and your MetaMask access immediately, without prior notice or liability, if you breach any of the terms or conditions of the Terms. Upon termination of your access, your right to use the Service will immediately cease.\n\nThe following provisions of the Terms survive any termination of these Terms: INDEMNITY; WARRANTY DISCLAIMERS; LIMITATION ON LIABILITY; OUR PROPRIETARY RIGHTS; LINKS; TERMINATION; NO THIRD PARTY BENEFICIARIES; BINDING ARBITRATION AND CLASS ACTION WAIVER; GENERAL INFORMATION.\n\n## 11. No Third Party Beneficiaries ##\n\nYou agree that, except as otherwise expressly provided in these Terms, there shall be no third party beneficiaries to the Terms.\n\n## 12. Notice and Procedure For Making Claims of Copyright Infringement ##\n\nIf you believe that your copyright or the copyright of a person on whose behalf you are authorized to act has been infringed, please provide MetaMask’s Copyright Agent a written Notice containing the following information:\n\n· an electronic or physical signature of the person authorized to act on behalf of the owner of the copyright or other intellectual property interest;\n\n· a description of the copyrighted work or other intellectual property that you claim has been infringed;\n\n· a description of where the material that you claim is infringing is located on the Service;\n\n· your address, telephone number, and email address;\n\n· a statement by you that you have a good faith belief that the disputed use is not authorized by the copyright owner, its agent, or the law;\n\n· a statement by you, made under penalty of perjury, that the above information in your Notice is accurate and that you are the copyright or intellectual property owner or authorized to act on the copyright or intellectual property owner's behalf.\n\nMetaMask’s Copyright Agent can be reached at:\n\nEmail: copyright [at] metamask [dot] io\n\nMail:\n\nAttention:\n\nMetaMask Copyright ℅ ConsenSys\n\n49 Bogart Street\n\nBrooklyn, NY 11206\n\n## 13. Binding Arbitration and Class Action Waiver ##\n\nPLEASE READ THIS SECTION CAREFULLY – IT MAY SIGNIFICANTLY AFFECT YOUR LEGAL RIGHTS, INCLUDING YOUR RIGHT TO FILE A LAWSUIT IN COURT\n\n### 13.1 Initial Dispute Resolution ###\n\nThe parties shall use their best efforts to engage directly to settle any dispute, claim, question, or disagreement and engage in good faith negotiations which shall be a condition to either party initiating a lawsuit or arbitration.\n\n### 13.2 Binding Arbitration ###\n\nIf the parties do not reach an agreed upon solution within a period of 30 days from the time informal dispute resolution under the Initial Dispute Resolution provision begins, then either party may initiate binding arbitration as the sole means to resolve claims, subject to the terms set forth below. Specifically, all claims arising out of or relating to these Terms (including their formation, performance and breach), the parties’ relationship with each other and/or your use of the Service shall be finally settled by binding arbitration administered by the American Arbitration Association in accordance with the provisions of its Commercial Arbitration Rules and the supplementary procedures for consumer related disputes of the American Arbitration Association (the \"AAA\"), excluding any rules or procedures governing or permitting class actions.\n\nThe arbitrator, and not any federal, state or local court or agency, shall have exclusive authority to resolve all disputes arising out of or relating to the interpretation, applicability, enforceability or formation of these Terms, including, but not limited to any claim that all or any part of these Terms are void or voidable, or whether a claim is subject to arbitration. The arbitrator shall be empowered to grant whatever relief would be available in a court under law or in equity. The arbitrator’s award shall be written, and binding on the parties and may be entered as a judgment in any court of competent jurisdiction.\n\nThe parties understand that, absent this mandatory provision, they would have the right to sue in court and have a jury trial. They further understand that, in some instances, the costs of arbitration could exceed the costs of litigation and the right to discovery may be more limited in arbitration than in court.\n\n### 13.3 Location ###\n\nBinding arbitration shall take place in New York. You agree to submit to the personal jurisdiction of any federal or state court in New York County, New York, in order to compel arbitration, to stay proceedings pending arbitration, or to confirm, modify, vacate or enter judgment on the award entered by the arbitrator.\n\n### 13.4 Class Action Waiver ###\n\nThe parties further agree that any arbitration shall be conducted in their individual capacities only and not as a class action or other representative action, and the parties expressly waive their right to file a class action or seek relief on a class basis. YOU AND METAMASK AGREE THAT EACH MAY BRING CLAIMS AGAINST THE OTHER ONLY IN YOUR OR ITS INDIVIDUAL CAPACITY, AND NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED CLASS OR REPRESENTATIVE PROCEEDING. If any court or arbitrator determines that the class action waiver set forth in this paragraph is void or unenforceable for any reason or that an arbitration can proceed on a class basis, then the arbitration provision set forth above shall be deemed null and void in its entirety and the parties shall be deemed to have not agreed to arbitrate disputes.\n\n### 13.5 Exception - Litigation of Intellectual Property and Small Claims Court Claims ###\n\nNotwithstanding the parties' decision to resolve all disputes through arbitration, either party may bring an action in state or federal court to protect its intellectual property rights (\"intellectual property rights\" means patents, copyrights, moral rights, trademarks, and trade secrets, but not privacy or publicity rights). Either party may also seek relief in a small claims court for disputes or claims within the scope of that court’s jurisdiction.\n\n### 13.6 30-Day Right to Opt Out ###\n\nYou have the right to opt-out and not be bound by the arbitration and class action waiver provisions set forth above by sending written notice of your decision to opt-out to the following address: MetaMask ℅ ConsenSys, 49 Bogart Street, Brooklyn NY 11206 and via email at support@metamask.io. The notice must be sent within 30 days of September 6, 2016 or your first use of the Service, whichever is later, otherwise you shall be bound to arbitrate disputes in accordance with the terms of those paragraphs. If you opt-out of these arbitration provisions, MetaMask also will not be bound by them.\n\n### 13.7 Changes to This Section ###\n\nMetaMask will provide 60-days’ notice of any changes to this section. Changes will become effective on the 60th day, and will apply prospectively only to any claims arising after the 60th day.\n\nFor any dispute not subject to arbitration you and MetaMask agree to submit to the personal and exclusive jurisdiction of and venue in the federal and state courts located in New York, New York. You further agree to accept service of process by mail, and hereby waive any and all jurisdictional and venue defenses otherwise available.\n\nThe Terms and the relationship between you and MetaMask shall be governed by the laws of the State of New York without regard to conflict of law provisions.\n\n## 14. General Information ##\n\n### 14.1 Entire Agreement ###\n\nThese Terms (and any additional terms, rules and conditions of participation that MetaMask may post on the Service) constitute the entire agreement between you and MetaMask with respect to the Service and supersedes any prior agreements, oral or written, between you and MetaMask. In the event of a conflict between these Terms and the additional terms, rules and conditions of participation, the latter will prevail over the Terms to the extent of the conflict.\n\n### 14.2 Waiver and Severability of Terms ###\n\nThe failure of MetaMask to exercise or enforce any right or provision of the Terms shall not constitute a waiver of such right or provision. If any provision of the Terms is found by an arbitrator or court of competent jurisdiction to be invalid, the parties nevertheless agree that the arbitrator or court should endeavor to give effect to the parties' intentions as reflected in the provision, and the other provisions of the Terms remain in full force and effect.\n\n### 14.3 Statute of Limitations ###\n\nYou agree that regardless of any statute or law to the contrary, any claim or cause of action arising out of or related to the use of the Service or the Terms must be filed within one (1) year after such claim or cause of action arose or be forever barred.\n\n### 14.4 Section Titles ###\n\nThe section titles in the Terms are for convenience only and have no legal or contractual effect.\n\n### 14.5 Communications ###\n\nUsers with questions, complaints or claims with respect to the Service may contact us using the relevant contact information set forth above and at communications@metamask.io.\n\n## 15 Related Links ##\n\n**[Terms of Use](https://metamask.io/terms.html)**\n\n**[Privacy](https://metamask.io/privacy.html)**\n\n**[Attributions](https://metamask.io/attributions.html)**\n\n",
"id": 0
},
"network": "3",
diff --git a/development/states/confirm-new-ui.json b/development/states/confirm-new-ui.json
index 71ccbd96c..2c2e17704 100644
--- a/development/states/confirm-new-ui.json
+++ b/development/states/confirm-new-ui.json
@@ -156,5 +156,23 @@
"fromDropdownOpen": false,
"toDropdownOpen": false,
"errors": {}
+ },
+ "confirmTransaction": {
+ "txData": {},
+ "tokenData": {},
+ "methodData": {},
+ "tokenProps": {
+ "tokenDecimals": "",
+ "tokenSymbol": ""
+ },
+ "fiatTransactionAmount": "",
+ "fiatTransactionFee": "",
+ "fiatTransactionTotal": "",
+ "ethTransactionAmount": "",
+ "ethTransactionFee": "",
+ "ethTransactionTotal": "",
+ "hexGasTotal": "",
+ "nonce": "",
+ "fetchingMethodData": false
}
}
diff --git a/development/states/confirm-sig-requests.json b/development/states/confirm-sig-requests.json
index 3c9caafb0..829f513a8 100644
--- a/development/states/confirm-sig-requests.json
+++ b/development/states/confirm-sig-requests.json
@@ -73,7 +73,7 @@
"from": "0x0d0c7188d9c72b019a5da9bca0d127680c22e658"
},
"status": "unapproved",
- "time": 1537889069339,
+ "time": 1537889070000,
"type": "eth_sign"
}
},
@@ -86,11 +86,11 @@
"from": "0x0d0c7188d9c72b019a5da9bca0d127680c22e659"
},
"status": "unapproved",
- "time": 1517889069339,
+ "time": 1537889065000,
"type": "personal_sign"
}
},
- "unapprovedPersonalMsgCount": 0,
+ "unapprovedPersonalMsgCount": 1 ,
"unapprovedTypedMessages": {
"8997167822566869": {
"id": 8997167822566869,
@@ -102,7 +102,7 @@
"from": "0x0d0c7188d9c72b019a5da9bca0d127680c22e659"
},
"status": "unapproved",
- "time": 1617889069339,
+ "time": 1537889060000,
"type": "eth_signTypedData"
}
},
@@ -172,5 +172,32 @@
"scrollToBottom": false,
"forgottenPassword": null
},
- "identities": {}
+ "identities": {},
+ "confirmTransaction": {
+ "txData": {
+ "id": 8927167822566864,
+ "msgParams": {
+ "data": "0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0",
+ "from": "0x0d0c7188d9c72b019a5da9bca0d127680c22e658"
+ },
+ "status": "unapproved",
+ "time": 1537889069339,
+ "type": "eth_sign"
+ },
+ "tokenData": {},
+ "methodData": {},
+ "tokenProps": {
+ "tokenDecimals": "",
+ "tokenSymbol": ""
+ },
+ "fiatTransactionAmount": "",
+ "fiatTransactionFee": "",
+ "fiatTransactionTotal": "",
+ "ethTransactionAmount": "",
+ "ethTransactionFee": "",
+ "ethTransactionTotal": "",
+ "hexGasTotal": "",
+ "nonce": "",
+ "fetchingMethodData": false
+ }
}
diff --git a/development/states/currency-localization.json b/development/states/currency-localization.json
index 8c8b18a91..6848c0840 100644
--- a/development/states/currency-localization.json
+++ b/development/states/currency-localization.json
@@ -130,5 +130,23 @@
"scrollToBottom": false,
"forgottenPassword": null
},
- "identities": {}
+ "identities": {},
+ "confirmTransaction": {
+ "txData": {},
+ "tokenData": {},
+ "methodData": {},
+ "tokenProps": {
+ "tokenDecimals": "",
+ "tokenSymbol": ""
+ },
+ "fiatTransactionAmount": "",
+ "fiatTransactionFee": "",
+ "fiatTransactionTotal": "",
+ "ethTransactionAmount": "",
+ "ethTransactionFee": "",
+ "ethTransactionTotal": "",
+ "hexGasTotal": "",
+ "nonce": "",
+ "fetchingMethodData": false
+ }
}
diff --git a/development/states/first-time.json b/development/states/first-time.json
index f10eefd35..f44148973 100644
--- a/development/states/first-time.json
+++ b/development/states/first-time.json
@@ -16,7 +16,7 @@
"read": false,
"date": "Thu Feb 09 2017",
"title": "Terms of Use",
- "body": "# Terms of Use #\n\n**THIS AGREEMENT IS SUBJECT TO BINDING ARBITRATION AND A WAIVER OF CLASS ACTION RIGHTS AS DETAILED IN SECTION 13. PLEASE READ THE AGREEMENT CAREFULLY.**\n\n_Our Terms of Use have been updated as of September 5, 2016_\n\n## 1. Acceptance of Terms ##\n\nMetaMask provides a platform for managing Ethereum (or \"ETH\") accounts, and allowing ordinary websites to interact with the Ethereum blockchain, while keeping the user in control over what transactions they approve, through our website located at[ ](http://metamask.io)[https://metamask.io/](https://metamask.io/) and browser plugin (the \"Site\") — which includes text, images, audio, code and other materials (collectively, the “Content”) and all of the features, and services provided. The Site, and any other features, tools, materials, or other services offered from time to time by MetaMask are referred to here as the “Service.” Please read these Terms of Use (the “Terms” or “Terms of Use”) carefully before using the Service. By using or otherwise accessing the Services, or clicking to accept or agree to these Terms where that option is made available, you (1) accept and agree to these Terms (2) consent to the collection, use, disclosure and other handling of information as described in our Privacy Policy and (3) any additional terms, rules and conditions of participation issued by MetaMask from time to time. If you do not agree to the Terms, then you may not access or use the Content or Services.\n\n## 2. Modification of Terms of Use ##\n\nExcept for Section 13, providing for binding arbitration and waiver of class action rights, MetaMask reserves the right, at its sole discretion, to modify or replace the Terms of Use at any time. The most current version of these Terms will be posted on our Site. You shall be responsible for reviewing and becoming familiar with any such modifications. Use of the Services by you after any modification to the Terms constitutes your acceptance of the Terms of Use as modified.\n\n\n\n## 3. Eligibility ##\n\nYou hereby represent and warrant that you are fully able and competent to enter into the terms, conditions, obligations, affirmations, representations and warranties set forth in these Terms and to abide by and comply with these Terms.\n\nMetaMask is a global platform and by accessing the Content or Services, you are representing and warranting that, you are of the legal age of majority in your jurisdiction as is required to access such Services and Content and enter into arrangements as provided by the Service. You further represent that you are otherwise legally permitted to use the service in your jurisdiction including owning cryptographic tokens of value, and interacting with the Services or Content in any way. You further represent you are responsible for ensuring compliance with the laws of your jurisdiction and acknowledge that MetaMask is not liable for your compliance with such laws.\n\n## 4 Account Password and Security ##\n\nWhen setting up an account within MetaMask, you will be responsible for keeping your own account secrets, which may be a twelve-word seed phrase, an account file, or other locally stored secret information. MetaMask encrypts this information locally with a password you provide, that we never send to our servers. You agree to (a) never use the same password for MetaMask that you have ever used outside of this service; (b) keep your secret information and password confidential and do not share them with anyone else; (c) immediately notify MetaMask of any unauthorized use of your account or breach of security. MetaMask cannot and will not be liable for any loss or damage arising from your failure to comply with this section.\n\n## 5. Representations, Warranties, and Risks ##\n\n### 5.1. Warranty Disclaimer ###\n\nYou expressly understand and agree that your use of the Service is at your sole risk. The Service (including the Service and the Content) are provided on an \"AS IS\" and \"as available\" basis, without warranties of any kind, either express or implied, including, without limitation, implied warranties of merchantability, fitness for a particular purpose or non-infringement. You acknowledge that MetaMask has no control over, and no duty to take any action regarding: which users gain access to or use the Service; what effects the Content may have on you; how you may interpret or use the Content; or what actions you may take as a result of having been exposed to the Content. You release MetaMask from all liability for you having acquired or not acquired Content through the Service. MetaMask makes no representations concerning any Content contained in or accessed through the Service, and MetaMask will not be responsible or liable for the accuracy, copyright compliance, legality or decency of material contained in or accessed through the Service.\n\n### 5.2 Sophistication and Risk of Cryptographic Systems ###\n\nBy utilizing the Service or interacting with the Content or platform in any way, you represent that you understand the inherent risks associated with cryptographic systems; and warrant that you have an understanding of the usage and intricacies of native cryptographic tokens, like Ether (ETH) and Bitcoin (BTC), smart contract based tokens such as those that follow the Ethereum Token Standard (https://github.com/ethereum/EIPs/issues/20), and blockchain-based software systems.\n\n### 5.3 Risk of Regulatory Actions in One or More Jurisdictions ###\n\nMetaMask and ETH could be impacted by one or more regulatory inquiries or regulatory action, which could impede or limit the ability of MetaMask to continue to develop, or which could impede or limit your ability to access or use the Service or Ethereum blockchain.\n\n### 5.4 Risk of Weaknesses or Exploits in the Field of Cryptography ###\n\nYou acknowledge and understand that Cryptography is a progressing field. Advances in code cracking or technical advances such as the development of quantum computers may present risks to cryptocurrencies and Services of Content, which could result in the theft or loss of your cryptographic tokens or property. To the extent possible, MetaMask intends to update the protocol underlying Services to account for any advances in cryptography and to incorporate additional security measures, but does not guarantee or otherwise represent full security of the system. By using the Service or accessing Content, you acknowledge these inherent risks.\n\n### 5.5 Volatility of Crypto Currencies ###\n\nYou understand that Ethereum and other blockchain technologies and associated currencies or tokens are highly volatile due to many factors including but not limited to adoption, speculation, technology and security risks. You also acknowledge that the cost of transacting on such technologies is variable and may increase at any time causing impact to any activities taking place on the Ethereum blockchain. You acknowledge these risks and represent that MetaMask cannot be held liable for such fluctuations or increased costs.\n\n### 5.6 Application Security ###\n\nYou acknowledge that Ethereum applications are code subject to flaws and acknowledge that you are solely responsible for evaluating any code provided by the Services or Content and the trustworthiness of any third-party websites, products, smart-contracts, or Content you access or use through the Service. You further expressly acknowledge and represent that Ethereum applications can be written maliciously or negligently, that MetaMask cannot be held liable for your interaction with such applications and that such applications may cause the loss of property or even identity. This warning and others later provided by MetaMask in no way evidence or represent an on-going duty to alert you to all of the potential risks of utilizing the Service or Content.\n\n## 6. Indemnity ##\n\nYou agree to release and to indemnify, defend and hold harmless MetaMask and its parents, subsidiaries, affiliates and agencies, as well as the officers, directors, employees, shareholders and representatives of any of the foregoing entities, from and against any and all losses, liabilities, expenses, damages, costs (including attorneys’ fees and court costs) claims or actions of any kind whatsoever arising or resulting from your use of the Service, your violation of these Terms of Use, and any of your acts or omissions that implicate publicity rights, defamation or invasion of privacy. MetaMask reserves the right, at its own expense, to assume exclusive defense and control of any matter otherwise subject to indemnification by you and, in such case, you agree to cooperate with MetaMask in the defense of such matter.\n\n## 7. Limitation on liability ##\n\nYOU ACKNOWLEDGE AND AGREE THAT YOU ASSUME FULL RESPONSIBILITY FOR YOUR USE OF THE SITE AND SERVICE. YOU ACKNOWLEDGE AND AGREE THAT ANY INFORMATION YOU SEND OR RECEIVE DURING YOUR USE OF THE SITE AND SERVICE MAY NOT BE SECURE AND MAY BE INTERCEPTED OR LATER ACQUIRED BY UNAUTHORIZED PARTIES. YOU ACKNOWLEDGE AND AGREE THAT YOUR USE OF THE SITE AND SERVICE IS AT YOUR OWN RISK. RECOGNIZING SUCH, YOU UNDERSTAND AND AGREE THAT, TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, NEITHER METAMASK NOR ITS SUPPLIERS OR LICENSORS WILL BE LIABLE TO YOU FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY OR OTHER DAMAGES OF ANY KIND, INCLUDING WITHOUT LIMITATION DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA OR OTHER TANGIBLE OR INTANGIBLE LOSSES OR ANY OTHER DAMAGES BASED ON CONTRACT, TORT, STRICT LIABILITY OR ANY OTHER THEORY (EVEN IF METAMASK HAD BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES), RESULTING FROM THE SITE OR SERVICE; THE USE OR THE INABILITY TO USE THE SITE OR SERVICE; UNAUTHORIZED ACCESS TO OR ALTERATION OF YOUR TRANSMISSIONS OR DATA; STATEMENTS OR CONDUCT OF ANY THIRD PARTY ON THE SITE OR SERVICE; ANY ACTIONS WE TAKE OR FAIL TO TAKE AS A RESULT OF COMMUNICATIONS YOU SEND TO US; HUMAN ERRORS; TECHNICAL MALFUNCTIONS; FAILURES, INCLUDING PUBLIC UTILITY OR TELEPHONE OUTAGES; OMISSIONS, INTERRUPTIONS, LATENCY, DELETIONS OR DEFECTS OF ANY DEVICE OR NETWORK, PROVIDERS, OR SOFTWARE (INCLUDING, BUT NOT LIMITED TO, THOSE THAT DO NOT PERMIT PARTICIPATION IN THE SERVICE); ANY INJURY OR DAMAGE TO COMPUTER EQUIPMENT; INABILITY TO FULLY ACCESS THE SITE OR SERVICE OR ANY OTHER WEBSITE; THEFT, TAMPERING, DESTRUCTION, OR UNAUTHORIZED ACCESS TO, IMAGES OR OTHER CONTENT OF ANY KIND; DATA THAT IS PROCESSED LATE OR INCORRECTLY OR IS INCOMPLETE OR LOST; TYPOGRAPHICAL, PRINTING OR OTHER ERRORS, OR ANY COMBINATION THEREOF; OR ANY OTHER MATTER RELATING TO THE SITE OR SERVICE.\n\nSOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF CERTAIN WARRANTIES OR THE LIMITATION OR EXCLUSION OF LIABILITY FOR INCIDENTAL OR CONSEQUENTIAL DAMAGES. ACCORDINGLY, SOME OF THE ABOVE LIMITATIONS MAY NOT APPLY TO YOU.\n\n## 8. Our Proprietary Rights ##\n\nAll title, ownership and intellectual property rights in and to the Service are owned by MetaMask or its licensors. You acknowledge and agree that the Service contains proprietary and confidential information that is protected by applicable intellectual property and other laws. Except as expressly authorized by MetaMask, you agree not to copy, modify, rent, lease, loan, sell, distribute, perform, display or create derivative works based on the Service, in whole or in part. MetaMask issues a license for MetaMask, found [here](https://github.com/MetaMask/metamask-plugin/blob/master/LICENSE). For information on other licenses utilized in the development of MetaMask, please see our attribution page at: [https://metamask.io/attributions.html](https://metamask.io/attributions.html)\n\n## 9. Links ##\n\nThe Service provides, or third parties may provide, links to other World Wide Web or accessible sites, applications or resources. Because MetaMask has no control over such sites, applications and resources, you acknowledge and agree that MetaMask is not responsible for the availability of such external sites, applications or resources, and does not endorse and is not responsible or liable for any content, advertising, products or other materials on or available from such sites or resources. You further acknowledge and agree that MetaMask shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such content, goods or services available on or through any such site or resource.\n\n## 10. Termination and Suspension ##\n\nMetaMask may terminate or suspend all or part of the Service and your MetaMask access immediately, without prior notice or liability, if you breach any of the terms or conditions of the Terms. Upon termination of your access, your right to use the Service will immediately cease.\n\nThe following provisions of the Terms survive any termination of these Terms: INDEMNITY; WARRANTY DISCLAIMERS; LIMITATION ON LIABILITY; OUR PROPRIETARY RIGHTS; LINKS; TERMINATION; NO THIRD PARTY BENEFICIARIES; BINDING ARBITRATION AND CLASS ACTION WAIVER; GENERAL INFORMATION.\n\n## 11. No Third Party Beneficiaries ##\n\nYou agree that, except as otherwise expressly provided in these Terms, there shall be no third party beneficiaries to the Terms.\n\n## 12. Notice and Procedure For Making Claims of Copyright Infringement ##\n\nIf you believe that your copyright or the copyright of a person on whose behalf you are authorized to act has been infringed, please provide MetaMask’s Copyright Agent a written Notice containing the following information:\n\n· an electronic or physical signature of the person authorized to act on behalf of the owner of the copyright or other intellectual property interest;\n\n· a description of the copyrighted work or other intellectual property that you claim has been infringed;\n\n· a description of where the material that you claim is infringing is located on the Service;\n\n· your address, telephone number, and email address;\n\n· a statement by you that you have a good faith belief that the disputed use is not authorized by the copyright owner, its agent, or the law;\n\n· a statement by you, made under penalty of perjury, that the above information in your Notice is accurate and that you are the copyright or intellectual property owner or authorized to act on the copyright or intellectual property owner's behalf.\n\nMetaMask’s Copyright Agent can be reached at:\n\nEmail: copyright [at] metamask [dot] io\n\nMail:\n\nAttention:\n\nMetaMask Copyright ℅ ConsenSys\n\n49 Bogart Street\n\nBrooklyn, NY 11206\n\n## 13. Binding Arbitration and Class Action Waiver ##\n\nPLEASE READ THIS SECTION CAREFULLY – IT MAY SIGNIFICANTLY AFFECT YOUR LEGAL RIGHTS, INCLUDING YOUR RIGHT TO FILE A LAWSUIT IN COURT\n\n### 13.1 Initial Dispute Resolution ###\n\nThe parties shall use their best efforts to engage directly to settle any dispute, claim, question, or disagreement and engage in good faith negotiations which shall be a condition to either party initiating a lawsuit or arbitration.\n\n### 13.2 Binding Arbitration ###\n\nIf the parties do not reach an agreed upon solution within a period of 30 days from the time informal dispute resolution under the Initial Dispute Resolution provision begins, then either party may initiate binding arbitration as the sole means to resolve claims, subject to the terms set forth below. Specifically, all claims arising out of or relating to these Terms (including their formation, performance and breach), the parties’ relationship with each other and/or your use of the Service shall be finally settled by binding arbitration administered by the American Arbitration Association in accordance with the provisions of its Commercial Arbitration Rules and the supplementary procedures for consumer related disputes of the American Arbitration Association (the \"AAA\"), excluding any rules or procedures governing or permitting class actions.\n\nThe arbitrator, and not any federal, state or local court or agency, shall have exclusive authority to resolve all disputes arising out of or relating to the interpretation, applicability, enforceability or formation of these Terms, including, but not limited to any claim that all or any part of these Terms are void or voidable, or whether a claim is subject to arbitration. The arbitrator shall be empowered to grant whatever relief would be available in a court under law or in equity. The arbitrator’s award shall be written, and binding on the parties and may be entered as a judgment in any court of competent jurisdiction.\n\nThe parties understand that, absent this mandatory provision, they would have the right to sue in court and have a jury trial. They further understand that, in some instances, the costs of arbitration could exceed the costs of litigation and the right to discovery may be more limited in arbitration than in court.\n\n### 13.3 Location ###\n\nBinding arbitration shall take place in New York. You agree to submit to the personal jurisdiction of any federal or state court in New York County, New York, in order to compel arbitration, to stay proceedings pending arbitration, or to confirm, modify, vacate or enter judgment on the award entered by the arbitrator.\n\n### 13.4 Class Action Waiver ###\n\nThe parties further agree that any arbitration shall be conducted in their individual capacities only and not as a class action or other representative action, and the parties expressly waive their right to file a class action or seek relief on a class basis. YOU AND METAMASK AGREE THAT EACH MAY BRING CLAIMS AGAINST THE OTHER ONLY IN YOUR OR ITS INDIVIDUAL CAPACITY, AND NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED CLASS OR REPRESENTATIVE PROCEEDING. If any court or arbitrator determines that the class action waiver set forth in this paragraph is void or unenforceable for any reason or that an arbitration can proceed on a class basis, then the arbitration provision set forth above shall be deemed null and void in its entirety and the parties shall be deemed to have not agreed to arbitrate disputes.\n\n### 13.5 Exception - Litigation of Intellectual Property and Small Claims Court Claims ###\n\nNotwithstanding the parties' decision to resolve all disputes through arbitration, either party may bring an action in state or federal court to protect its intellectual property rights (\"intellectual property rights\" means patents, copyrights, moral rights, trademarks, and trade secrets, but not privacy or publicity rights). Either party may also seek relief in a small claims court for disputes or claims within the scope of that court’s jurisdiction.\n\n### 13.6 30-Day Right to Opt Out ###\n\nYou have the right to opt-out and not be bound by the arbitration and class action waiver provisions set forth above by sending written notice of your decision to opt-out to the following address: MetaMask ℅ ConsenSys, 49 Bogart Street, Brooklyn NY 11206 and via email at legal-opt@metamask.io. The notice must be sent within 30 days of September 6, 2016 or your first use of the Service, whichever is later, otherwise you shall be bound to arbitrate disputes in accordance with the terms of those paragraphs. If you opt-out of these arbitration provisions, MetaMask also will not be bound by them.\n\n### 13.7 Changes to This Section ###\n\nMetaMask will provide 60-days’ notice of any changes to this section. Changes will become effective on the 60th day, and will apply prospectively only to any claims arising after the 60th day.\n\nFor any dispute not subject to arbitration you and MetaMask agree to submit to the personal and exclusive jurisdiction of and venue in the federal and state courts located in New York, New York. You further agree to accept service of process by mail, and hereby waive any and all jurisdictional and venue defenses otherwise available.\n\nThe Terms and the relationship between you and MetaMask shall be governed by the laws of the State of New York without regard to conflict of law provisions.\n\n## 14. General Information ##\n\n### 14.1 Entire Agreement ###\n\nThese Terms (and any additional terms, rules and conditions of participation that MetaMask may post on the Service) constitute the entire agreement between you and MetaMask with respect to the Service and supersedes any prior agreements, oral or written, between you and MetaMask. In the event of a conflict between these Terms and the additional terms, rules and conditions of participation, the latter will prevail over the Terms to the extent of the conflict.\n\n### 14.2 Waiver and Severability of Terms ###\n\nThe failure of MetaMask to exercise or enforce any right or provision of the Terms shall not constitute a waiver of such right or provision. If any provision of the Terms is found by an arbitrator or court of competent jurisdiction to be invalid, the parties nevertheless agree that the arbitrator or court should endeavor to give effect to the parties' intentions as reflected in the provision, and the other provisions of the Terms remain in full force and effect.\n\n### 14.3 Statute of Limitations ###\n\nYou agree that regardless of any statute or law to the contrary, any claim or cause of action arising out of or related to the use of the Service or the Terms must be filed within one (1) year after such claim or cause of action arose or be forever barred.\n\n### 14.4 Section Titles ###\n\nThe section titles in the Terms are for convenience only and have no legal or contractual effect.\n\n### 14.5 Communications ###\n\nUsers with questions, complaints or claims with respect to the Service may contact us using the relevant contact information set forth above and at communications@metamask.io.\n\n## 15 Related Links ##\n\n**[Terms of Use](https://metamask.io/terms.html)**\n\n**[Privacy](https://metamask.io/privacy.html)**\n\n**[Attributions](https://metamask.io/attributions.html)**\n\n",
+ "body": "# Terms of Use #\n\n**THIS AGREEMENT IS SUBJECT TO BINDING ARBITRATION AND A WAIVER OF CLASS ACTION RIGHTS AS DETAILED IN SECTION 13. PLEASE READ THE AGREEMENT CAREFULLY.**\n\n_Our Terms of Use have been updated as of September 5, 2016_\n\n## 1. Acceptance of Terms ##\n\nMetaMask provides a platform for managing Ethereum (or \"ETH\") accounts, and allowing ordinary websites to interact with the Ethereum blockchain, while keeping the user in control over what transactions they approve, through our website located at[ ](http://metamask.io)[https://metamask.io/](https://metamask.io/) and browser plugin (the \"Site\") — which includes text, images, audio, code and other materials (collectively, the “Content”) and all of the features, and services provided. The Site, and any other features, tools, materials, or other services offered from time to time by MetaMask are referred to here as the “Service.” Please read these Terms of Use (the “Terms” or “Terms of Use”) carefully before using the Service. By using or otherwise accessing the Services, or clicking to accept or agree to these Terms where that option is made available, you (1) accept and agree to these Terms (2) consent to the collection, use, disclosure and other handling of information as described in our Privacy Policy and (3) any additional terms, rules and conditions of participation issued by MetaMask from time to time. If you do not agree to the Terms, then you may not access or use the Content or Services.\n\n## 2. Modification of Terms of Use ##\n\nExcept for Section 13, providing for binding arbitration and waiver of class action rights, MetaMask reserves the right, at its sole discretion, to modify or replace the Terms of Use at any time. The most current version of these Terms will be posted on our Site. You shall be responsible for reviewing and becoming familiar with any such modifications. Use of the Services by you after any modification to the Terms constitutes your acceptance of the Terms of Use as modified.\n\n\n\n## 3. Eligibility ##\n\nYou hereby represent and warrant that you are fully able and competent to enter into the terms, conditions, obligations, affirmations, representations and warranties set forth in these Terms and to abide by and comply with these Terms.\n\nMetaMask is a global platform and by accessing the Content or Services, you are representing and warranting that, you are of the legal age of majority in your jurisdiction as is required to access such Services and Content and enter into arrangements as provided by the Service. You further represent that you are otherwise legally permitted to use the service in your jurisdiction including owning cryptographic tokens of value, and interacting with the Services or Content in any way. You further represent you are responsible for ensuring compliance with the laws of your jurisdiction and acknowledge that MetaMask is not liable for your compliance with such laws.\n\n## 4 Account Password and Security ##\n\nWhen setting up an account within MetaMask, you will be responsible for keeping your own account secrets, which may be a twelve-word seed phrase, an account file, or other locally stored secret information. MetaMask encrypts this information locally with a password you provide, that we never send to our servers. You agree to (a) never use the same password for MetaMask that you have ever used outside of this service; (b) keep your secret information and password confidential and do not share them with anyone else; (c) immediately notify MetaMask of any unauthorized use of your account or breach of security. MetaMask cannot and will not be liable for any loss or damage arising from your failure to comply with this section.\n\n## 5. Representations, Warranties, and Risks ##\n\n### 5.1. Warranty Disclaimer ###\n\nYou expressly understand and agree that your use of the Service is at your sole risk. The Service (including the Service and the Content) are provided on an \"AS IS\" and \"as available\" basis, without warranties of any kind, either express or implied, including, without limitation, implied warranties of merchantability, fitness for a particular purpose or non-infringement. You acknowledge that MetaMask has no control over, and no duty to take any action regarding: which users gain access to or use the Service; what effects the Content may have on you; how you may interpret or use the Content; or what actions you may take as a result of having been exposed to the Content. You release MetaMask from all liability for you having acquired or not acquired Content through the Service. MetaMask makes no representations concerning any Content contained in or accessed through the Service, and MetaMask will not be responsible or liable for the accuracy, copyright compliance, legality or decency of material contained in or accessed through the Service.\n\n### 5.2 Sophistication and Risk of Cryptographic Systems ###\n\nBy utilizing the Service or interacting with the Content or platform in any way, you represent that you understand the inherent risks associated with cryptographic systems; and warrant that you have an understanding of the usage and intricacies of native cryptographic tokens, like Ether (ETH) and Bitcoin (BTC), smart contract based tokens such as those that follow the Ethereum Token Standard (https://github.com/ethereum/EIPs/issues/20), and blockchain-based software systems.\n\n### 5.3 Risk of Regulatory Actions in One or More Jurisdictions ###\n\nMetaMask and ETH could be impacted by one or more regulatory inquiries or regulatory action, which could impede or limit the ability of MetaMask to continue to develop, or which could impede or limit your ability to access or use the Service or Ethereum blockchain.\n\n### 5.4 Risk of Weaknesses or Exploits in the Field of Cryptography ###\n\nYou acknowledge and understand that Cryptography is a progressing field. Advances in code cracking or technical advances such as the development of quantum computers may present risks to cryptocurrencies and Services of Content, which could result in the theft or loss of your cryptographic tokens or property. To the extent possible, MetaMask intends to update the protocol underlying Services to account for any advances in cryptography and to incorporate additional security measures, but does not guarantee or otherwise represent full security of the system. By using the Service or accessing Content, you acknowledge these inherent risks.\n\n### 5.5 Volatility of Crypto Currencies ###\n\nYou understand that Ethereum and other blockchain technologies and associated currencies or tokens are highly volatile due to many factors including but not limited to adoption, speculation, technology and security risks. You also acknowledge that the cost of transacting on such technologies is variable and may increase at any time causing impact to any activities taking place on the Ethereum blockchain. You acknowledge these risks and represent that MetaMask cannot be held liable for such fluctuations or increased costs.\n\n### 5.6 Application Security ###\n\nYou acknowledge that Ethereum applications are code subject to flaws and acknowledge that you are solely responsible for evaluating any code provided by the Services or Content and the trustworthiness of any third-party websites, products, smart-contracts, or Content you access or use through the Service. You further expressly acknowledge and represent that Ethereum applications can be written maliciously or negligently, that MetaMask cannot be held liable for your interaction with such applications and that such applications may cause the loss of property or even identity. This warning and others later provided by MetaMask in no way evidence or represent an on-going duty to alert you to all of the potential risks of utilizing the Service or Content.\n\n## 6. Indemnity ##\n\nYou agree to release and to indemnify, defend and hold harmless MetaMask and its parents, subsidiaries, affiliates and agencies, as well as the officers, directors, employees, shareholders and representatives of any of the foregoing entities, from and against any and all losses, liabilities, expenses, damages, costs (including attorneys’ fees and court costs) claims or actions of any kind whatsoever arising or resulting from your use of the Service, your violation of these Terms of Use, and any of your acts or omissions that implicate publicity rights, defamation or invasion of privacy. MetaMask reserves the right, at its own expense, to assume exclusive defense and control of any matter otherwise subject to indemnification by you and, in such case, you agree to cooperate with MetaMask in the defense of such matter.\n\n## 7. Limitation on liability ##\n\nYOU ACKNOWLEDGE AND AGREE THAT YOU ASSUME FULL RESPONSIBILITY FOR YOUR USE OF THE SITE AND SERVICE. YOU ACKNOWLEDGE AND AGREE THAT ANY INFORMATION YOU SEND OR RECEIVE DURING YOUR USE OF THE SITE AND SERVICE MAY NOT BE SECURE AND MAY BE INTERCEPTED OR LATER ACQUIRED BY UNAUTHORIZED PARTIES. YOU ACKNOWLEDGE AND AGREE THAT YOUR USE OF THE SITE AND SERVICE IS AT YOUR OWN RISK. RECOGNIZING SUCH, YOU UNDERSTAND AND AGREE THAT, TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, NEITHER METAMASK NOR ITS SUPPLIERS OR LICENSORS WILL BE LIABLE TO YOU FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY OR OTHER DAMAGES OF ANY KIND, INCLUDING WITHOUT LIMITATION DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA OR OTHER TANGIBLE OR INTANGIBLE LOSSES OR ANY OTHER DAMAGES BASED ON CONTRACT, TORT, STRICT LIABILITY OR ANY OTHER THEORY (EVEN IF METAMASK HAD BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES), RESULTING FROM THE SITE OR SERVICE; THE USE OR THE INABILITY TO USE THE SITE OR SERVICE; UNAUTHORIZED ACCESS TO OR ALTERATION OF YOUR TRANSMISSIONS OR DATA; STATEMENTS OR CONDUCT OF ANY THIRD PARTY ON THE SITE OR SERVICE; ANY ACTIONS WE TAKE OR FAIL TO TAKE AS A RESULT OF COMMUNICATIONS YOU SEND TO US; HUMAN ERRORS; TECHNICAL MALFUNCTIONS; FAILURES, INCLUDING PUBLIC UTILITY OR TELEPHONE OUTAGES; OMISSIONS, INTERRUPTIONS, LATENCY, DELETIONS OR DEFECTS OF ANY DEVICE OR NETWORK, PROVIDERS, OR SOFTWARE (INCLUDING, BUT NOT LIMITED TO, THOSE THAT DO NOT PERMIT PARTICIPATION IN THE SERVICE); ANY INJURY OR DAMAGE TO COMPUTER EQUIPMENT; INABILITY TO FULLY ACCESS THE SITE OR SERVICE OR ANY OTHER WEBSITE; THEFT, TAMPERING, DESTRUCTION, OR UNAUTHORIZED ACCESS TO, IMAGES OR OTHER CONTENT OF ANY KIND; DATA THAT IS PROCESSED LATE OR INCORRECTLY OR IS INCOMPLETE OR LOST; TYPOGRAPHICAL, PRINTING OR OTHER ERRORS, OR ANY COMBINATION THEREOF; OR ANY OTHER MATTER RELATING TO THE SITE OR SERVICE.\n\nSOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF CERTAIN WARRANTIES OR THE LIMITATION OR EXCLUSION OF LIABILITY FOR INCIDENTAL OR CONSEQUENTIAL DAMAGES. ACCORDINGLY, SOME OF THE ABOVE LIMITATIONS MAY NOT APPLY TO YOU.\n\n## 8. Our Proprietary Rights ##\n\nAll title, ownership and intellectual property rights in and to the Service are owned by MetaMask or its licensors. You acknowledge and agree that the Service contains proprietary and confidential information that is protected by applicable intellectual property and other laws. Except as expressly authorized by MetaMask, you agree not to copy, modify, rent, lease, loan, sell, distribute, perform, display or create derivative works based on the Service, in whole or in part. MetaMask issues a license for MetaMask, found [here](https://github.com/MetaMask/metamask-plugin/blob/master/LICENSE). For information on other licenses utilized in the development of MetaMask, please see our attribution page at: [https://metamask.io/attributions.html](https://metamask.io/attributions.html)\n\n## 9. Links ##\n\nThe Service provides, or third parties may provide, links to other World Wide Web or accessible sites, applications or resources. Because MetaMask has no control over such sites, applications and resources, you acknowledge and agree that MetaMask is not responsible for the availability of such external sites, applications or resources, and does not endorse and is not responsible or liable for any content, advertising, products or other materials on or available from such sites or resources. You further acknowledge and agree that MetaMask shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such content, goods or services available on or through any such site or resource.\n\n## 10. Termination and Suspension ##\n\nMetaMask may terminate or suspend all or part of the Service and your MetaMask access immediately, without prior notice or liability, if you breach any of the terms or conditions of the Terms. Upon termination of your access, your right to use the Service will immediately cease.\n\nThe following provisions of the Terms survive any termination of these Terms: INDEMNITY; WARRANTY DISCLAIMERS; LIMITATION ON LIABILITY; OUR PROPRIETARY RIGHTS; LINKS; TERMINATION; NO THIRD PARTY BENEFICIARIES; BINDING ARBITRATION AND CLASS ACTION WAIVER; GENERAL INFORMATION.\n\n## 11. No Third Party Beneficiaries ##\n\nYou agree that, except as otherwise expressly provided in these Terms, there shall be no third party beneficiaries to the Terms.\n\n## 12. Notice and Procedure For Making Claims of Copyright Infringement ##\n\nIf you believe that your copyright or the copyright of a person on whose behalf you are authorized to act has been infringed, please provide MetaMask’s Copyright Agent a written Notice containing the following information:\n\n· an electronic or physical signature of the person authorized to act on behalf of the owner of the copyright or other intellectual property interest;\n\n· a description of the copyrighted work or other intellectual property that you claim has been infringed;\n\n· a description of where the material that you claim is infringing is located on the Service;\n\n· your address, telephone number, and email address;\n\n· a statement by you that you have a good faith belief that the disputed use is not authorized by the copyright owner, its agent, or the law;\n\n· a statement by you, made under penalty of perjury, that the above information in your Notice is accurate and that you are the copyright or intellectual property owner or authorized to act on the copyright or intellectual property owner's behalf.\n\nMetaMask’s Copyright Agent can be reached at:\n\nEmail: copyright [at] metamask [dot] io\n\nMail:\n\nAttention:\n\nMetaMask Copyright ℅ ConsenSys\n\n49 Bogart Street\n\nBrooklyn, NY 11206\n\n## 13. Binding Arbitration and Class Action Waiver ##\n\nPLEASE READ THIS SECTION CAREFULLY – IT MAY SIGNIFICANTLY AFFECT YOUR LEGAL RIGHTS, INCLUDING YOUR RIGHT TO FILE A LAWSUIT IN COURT\n\n### 13.1 Initial Dispute Resolution ###\n\nThe parties shall use their best efforts to engage directly to settle any dispute, claim, question, or disagreement and engage in good faith negotiations which shall be a condition to either party initiating a lawsuit or arbitration.\n\n### 13.2 Binding Arbitration ###\n\nIf the parties do not reach an agreed upon solution within a period of 30 days from the time informal dispute resolution under the Initial Dispute Resolution provision begins, then either party may initiate binding arbitration as the sole means to resolve claims, subject to the terms set forth below. Specifically, all claims arising out of or relating to these Terms (including their formation, performance and breach), the parties’ relationship with each other and/or your use of the Service shall be finally settled by binding arbitration administered by the American Arbitration Association in accordance with the provisions of its Commercial Arbitration Rules and the supplementary procedures for consumer related disputes of the American Arbitration Association (the \"AAA\"), excluding any rules or procedures governing or permitting class actions.\n\nThe arbitrator, and not any federal, state or local court or agency, shall have exclusive authority to resolve all disputes arising out of or relating to the interpretation, applicability, enforceability or formation of these Terms, including, but not limited to any claim that all or any part of these Terms are void or voidable, or whether a claim is subject to arbitration. The arbitrator shall be empowered to grant whatever relief would be available in a court under law or in equity. The arbitrator’s award shall be written, and binding on the parties and may be entered as a judgment in any court of competent jurisdiction.\n\nThe parties understand that, absent this mandatory provision, they would have the right to sue in court and have a jury trial. They further understand that, in some instances, the costs of arbitration could exceed the costs of litigation and the right to discovery may be more limited in arbitration than in court.\n\n### 13.3 Location ###\n\nBinding arbitration shall take place in New York. You agree to submit to the personal jurisdiction of any federal or state court in New York County, New York, in order to compel arbitration, to stay proceedings pending arbitration, or to confirm, modify, vacate or enter judgment on the award entered by the arbitrator.\n\n### 13.4 Class Action Waiver ###\n\nThe parties further agree that any arbitration shall be conducted in their individual capacities only and not as a class action or other representative action, and the parties expressly waive their right to file a class action or seek relief on a class basis. YOU AND METAMASK AGREE THAT EACH MAY BRING CLAIMS AGAINST THE OTHER ONLY IN YOUR OR ITS INDIVIDUAL CAPACITY, AND NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED CLASS OR REPRESENTATIVE PROCEEDING. If any court or arbitrator determines that the class action waiver set forth in this paragraph is void or unenforceable for any reason or that an arbitration can proceed on a class basis, then the arbitration provision set forth above shall be deemed null and void in its entirety and the parties shall be deemed to have not agreed to arbitrate disputes.\n\n### 13.5 Exception - Litigation of Intellectual Property and Small Claims Court Claims ###\n\nNotwithstanding the parties' decision to resolve all disputes through arbitration, either party may bring an action in state or federal court to protect its intellectual property rights (\"intellectual property rights\" means patents, copyrights, moral rights, trademarks, and trade secrets, but not privacy or publicity rights). Either party may also seek relief in a small claims court for disputes or claims within the scope of that court’s jurisdiction.\n\n### 13.6 30-Day Right to Opt Out ###\n\nYou have the right to opt-out and not be bound by the arbitration and class action waiver provisions set forth above by sending written notice of your decision to opt-out to the following address: MetaMask ℅ ConsenSys, 49 Bogart Street, Brooklyn NY 11206 and via email at support@metamask.io. The notice must be sent within 30 days of September 6, 2016 or your first use of the Service, whichever is later, otherwise you shall be bound to arbitrate disputes in accordance with the terms of those paragraphs. If you opt-out of these arbitration provisions, MetaMask also will not be bound by them.\n\n### 13.7 Changes to This Section ###\n\nMetaMask will provide 60-days’ notice of any changes to this section. Changes will become effective on the 60th day, and will apply prospectively only to any claims arising after the 60th day.\n\nFor any dispute not subject to arbitration you and MetaMask agree to submit to the personal and exclusive jurisdiction of and venue in the federal and state courts located in New York, New York. You further agree to accept service of process by mail, and hereby waive any and all jurisdictional and venue defenses otherwise available.\n\nThe Terms and the relationship between you and MetaMask shall be governed by the laws of the State of New York without regard to conflict of law provisions.\n\n## 14. General Information ##\n\n### 14.1 Entire Agreement ###\n\nThese Terms (and any additional terms, rules and conditions of participation that MetaMask may post on the Service) constitute the entire agreement between you and MetaMask with respect to the Service and supersedes any prior agreements, oral or written, between you and MetaMask. In the event of a conflict between these Terms and the additional terms, rules and conditions of participation, the latter will prevail over the Terms to the extent of the conflict.\n\n### 14.2 Waiver and Severability of Terms ###\n\nThe failure of MetaMask to exercise or enforce any right or provision of the Terms shall not constitute a waiver of such right or provision. If any provision of the Terms is found by an arbitrator or court of competent jurisdiction to be invalid, the parties nevertheless agree that the arbitrator or court should endeavor to give effect to the parties' intentions as reflected in the provision, and the other provisions of the Terms remain in full force and effect.\n\n### 14.3 Statute of Limitations ###\n\nYou agree that regardless of any statute or law to the contrary, any claim or cause of action arising out of or related to the use of the Service or the Terms must be filed within one (1) year after such claim or cause of action arose or be forever barred.\n\n### 14.4 Section Titles ###\n\nThe section titles in the Terms are for convenience only and have no legal or contractual effect.\n\n### 14.5 Communications ###\n\nUsers with questions, complaints or claims with respect to the Service may contact us using the relevant contact information set forth above and at communications@metamask.io.\n\n## 15 Related Links ##\n\n**[Terms of Use](https://metamask.io/terms.html)**\n\n**[Privacy](https://metamask.io/privacy.html)**\n\n**[Attributions](https://metamask.io/attributions.html)**\n\n",
"id": 0
},
"network": "3",
@@ -58,5 +58,23 @@
}
},
"identities": {},
- "computedBalances": {}
+ "computedBalances": {},
+ "confirmTransaction": {
+ "txData": {},
+ "tokenData": {},
+ "methodData": {},
+ "tokenProps": {
+ "tokenDecimals": "",
+ "tokenSymbol": ""
+ },
+ "fiatTransactionAmount": "",
+ "fiatTransactionFee": "",
+ "fiatTransactionTotal": "",
+ "ethTransactionAmount": "",
+ "ethTransactionFee": "",
+ "ethTransactionTotal": "",
+ "hexGasTotal": "",
+ "nonce": "",
+ "fetchingMethodData": false
+ }
}
diff --git a/development/states/send-edit.json b/development/states/send-edit.json
index b05acbf3b..8e5c25a82 100644
--- a/development/states/send-edit.json
+++ b/development/states/send-edit.json
@@ -156,5 +156,23 @@
"fromDropdownOpen": false,
"toDropdownOpen": false,
"errors": {}
+ },
+ "confirmTransaction": {
+ "txData": {},
+ "tokenData": {},
+ "methodData": {},
+ "tokenProps": {
+ "tokenDecimals": "",
+ "tokenSymbol": ""
+ },
+ "fiatTransactionAmount": "",
+ "fiatTransactionFee": "",
+ "fiatTransactionTotal": "",
+ "ethTransactionAmount": "",
+ "ethTransactionFee": "",
+ "ethTransactionTotal": "",
+ "hexGasTotal": "",
+ "nonce": "",
+ "fetchingMethodData": false
}
}
diff --git a/development/states/send-new-ui.json b/development/states/send-new-ui.json
index b457749ee..ad2ff3d6e 100644
--- a/development/states/send-new-ui.json
+++ b/development/states/send-new-ui.json
@@ -135,5 +135,23 @@
"fromDropdownOpen": false,
"toDropdownOpen": false,
"errors": {}
+ },
+ "confirmTransaction": {
+ "txData": {},
+ "tokenData": {},
+ "methodData": {},
+ "tokenProps": {
+ "tokenDecimals": "",
+ "tokenSymbol": ""
+ },
+ "fiatTransactionAmount": "",
+ "fiatTransactionFee": "",
+ "fiatTransactionTotal": "",
+ "ethTransactionAmount": "",
+ "ethTransactionFee": "",
+ "ethTransactionTotal": "",
+ "hexGasTotal": "",
+ "nonce": "",
+ "fetchingMethodData": false
}
}
diff --git a/docs/trezor-emulator.md b/docs/trezor-emulator.md
new file mode 100644
index 000000000..8f66ba213
--- /dev/null
+++ b/docs/trezor-emulator.md
@@ -0,0 +1,25 @@
+# Using the TREZOR simulator
+
+You can install the TREZOR emulator and use it with Metamask.
+Here is how:
+
+## 1 - Install the TREZOR Bridge
+
+Download the corresponding bridge for your platform from [this url](https://wallet.trezor.io/data/bridge/latest/index.html)
+
+## 2 - Download and build the simulator
+
+Follow this instructions: https://github.com/trezor/trezor-core/blob/master/docs/build.md
+
+## 3 - Restart the bridge with emulator support (Mac OSx instructions)
+
+`
+ # stop any existing instance of trezord
+ killall trezord
+
+ # start the bridge for the simulator
+ /Applications/Utilities/TREZOR\ Bridge/trezord -e 21324 >> /dev/null 2>&1 &
+
+ # launch the emulator
+ ~/trezor-core/emu.sh
+`
diff --git a/notices/archive/notice_0.md b/notices/archive/notice_0.md
index 40a338016..c485edd1d 100644
--- a/notices/archive/notice_0.md
+++ b/notices/archive/notice_0.md
@@ -138,7 +138,7 @@ Notwithstanding the parties' decision to resolve all disputes through arbitratio
### 13.6 30-Day Right to Opt Out ###
-You have the right to opt-out and not be bound by the arbitration and class action waiver provisions set forth above by sending written notice of your decision to opt-out to the following address: MetaMask ℅ ConsenSys, 49 Bogart Street, Brooklyn NY 11206 and via email at legal-opt@metamask.io. The notice must be sent within 30 days of September 6, 2016 or your first use of the Service, whichever is later, otherwise you shall be bound to arbitrate disputes in accordance with the terms of those paragraphs. If you opt-out of these arbitration provisions, MetaMask also will not be bound by them.
+You have the right to opt-out and not be bound by the arbitration and class action waiver provisions set forth above by sending written notice of your decision to opt-out to the following address: MetaMask ℅ ConsenSys, 49 Bogart Street, Brooklyn NY 11206 and via email at support@metamask.io. The notice must be sent within 30 days of September 6, 2016 or your first use of the Service, whichever is later, otherwise you shall be bound to arbitrate disputes in accordance with the terms of those paragraphs. If you opt-out of these arbitration provisions, MetaMask also will not be bound by them.
### 13.7 Changes to This Section ###
@@ -177,4 +177,3 @@ Users with questions, complaints or claims with respect to the Service may conta
**[Privacy](https://metamask.io/privacy.html)**
**[Attributions](https://metamask.io/attributions.html)**
-
diff --git a/old-ui/app/app.js b/old-ui/app/app.js
index 5c2dccc03..0637e3b5b 100644
--- a/old-ui/app/app.js
+++ b/old-ui/app/app.js
@@ -183,6 +183,7 @@ App.prototype.renderAppBar = function () {
this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen })
},
}),
+
]),
props.isUnlocked && h('div', {
diff --git a/old-ui/app/keychains/hd/restore-vault.js b/old-ui/app/keychains/hd/restore-vault.js
index d334d8e5f..e9486a28d 100644
--- a/old-ui/app/keychains/hd/restore-vault.js
+++ b/old-ui/app/keychains/hd/restore-vault.js
@@ -42,9 +42,6 @@ RestoreVaultScreen.prototype.render = function () {
// wallet seed entry
h('h3', 'Wallet Seed'),
h('textarea.twelve-word-phrase.letter-spacey', {
- dataset: {
- persistentFormId: 'wallet-seed',
- },
placeholder: 'Enter your secret twelve word phrase here to restore your vault.',
}),
diff --git a/package-lock.json b/package-lock.json
index 80096daf3..f1f12705d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3619,9 +3619,9 @@
}
},
"babel-preset-env": {
- "version": "1.6.1",
- "resolved": "https://registry.npmjs.org/babel-preset-env/-/babel-preset-env-1.6.1.tgz",
- "integrity": "sha512-W6VIyA6Ch9ePMI7VptNn2wBM6dbG0eSz25HEiL40nQXCsXGTGZSTZu1Iap+cj3Q0S5a7T9+529l/5Bkvd+afNA==",
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/babel-preset-env/-/babel-preset-env-1.7.0.tgz",
+ "integrity": "sha512-9OR2afuKDneX2/q2EurSftUYM0xGu4O2D9adAhVfADDhrYDaxXV0rBbevVYoY9n6nyX1PmQW/0jtpJvUNr9CHg==",
"requires": {
"babel-plugin-check-es2015-constants": "^6.22.0",
"babel-plugin-syntax-trailing-function-commas": "^6.22.0",
@@ -3650,7 +3650,7 @@
"babel-plugin-transform-es2015-unicode-regex": "^6.22.0",
"babel-plugin-transform-exponentiation-operator": "^6.22.0",
"babel-plugin-transform-regenerator": "^6.22.0",
- "browserslist": "^2.1.2",
+ "browserslist": "^3.2.6",
"invariant": "^2.2.2",
"semver": "^5.3.0"
}
@@ -4226,6 +4226,7 @@
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz",
"integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=",
+ "dev": true,
"requires": {
"hoek": "2.x.x"
},
@@ -4233,7 +4234,8 @@
"hoek": {
"version": "2.16.3",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz",
- "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0="
+ "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=",
+ "dev": true
}
}
},
@@ -4664,12 +4666,19 @@
}
},
"browserslist": {
- "version": "2.11.0",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.11.0.tgz",
- "integrity": "sha512-mNYp0RNeu1xueGuJFSXkU+K0nH+dBE/gcjtyhtNKfU8hwdrVIfoA7i5iFSjOmzkGdL2QaO7YX9ExiVPE7AY9JA==",
+ "version": "3.2.8",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-3.2.8.tgz",
+ "integrity": "sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ==",
"requires": {
- "caniuse-lite": "^1.0.30000784",
- "electron-to-chromium": "^1.3.30"
+ "caniuse-lite": "^1.0.30000844",
+ "electron-to-chromium": "^1.3.47"
+ },
+ "dependencies": {
+ "electron-to-chromium": {
+ "version": "1.3.52",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.52.tgz",
+ "integrity": "sha1-0tnxJwuko7lnuDHEDvcftNmrXOA="
+ }
}
},
"bs58": {
@@ -4996,9 +5005,9 @@
"integrity": "sha1-MN/YMAnVcE8C3/s3clBo7RKjZrs="
},
"caniuse-lite": {
- "version": "1.0.30000786",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000786.tgz",
- "integrity": "sha1-G0Jc2FaNgFvFY4veSQXNhjVod0Y="
+ "version": "1.0.30000865",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000865.tgz",
+ "integrity": "sha512-vs79o1mOSKRGv/1pSkp4EXgl4ZviWeYReXw60XfacPU64uQWZwJT6vZNmxRF9O+6zu71sJwMxLK5JXxbzuVrLw=="
},
"capture-stack-trace": {
"version": "1.0.0",
@@ -6158,6 +6167,8 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz",
"integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=",
+ "dev": true,
+ "optional": true,
"requires": {
"boom": "2.x.x"
}
@@ -6856,9 +6867,9 @@
"dev": true
},
"yauzl": {
- "version": "2.9.2",
- "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.9.2.tgz",
- "integrity": "sha1-T7G8euH8L1cDe1SvasyP4QMcW3c=",
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+ "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=",
"dev": true,
"requires": {
"buffer-crc32": "~0.2.3",
@@ -8369,11 +8380,11 @@
"from": "github:MetaMask/eth-contract-metadata#master"
},
"eth-ens-namehash": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/eth-ens-namehash/-/eth-ens-namehash-1.0.2.tgz",
- "integrity": "sha1-Bezda6wtf9e8XKhKmTxrrZ2k7bk=",
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/eth-ens-namehash/-/eth-ens-namehash-2.0.8.tgz",
+ "integrity": "sha1-IprEbsqG1S4MmR58sq74P/D2i88=",
"requires": {
- "idna-uts46": "^1.0.1",
+ "idna-uts46-hx": "^2.3.1",
"js-sha3": "^0.5.7"
},
"dependencies": {
@@ -8385,22 +8396,41 @@
}
},
"eth-hd-keyring": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/eth-hd-keyring/-/eth-hd-keyring-1.2.2.tgz",
- "integrity": "sha1-rV9HkHRDapO0ObC5XHkJXCh5GII=",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/eth-hd-keyring/-/eth-hd-keyring-2.0.0.tgz",
+ "integrity": "sha512-lTeANNPNj/j08sWU7LUQZTsx9NUJaUsiOdVxeP0UI5kke7L+Sd7zJWBmCShudEVG8PkqKLE1KJo08o430sl6rw==",
"requires": {
"bip39": "^2.2.0",
- "eth-sig-util": "^1.4.2",
+ "eth-sig-util": "^2.0.1",
+ "ethereumjs-abi": "^0.6.5",
"ethereumjs-util": "^5.1.1",
"ethereumjs-wallet": "^0.6.0",
"events": "^1.1.1",
"xtend": "^4.0.1"
},
"dependencies": {
+ "eth-sig-util": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-2.0.1.tgz",
+ "integrity": "sha512-lxHZOQspexk3DaGj4RBbWy4C/qNOWRnxpaJzNnYD3WEmC8shcJ4tHs7Xv878rzvILfJnSFSCCiKQhng1m80oBQ==",
+ "requires": {
+ "ethereumjs-util": "^5.1.1"
+ },
+ "dependencies": {
+ "ethereumjs-abi": {
+ "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7",
+ "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7",
+ "requires": {
+ "bn.js": "^4.10.0",
+ "ethereumjs-util": "^5.0.0"
+ }
+ }
+ }
+ },
"ethereumjs-util": {
- "version": "5.1.4",
- "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.1.4.tgz",
- "integrity": "sha512-wbeTc5prEzIWFSQUcEsCAZbqubtJKy6yS+oZMY1cGG6GLYzLjm4YhC2RNrWIg8hRYnclWpnZmx2zkiufQkmd3w==",
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz",
+ "integrity": "sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA==",
"requires": {
"bn.js": "^4.11.0",
"create-hash": "^1.1.2",
@@ -8567,16 +8597,16 @@
}
},
"eth-keyring-controller": {
- "version": "3.1.4",
- "resolved": "https://registry.npmjs.org/eth-keyring-controller/-/eth-keyring-controller-3.1.4.tgz",
- "integrity": "sha512-NNlVB/TBc8p9CblwECjPlUR+7MNQKiBa7tEFxIzZ9MjjNCEYPWDXTm0vJZzuDtVmFxYwIA53UD0QEn0QNxWNEQ==",
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/eth-keyring-controller/-/eth-keyring-controller-3.3.0.tgz",
+ "integrity": "sha512-ZQtWxg0mNSAeQ9JcOaeTKaN5vfr+MbjRhSfeaF1VIWZsQgULpHrbjT6m94qiAFdh7ZngoR7OEpK9PATBH7JNYw==",
"requires": {
"bip39": "^2.4.0",
"bluebird": "^3.5.0",
"browser-passworder": "^2.0.3",
"eth-hd-keyring": "^1.2.2",
"eth-sig-util": "^1.4.0",
- "eth-simple-keyring": "^1.2.2",
+ "eth-simple-keyring": "^1.3.0",
"ethereumjs-util": "^5.1.2",
"loglevel": "^1.5.0",
"obs-store": "^2.4.1",
@@ -8592,6 +8622,19 @@
"object-assign": "^4.0.0"
}
},
+ "eth-hd-keyring": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/eth-hd-keyring/-/eth-hd-keyring-1.2.2.tgz",
+ "integrity": "sha1-rV9HkHRDapO0ObC5XHkJXCh5GII=",
+ "requires": {
+ "bip39": "^2.2.0",
+ "eth-sig-util": "^1.4.2",
+ "ethereumjs-util": "^5.1.1",
+ "ethereumjs-wallet": "^0.6.0",
+ "events": "^1.1.1",
+ "xtend": "^4.0.1"
+ }
+ },
"ethereumjs-util": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz",
@@ -8635,6 +8678,108 @@
"xhr-request-promise": "^0.1.2"
}
},
+ "eth-method-registry": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/eth-method-registry/-/eth-method-registry-1.0.0.tgz",
+ "integrity": "sha1-8Ij3Wdad6f3BK3EEm83GiKMoOLY=",
+ "requires": {
+ "ethjs": "^0.3.0"
+ },
+ "dependencies": {
+ "bn.js": {
+ "version": "4.11.6",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz",
+ "integrity": "sha1-UzRK2xRhehP26N0s4okF0cC6MhU="
+ },
+ "ethjs": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/ethjs/-/ethjs-0.3.9.tgz",
+ "integrity": "sha512-gOQzA3tDUjoLpNONSOALJ/rUFtHi5tXl2mholHasF1cvXhoddqi06yU4OJFJu9AGd6n9v9ywzHlYeIKg1t1hdw==",
+ "requires": {
+ "bn.js": "4.11.6",
+ "ethjs-abi": "0.2.1",
+ "ethjs-contract": "0.2.2",
+ "ethjs-filter": "0.1.8",
+ "ethjs-provider-http": "0.1.6",
+ "ethjs-query": "0.3.7",
+ "ethjs-unit": "0.1.6",
+ "ethjs-util": "0.1.3",
+ "js-sha3": "0.5.5",
+ "number-to-bn": "1.7.0"
+ }
+ },
+ "ethjs-abi": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/ethjs-abi/-/ethjs-abi-0.2.1.tgz",
+ "integrity": "sha1-4KepOn6BFjqUR3utVu3lJKtt5TM=",
+ "requires": {
+ "bn.js": "4.11.6",
+ "js-sha3": "0.5.5",
+ "number-to-bn": "1.7.0"
+ }
+ },
+ "ethjs-contract": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/ethjs-contract/-/ethjs-contract-0.2.2.tgz",
+ "integrity": "sha512-xxPqEjsULQ/QNWuvX6Ako0PGs5RxALA8N/H3+boLvnaXDFZVGpD7H63H1gBCRTZyYqCldPpVlVHuw/rD45vazw==",
+ "requires": {
+ "ethjs-abi": "0.2.0",
+ "ethjs-filter": "0.1.8",
+ "ethjs-util": "0.1.3",
+ "js-sha3": "0.5.5"
+ },
+ "dependencies": {
+ "ethjs-abi": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/ethjs-abi/-/ethjs-abi-0.2.0.tgz",
+ "integrity": "sha1-0+LCIQEVIPxJm3FoIDbBT8wvWyU=",
+ "requires": {
+ "bn.js": "4.11.6",
+ "js-sha3": "0.5.5",
+ "number-to-bn": "1.7.0"
+ }
+ }
+ }
+ },
+ "ethjs-filter": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/ethjs-filter/-/ethjs-filter-0.1.8.tgz",
+ "integrity": "sha512-qTDPskDL2UadHwjvM8A+WG9HwM4/FoSY3p3rMJORkHltYcAuiQZd2otzOYKcL5w2Q3sbAkW/E3yt/FPFL/AVXA=="
+ },
+ "ethjs-query": {
+ "version": "0.3.7",
+ "resolved": "https://registry.npmjs.org/ethjs-query/-/ethjs-query-0.3.7.tgz",
+ "integrity": "sha512-TZnKUwfkWjy0SowFdPLtmsytCorHi0i4vvkQn7Jg8rZt33cRzKhuzOwKr/G3vdigCc+ePXOhUGMcJSAPlOG44A==",
+ "requires": {
+ "ethjs-format": "0.2.7",
+ "ethjs-rpc": "0.2.0",
+ "promise-to-callback": "^1.0.0"
+ }
+ },
+ "ethjs-rpc": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/ethjs-rpc/-/ethjs-rpc-0.2.0.tgz",
+ "integrity": "sha512-RINulkNZTKnj4R/cjYYtYMnFFaBcVALzbtEJEONrrka8IeoarNB9Jbzn+2rT00Cv8y/CxAI+GgY1d0/i2iQeOg==",
+ "requires": {
+ "promise-to-callback": "^1.0.0"
+ }
+ },
+ "ethjs-util": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.3.tgz",
+ "integrity": "sha1-39XqSkANxeQhqInK9H4IGtp4u1U=",
+ "requires": {
+ "is-hex-prefixed": "1.0.0",
+ "strip-hex-prefix": "1.0.0"
+ }
+ },
+ "js-sha3": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.5.tgz",
+ "integrity": "sha1-uvDA6MVK1ZA0R9+Wreekobynmko="
+ }
+ }
+ },
"eth-phishing-detect": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/eth-phishing-detect/-/eth-phishing-detect-1.1.12.tgz",
@@ -8663,7 +8808,7 @@
"dependencies": {
"ethereumjs-abi": {
"version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7",
- "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git",
+ "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7",
"requires": {
"bn.js": "^4.10.0",
"ethereumjs-util": "^5.0.0"
@@ -8686,9 +8831,9 @@
}
},
"eth-simple-keyring": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/eth-simple-keyring/-/eth-simple-keyring-1.2.2.tgz",
- "integrity": "sha512-uQVBYshHUOaXVoat1BpLA/QNMCr4hgdFBgwIB7rRmQ+m3vQQAseUsOM+biPDYzq6end+6LjcccElLpQaIZe6dg==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/eth-simple-keyring/-/eth-simple-keyring-1.3.0.tgz",
+ "integrity": "sha512-KNB3WXHyY/NfUiVTllsGScJ8AFTR6OGcrjKn4oSYG+HIvhkOoBqMAJ94BR3zGTvzyg5rHywHwT4L2K4+ZPFp5Q==",
"requires": {
"eth-sig-util": "^1.4.2",
"ethereumjs-util": "^5.1.1",
@@ -8882,6 +9027,63 @@
}
}
},
+ "eth-trezor-keyring": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/eth-trezor-keyring/-/eth-trezor-keyring-0.1.0.tgz",
+ "integrity": "sha512-7ynDXiXGQOh9CslksJSmGGK726lV9fTnIp2QQnjbZJgR4zJIoSUYQYKvT2wXcxLhVrTUl2hLjwKN9QGqDCMVwA==",
+ "requires": {
+ "eth-sig-util": "^1.4.2",
+ "ethereumjs-tx": "^1.3.4",
+ "ethereumjs-util": "^5.1.5",
+ "events": "^2.0.0",
+ "hdkey": "0.8.0"
+ },
+ "dependencies": {
+ "ethereum-common": {
+ "version": "0.0.18",
+ "resolved": "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.0.18.tgz",
+ "integrity": "sha1-L9w1dvIykDNYl26znaeDIT/5Uj8="
+ },
+ "ethereumjs-tx": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-1.3.6.tgz",
+ "integrity": "sha512-wzsEs0mCSLqdDjqSDg6AWh1hyL8H3R/pyZxehkcCXq5MJEFXWz+eJ2jSv+3yEaLy6tXrNP7dmqS3Kyb3zAONkg==",
+ "requires": {
+ "ethereum-common": "^0.0.18",
+ "ethereumjs-util": "^5.0.0"
+ }
+ },
+ "ethereumjs-util": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz",
+ "integrity": "sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA==",
+ "requires": {
+ "bn.js": "^4.11.0",
+ "create-hash": "^1.1.2",
+ "ethjs-util": "^0.1.3",
+ "keccak": "^1.0.2",
+ "rlp": "^2.0.0",
+ "safe-buffer": "^5.1.1",
+ "secp256k1": "^3.0.1"
+ }
+ },
+ "events": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-2.1.0.tgz",
+ "integrity": "sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg=="
+ },
+ "hdkey": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/hdkey/-/hdkey-0.8.0.tgz",
+ "integrity": "sha512-oYsdlK22eobT68N5faWI3776f6tOLyqxLLYwxMx+TP0rkWzuCs0oiOm2VbLWcxdpHFP4LtiRR8udaIX8VkEaZQ==",
+ "requires": {
+ "coinstring": "^2.0.0",
+ "safe-buffer": "^5.1.1",
+ "secp256k1": "^3.0.1"
+ }
+ }
+ }
+ },
"eth-tx-summary": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/eth-tx-summary/-/eth-tx-summary-3.2.3.tgz",
@@ -9380,6 +9582,22 @@
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz",
"integrity": "sha1-UzRK2xRhehP26N0s4okF0cC6MhU="
},
+ "eth-ens-namehash": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/eth-ens-namehash/-/eth-ens-namehash-1.0.2.tgz",
+ "integrity": "sha1-Bezda6wtf9e8XKhKmTxrrZ2k7bk=",
+ "requires": {
+ "idna-uts46": "^1.0.1",
+ "js-sha3": "^0.5.7"
+ },
+ "dependencies": {
+ "js-sha3": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.7.tgz",
+ "integrity": "sha1-DU/9gALVMzqrr0oj7tL2N0yfKOc="
+ }
+ }
+ },
"ethjs-contract": {
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/ethjs-contract/-/ethjs-contract-0.1.9.tgz",
@@ -11262,12 +11480,14 @@
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
- "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+ "optional": true
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -11287,7 +11507,8 @@
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "optional": true
},
"console-control-strings": {
"version": "1.1.0",
@@ -11423,6 +11644,7 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -11823,9 +12045,9 @@
}
},
"ganache-core": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ganache-core/-/ganache-core-2.1.3.tgz",
- "integrity": "sha512-fHDXzzcaB+jZC4q3HnQdcSj/otcVnQ6ddyqmtFtcyv29ET/44nXL5++3MmYBmAn3iLxV8PUgL+meUpm1aim9kg==",
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/ganache-core/-/ganache-core-2.1.5.tgz",
+ "integrity": "sha512-PuHUfISgrQknb7JpspxSzhpoYfqBWoOdTBHQj/81gu6YypRUHzD2Z6gZmFDxDzG30MFElEHp8JtexaGdgq9iYw==",
"dev": true,
"requires": {
"abstract-leveldown": "^3.0.0",
@@ -11837,7 +12059,7 @@
"clone": "^2.1.1",
"ethereumjs-account": "~2.0.4",
"ethereumjs-block": "~1.2.2",
- "ethereumjs-tx": "^1.3.0",
+ "ethereumjs-tx": "1.3.4",
"ethereumjs-util": "^5.2.0",
"ethereumjs-vm": "2.3.5",
"ethereumjs-wallet": "~0.6.0",
@@ -12034,6 +12256,24 @@
}
}
},
+ "ethereumjs-tx": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-1.3.4.tgz",
+ "integrity": "sha512-kOgUd5jC+0tgV7t52UDECMMz9Uf+Lro+6fSpCvzWemtXfMEcwI3EOxf5mVPMRbTFkMMhuERokNNVF3jItAjidg==",
+ "dev": true,
+ "requires": {
+ "ethereum-common": "^0.0.18",
+ "ethereumjs-util": "^5.0.0"
+ },
+ "dependencies": {
+ "ethereum-common": {
+ "version": "0.0.18",
+ "resolved": "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.0.18.tgz",
+ "integrity": "sha1-L9w1dvIykDNYl26znaeDIT/5Uj8=",
+ "dev": true
+ }
+ }
+ },
"ethereumjs-util": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz",
@@ -12211,9 +12451,9 @@
"dev": true
},
"ws": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.1.tgz",
- "integrity": "sha512-2NkHdPKjDBj3CHdnAGNpmlliryKqF+n9MYXX7/wsVC4yqYocKreKNjydPDvT3wShAZnndlM0RytEfTALCDvz7A==",
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz",
+ "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==",
"dev": true,
"requires": {
"async-limiter": "~1.0.0"
@@ -12388,12 +12628,16 @@
"generate-function": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz",
- "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ="
+ "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=",
+ "dev": true,
+ "optional": true
},
"generate-object-property": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz",
"integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=",
+ "dev": true,
+ "optional": true,
"requires": {
"is-property": "^1.0.0"
}
@@ -13379,21 +13623,6 @@
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
"integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA="
},
- "assert-plus": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz",
- "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ="
- },
- "aws-sign2": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz",
- "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8="
- },
- "caseless": {
- "version": "0.11.0",
- "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz",
- "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c="
- },
"chalk": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz",
@@ -13422,79 +13651,16 @@
"is-extendable": "^1.0.1"
}
},
- "form-data": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz",
- "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=",
- "requires": {
- "asynckit": "^0.4.0",
- "combined-stream": "^1.0.5",
- "mime-types": "^2.1.12"
- }
- },
"get-stdin": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
"integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4="
},
- "har-validator": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz",
- "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=",
- "requires": {
- "chalk": "^1.1.1",
- "commander": "^2.9.0",
- "is-my-json-valid": "^2.12.4",
- "pinkie-promise": "^2.0.0"
- },
- "dependencies": {
- "ansi-styles": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
- "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4="
- },
- "chalk": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
- "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
- "requires": {
- "ansi-styles": "^2.2.1",
- "escape-string-regexp": "^1.0.2",
- "has-ansi": "^2.0.0",
- "strip-ansi": "^3.0.0",
- "supports-color": "^2.0.0"
- }
- },
- "strip-ansi": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
- "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
- "requires": {
- "ansi-regex": "^2.0.0"
- }
- },
- "supports-color": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
- "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
- }
- }
- },
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
},
- "http-signature": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz",
- "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=",
- "requires": {
- "assert-plus": "^0.2.0",
- "jsprim": "^1.2.2",
- "sshpk": "^1.7.0"
- }
- },
"is-extendable": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
@@ -13509,9 +13675,9 @@
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA=="
},
"node-sass": {
- "version": "4.9.0",
- "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.0.tgz",
- "integrity": "sha512-QFHfrZl6lqRU3csypwviz2XLgGNOoWQbo2GOvtsfQqOfL4cy1BtWnhx/XUeAO9LT3ahBzSRXcEO6DdvAH9DzSg==",
+ "version": "4.9.2",
+ "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.2.tgz",
+ "integrity": "sha512-LdxoJLZutx0aQXHtWIYwJKMj+9pTjneTcLWJgzf2XbGu0q5pRNqW5QvFCEdm3mc5rJOdru/mzln5d0EZLacf6g==",
"requires": {
"async-foreach": "^0.1.3",
"chalk": "^1.1.1",
@@ -13528,7 +13694,7 @@
"nan": "^2.10.0",
"node-gyp": "^3.3.1",
"npmlog": "^4.0.0",
- "request": "~2.79.0",
+ "request": "2.87.0",
"sass-graph": "^2.2.4",
"stdout-stream": "^1.4.0",
"true-case-path": "^1.0.2"
@@ -13577,38 +13743,6 @@
"extend-shallow": "^3.0.2"
}
},
- "qs": {
- "version": "6.3.2",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz",
- "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw="
- },
- "request": {
- "version": "2.79.0",
- "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz",
- "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=",
- "requires": {
- "aws-sign2": "~0.6.0",
- "aws4": "^1.2.1",
- "caseless": "~0.11.0",
- "combined-stream": "~1.0.5",
- "extend": "~3.0.0",
- "forever-agent": "~0.6.1",
- "form-data": "~2.1.1",
- "har-validator": "~2.0.6",
- "hawk": "~3.1.3",
- "http-signature": "~1.1.0",
- "is-typedarray": "~1.0.0",
- "isstream": "~0.1.2",
- "json-stringify-safe": "~5.0.1",
- "mime-types": "~2.1.7",
- "oauth-sign": "~0.8.1",
- "qs": "~6.3.0",
- "stringstream": "~0.0.4",
- "tough-cookie": "~2.3.0",
- "tunnel-agent": "~0.4.1",
- "uuid": "^3.0.0"
- }
- },
"strip-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
@@ -13631,16 +13765,6 @@
"requires": {
"has-flag": "^3.0.0"
}
- },
- "tunnel-agent": {
- "version": "0.4.3",
- "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz",
- "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us="
- },
- "uuid": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz",
- "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA=="
}
}
},
@@ -14622,6 +14746,8 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz",
"integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=",
+ "dev": true,
+ "optional": true,
"requires": {
"boom": "2.x.x",
"cryptiles": "2.x.x",
@@ -14632,7 +14758,9 @@
"hoek": {
"version": "2.16.3",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz",
- "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0="
+ "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=",
+ "dev": true,
+ "optional": true
}
}
},
@@ -15301,6 +15429,21 @@
},
"dependencies": {
"punycode": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
+ }
+ }
+ },
+ "idna-uts46-hx": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/idna-uts46-hx/-/idna-uts46-hx-2.3.1.tgz",
+ "integrity": "sha512-PWoF9Keq6laYdIRwwCdhTPl60xRqAloYNMQLiyUnG42VjT53oW07BXIRM+NK7eQjzXjAk2gUvX9caRxlnF9TAA==",
+ "requires": {
+ "punycode": "2.1.0"
+ },
+ "dependencies": {
+ "punycode": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz",
"integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0="
@@ -15914,12 +16057,16 @@
"is-my-ip-valid": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz",
- "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ=="
+ "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==",
+ "dev": true,
+ "optional": true
},
"is-my-json-valid": {
"version": "2.17.2",
"resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz",
"integrity": "sha512-IBhBslgngMQN8DDSppmgDv7RNrlFotuuDsKcrCP3+HbFaVivIBU7u9oiiErw8sH4ynx3+gOGQ3q2otkgiSi6kg==",
+ "dev": true,
+ "optional": true,
"requires": {
"generate-function": "^2.0.0",
"generate-object-property": "^1.1.0",
@@ -16043,7 +16190,9 @@
"is-property": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
- "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ="
+ "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=",
+ "dev": true,
+ "optional": true
},
"is-redirect": {
"version": "1.0.0",
@@ -17053,7 +17202,9 @@
"jsonpointer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz",
- "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk="
+ "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=",
+ "dev": true,
+ "optional": true
},
"jsprim": {
"version": "1.4.1",
@@ -17963,9 +18114,9 @@
}
},
"level-sublevel": {
- "version": "6.6.2",
- "resolved": "https://registry.npmjs.org/level-sublevel/-/level-sublevel-6.6.2.tgz",
- "integrity": "sha512-+hptqmFYPKFju9QG4F6scvx3ZXkhrSmmhYui+hPzRn/jiC3DJ6VNZRKsIhGMpeajVBWfRV7XiysUThrJ/7PgXQ==",
+ "version": "6.6.5",
+ "resolved": "https://registry.npmjs.org/level-sublevel/-/level-sublevel-6.6.5.tgz",
+ "integrity": "sha512-SBSR60x+dghhwGUxPKS+BvV1xNqnwsEUBKmnFepPaHJ6VkBXyPK9SImGc3K2BkwBfpxlt7GKkBNlCnrdufsejA==",
"dev": true,
"requires": {
"bytewise": "~1.1.0",
@@ -18798,9 +18949,9 @@
}
},
"log4js": {
- "version": "2.10.0",
- "resolved": "https://registry.npmjs.org/log4js/-/log4js-2.10.0.tgz",
- "integrity": "sha512-NnhN9PjFF9zhxinAjlmDYvkqqrIW+yA3LLJAoTJ3fs6d1zru86OqQHfsxiUcc1kRq3z+faGR4DeyXUfiNbVxKQ==",
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/log4js/-/log4js-2.11.0.tgz",
+ "integrity": "sha512-z1XdwyGFg8/WGkOyF6DPJjivCWNLKrklGdViywdYnSKOvgtEBo2UyEMZS5sD2mZrQlU3TvO8wDWLc8mzE1ncBQ==",
"dev": true,
"requires": {
"amqplib": "^0.5.2",
@@ -18819,9 +18970,9 @@
},
"dependencies": {
"circular-json": {
- "version": "0.5.4",
- "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.5.4.tgz",
- "integrity": "sha512-vnJA8KS0BfOihugYEUkLRcnmq21FbuivbxgzDLXNs3zIk4KllV4Mx4UuTzBXht9F00C7QfD1YqMXg1zP6EXpig==",
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.5.5.tgz",
+ "integrity": "sha512-13YaR6kiz0kBNmIVM87Io8Hp7bWOo4r61vkEANy8iH9R9bc6avud/1FT0SBpqR1RpIQADOh/Q+yHZDA1iL6ysA==",
"dev": true
},
"debug": {
@@ -20289,6 +20440,38 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
+ "multihashes": {
+ "version": "0.4.13",
+ "resolved": "https://registry.npmjs.org/multihashes/-/multihashes-0.4.13.tgz",
+ "integrity": "sha512-HwJGEKPCpLlNlgGQA56CYh/Wsqa+c4JAq8+mheIgw7OK5T4QvNJqgp6TH8gZ4q4l1aiWeNat/H/MrFXmTuoFfQ==",
+ "requires": {
+ "bs58": "^4.0.1",
+ "varint": "^5.0.0"
+ },
+ "dependencies": {
+ "base-x": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.4.tgz",
+ "integrity": "sha512-UYOadoSIkEI/VrRGSG6qp93rp2WdokiAiNYDfGW5qURAY8GiAQkvMbwNNSDYiVJopqv4gCna7xqf4rrNGp+5AA==",
+ "requires": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "bs58": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
+ "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=",
+ "requires": {
+ "base-x": "^3.0.2"
+ }
+ },
+ "varint": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/varint/-/varint-5.0.0.tgz",
+ "integrity": "sha1-2Ca4n3SQcy+rwMDtaT7Uddyynr8="
+ }
+ }
+ },
"multimatch": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/multimatch/-/multimatch-2.1.0.tgz",
@@ -20717,9 +20900,9 @@
}
},
"node-sass": {
- "version": "4.9.0",
- "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.0.tgz",
- "integrity": "sha512-QFHfrZl6lqRU3csypwviz2XLgGNOoWQbo2GOvtsfQqOfL4cy1BtWnhx/XUeAO9LT3ahBzSRXcEO6DdvAH9DzSg==",
+ "version": "4.9.2",
+ "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.2.tgz",
+ "integrity": "sha512-LdxoJLZutx0aQXHtWIYwJKMj+9pTjneTcLWJgzf2XbGu0q5pRNqW5QvFCEdm3mc5rJOdru/mzln5d0EZLacf6g==",
"dev": true,
"requires": {
"async-foreach": "^0.1.3",
@@ -20737,30 +20920,12 @@
"nan": "^2.10.0",
"node-gyp": "^3.3.1",
"npmlog": "^4.0.0",
- "request": "~2.79.0",
+ "request": "2.87.0",
"sass-graph": "^2.2.4",
"stdout-stream": "^1.4.0",
"true-case-path": "^1.0.2"
},
"dependencies": {
- "assert-plus": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz",
- "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=",
- "dev": true
- },
- "aws-sign2": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz",
- "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=",
- "dev": true
- },
- "caseless": {
- "version": "0.11.0",
- "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz",
- "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=",
- "dev": true
- },
"cross-spawn": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz",
@@ -20771,97 +20936,17 @@
"which": "^1.2.9"
}
},
- "form-data": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz",
- "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=",
- "dev": true,
- "requires": {
- "asynckit": "^0.4.0",
- "combined-stream": "^1.0.5",
- "mime-types": "^2.1.12"
- }
- },
"get-stdin": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
"integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=",
"dev": true
},
- "har-validator": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz",
- "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=",
- "dev": true,
- "requires": {
- "chalk": "^1.1.1",
- "commander": "^2.9.0",
- "is-my-json-valid": "^2.12.4",
- "pinkie-promise": "^2.0.0"
- }
- },
- "http-signature": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz",
- "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=",
- "dev": true,
- "requires": {
- "assert-plus": "^0.2.0",
- "jsprim": "^1.2.2",
- "sshpk": "^1.7.0"
- }
- },
"nan": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==",
"dev": true
- },
- "qs": {
- "version": "6.3.2",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz",
- "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=",
- "dev": true
- },
- "request": {
- "version": "2.79.0",
- "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz",
- "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=",
- "dev": true,
- "requires": {
- "aws-sign2": "~0.6.0",
- "aws4": "^1.2.1",
- "caseless": "~0.11.0",
- "combined-stream": "~1.0.5",
- "extend": "~3.0.0",
- "forever-agent": "~0.6.1",
- "form-data": "~2.1.1",
- "har-validator": "~2.0.6",
- "hawk": "~3.1.3",
- "http-signature": "~1.1.0",
- "is-typedarray": "~1.0.0",
- "isstream": "~0.1.2",
- "json-stringify-safe": "~5.0.1",
- "mime-types": "~2.1.7",
- "oauth-sign": "~0.8.1",
- "qs": "~6.3.0",
- "stringstream": "~0.0.4",
- "tough-cookie": "~2.3.0",
- "tunnel-agent": "~0.4.1",
- "uuid": "^3.0.0"
- }
- },
- "tunnel-agent": {
- "version": "0.4.3",
- "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz",
- "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=",
- "dev": true
- },
- "uuid": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz",
- "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==",
- "dev": true
}
}
},
@@ -26664,6 +26749,15 @@
"deep-diff": "^0.3.5"
}
},
+ "redux-mock-store": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.3.tgz",
+ "integrity": "sha512-ryhkkb/4D4CUGpAV2ln1GOY/uh51aczjcRz9k2L2bPx/Xja3c5pSGJJPyR25GNVRXtKIExScdAgFdiXp68GmJA==",
+ "dev": true,
+ "requires": {
+ "lodash.isplainobject": "^4.0.6"
+ }
+ },
"redux-test-utils": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/redux-test-utils/-/redux-test-utils-0.2.2.tgz",
@@ -27065,6 +27159,11 @@
}
}
},
+ "reselect": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-3.0.1.tgz",
+ "integrity": "sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc="
+ },
"resolve": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz",
@@ -28295,6 +28394,8 @@
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz",
"integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=",
+ "dev": true,
+ "optional": true,
"requires": {
"hoek": "2.x.x"
},
@@ -28302,7 +28403,9 @@
"hoek": {
"version": "2.16.3",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz",
- "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0="
+ "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=",
+ "dev": true,
+ "optional": true
}
}
},
@@ -29080,7 +29183,9 @@
"stringstream": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz",
- "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA=="
+ "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==",
+ "dev": true,
+ "optional": true
},
"strip-ansi": {
"version": "3.0.1",
diff --git a/package.json b/package.json
index 6489dfe24..d7f126cb4 100644
--- a/package.json
+++ b/package.json
@@ -54,9 +54,19 @@
"babelify",
{
"presets": [
- "es2015",
+ [
+ "env",
+ {
+ "debug": true
+ }
+ ],
"stage-0"
]
+ },
+ {
+ "plugins": [
+ "transform-class-properties"
+ ]
}
],
"reactify",
@@ -95,15 +105,18 @@
"eth-bin-to-ops": "^1.0.1",
"eth-block-tracker": "^4.0.1",
"eth-contract-metadata": "github:MetaMask/eth-contract-metadata#master",
- "eth-hd-keyring": "^1.2.1",
"eth-json-rpc-filters": "^2.1.1",
- "eth-json-rpc-infura": "^3.0.0",
"eth-json-rpc-middleware": "^2.4.0",
"eth-keyring-controller": "^3.1.4",
+ "eth-ens-namehash": "^2.0.8",
+ "eth-hd-keyring": "^2.0.0",
+ "eth-json-rpc-infura": "^3.0.0",
+ "eth-method-registry": "^1.0.0",
"eth-phishing-detect": "^1.1.4",
"eth-query": "^2.1.2",
"eth-sig-util": "^1.4.2",
"eth-token-tracker": "^1.1.4",
+ "eth-trezor-keyring": "^0.1.0",
"ethereumjs-abi": "^0.6.4",
"ethereumjs-tx": "^1.3.0",
"ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9",
@@ -143,6 +156,7 @@
"metamascara": "^2.0.0",
"metamask-logo": "^2.1.4",
"mkdirp": "^0.5.1",
+ "multihashes": "^0.4.12",
"multiplex": "^6.7.0",
"number-to-bn": "^1.7.0",
"obj-multiplex": "^1.0.0",
@@ -182,6 +196,7 @@
"redux-logger": "^3.0.6",
"redux-thunk": "^2.2.0",
"request-promise": "^4.2.1",
+ "reselect": "^3.0.1",
"sandwich-expando": "^1.1.3",
"semaphore": "^1.0.5",
"semver": "^5.4.1",
@@ -207,6 +222,7 @@
"babel-plugin-transform-async-to-generator": "^6.24.1",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-polyfill": "^6.23.0",
+ "babel-preset-env": "^1.7.0",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"babel-register": "^6.7.2",
@@ -231,11 +247,13 @@
"eslint-plugin-json": "^1.2.0",
"eslint-plugin-mocha": "^5.0.0",
"eslint-plugin-react": "^7.4.0",
+ "eth-json-rpc-middleware": "^1.6.0",
+ "eth-keyring-controller": "^4.0.0",
"file-loader": "^1.1.11",
"fs-extra": "^6.0.1",
"fs-promise": "^2.0.3",
"ganache-cli": "^6.1.0",
- "ganache-core": "^2.1.3",
+ "ganache-core": "^2.1.5",
"geckodriver": "^1.11.0",
"gh-pages": "^1.2.0",
"gifencoder": "^1.1.0",
@@ -271,7 +289,7 @@
"mocha-jsdom": "^1.1.0",
"mocha-sinon": "^2.0.0",
"nock": "^9.0.14",
- "node-sass": "^4.9.0",
+ "node-sass": "^4.9.2",
"nsp": "^3.2.1",
"nyc": "^13.0.0",
"open": "0.0.5",
@@ -286,6 +304,7 @@
"react-addons-test-utils": "^15.5.1",
"react-test-renderer": "^15.6.2",
"react-testutils-additions": "^15.2.0",
+ "redux-mock-store": "^1.5.3",
"redux-test-utils": "^0.2.2",
"resolve-url-loader": "^2.3.0",
"rimraf": "^2.6.2",
@@ -304,6 +323,7 @@
"watchify": "^3.11.0"
},
"engines": {
- "node": ">=8.0.0"
+ "node": "8.11.3",
+ "npm": "^6.1.0"
}
}
diff --git a/test/e2e/beta/contract-test/contract.js b/test/e2e/beta/contract-test/contract.js
index 4b0fd3377..8af008dce 100644
--- a/test/e2e/beta/contract-test/contract.js
+++ b/test/e2e/beta/contract-test/contract.js
@@ -32,35 +32,103 @@ var piggybankContract = web3.eth.contract([{'constant': false, 'inputs': [{'name
const deployButton = document.getElementById('deployButton')
const depositButton = document.getElementById('depositButton')
const withdrawButton = document.getElementById('withdrawButton')
+const sendButton = document.getElementById('sendButton')
+const createToken = document.getElementById('createToken')
+const transferTokens = document.getElementById('transferTokens')
+const approveTokens = document.getElementById('approveTokens')
-deployButton.addEventListener('click', function (event) {
-
- piggybankContract.new(
- {
- from: web3.eth.accounts[0],
- data: '0x608060405234801561001057600080fd5b5033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506000808190555061023b806100686000396000f300608060405260043610610057576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e1a7d4d1461005c5780638da5cb5b1461009d578063d0e30db0146100f4575b600080fd5b34801561006857600080fd5b5061008760048036038101908080359060200190929190505050610112565b6040518082815260200191505060405180910390f35b3480156100a957600080fd5b506100b26101d0565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100fc6101f6565b6040518082815260200191505060405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614151561017057600080fd5b8160008082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc839081150290604051600060405180830381858888f193505050501580156101c5573d6000803e3d6000fd5b506000549050919050565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60003460008082825401925050819055506000549050905600a165627a7a72305820f237db3ec816a52589d82512117bc85bc08d3537683ffeff9059108caf3e5d400029',
- gas: '4700000',
- }, function (e, contract) {
- console.log(e, contract)
- console.log('deployed!', contract)
- if (typeof contract.address !== 'undefined') {
- console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash)
-
- console.log(`contract`, contract)
-
- depositButton.addEventListener('click', function (event) {
- console.log('contract.deposit', { from: web3.eth.accounts[0], value: '0x29a2241af62c0000' })
- contract.deposit({ from: web3.eth.accounts[0], value: '0x29a2241af62c0000' }, function (result) {
- console.log(result)
- })
+deployButton.addEventListener('click', async function (event) {
+ var piggybank = await piggybankContract.new(
+ {
+ from: web3.eth.accounts[0],
+ data: '0x608060405234801561001057600080fd5b5033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506000808190555061023b806100686000396000f300608060405260043610610057576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e1a7d4d1461005c5780638da5cb5b1461009d578063d0e30db0146100f4575b600080fd5b34801561006857600080fd5b5061008760048036038101908080359060200190929190505050610112565b6040518082815260200191505060405180910390f35b3480156100a957600080fd5b506100b26101d0565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100fc6101f6565b6040518082815260200191505060405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614151561017057600080fd5b8160008082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc839081150290604051600060405180830381858888f193505050501580156101c5573d6000803e3d6000fd5b506000549050919050565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60003460008082825401925050819055506000549050905600a165627a7a72305820f237db3ec816a52589d82512117bc85bc08d3537683ffeff9059108caf3e5d400029',
+ gas: '4700000',
+ }, function (e, contract) {
+ console.log(e, contract)
+ if (typeof contract.address !== 'undefined') {
+ console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash)
+
+ console.log(`contract`, contract)
+
+ depositButton.addEventListener('click', function (event) {
+ contract.deposit({ from: web3.eth.accounts[0], value: '0x3782dace9d900000' }, function (result) {
+ console.log(result)
+ })
+ })
+
+ withdrawButton.addEventListener('click', function (event) {
+ contract.withdraw('0xde0b6b3a7640000', { from: web3.eth.accounts[0] }, function (result) {
+ console.log(result)
+ })
+ })
+ }
+ })
+
+ console.log(piggybank)
+})
+
+sendButton.addEventListener('click', function (event) {
+ web3.eth.sendTransaction({
+ from: web3.eth.accounts[0],
+ to: '0x2f318C334780961FB129D2a6c30D0763d9a5C970',
+ value: '0x29a2241af62c0000',
+ gas: 21000,
+ gasPrice: 20000000000,
+ }, (result) => {
+ console.log(result)
+ })
+})
+
+
+createToken.addEventListener('click', async function (event) {
+ var _initialAmount = 100
+ var _tokenName = 'TST'
+ var _decimalUnits = 0
+ var _tokenSymbol = 'TST'
+ var humanstandardtokenContract = web3.eth.contract([{'constant': true, 'inputs': [], 'name': 'name', 'outputs': [{'name': '', 'type': 'string'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': false, 'inputs': [{'name': '_spender', 'type': 'address'}, {'name': '_value', 'type': 'uint256'}], 'name': 'approve', 'outputs': [{'name': 'success', 'type': 'bool'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': true, 'inputs': [], 'name': 'totalSupply', 'outputs': [{'name': '', 'type': 'uint256'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': false, 'inputs': [{'name': '_from', 'type': 'address'}, {'name': '_to', 'type': 'address'}, {'name': '_value', 'type': 'uint256'}], 'name': 'transferFrom', 'outputs': [{'name': 'success', 'type': 'bool'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': true, 'inputs': [], 'name': 'decimals', 'outputs': [{'name': '', 'type': 'uint8'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': true, 'inputs': [], 'name': 'version', 'outputs': [{'name': '', 'type': 'string'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': true, 'inputs': [{'name': '_owner', 'type': 'address'}], 'name': 'balanceOf', 'outputs': [{'name': 'balance', 'type': 'uint256'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': true, 'inputs': [], 'name': 'symbol', 'outputs': [{'name': '', 'type': 'string'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': false, 'inputs': [{'name': '_to', 'type': 'address'}, {'name': '_value', 'type': 'uint256'}], 'name': 'transfer', 'outputs': [{'name': 'success', 'type': 'bool'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': false, 'inputs': [{'name': '_spender', 'type': 'address'}, {'name': '_value', 'type': 'uint256'}, {'name': '_extraData', 'type': 'bytes'}], 'name': 'approveAndCall', 'outputs': [{'name': 'success', 'type': 'bool'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': true, 'inputs': [{'name': '_owner', 'type': 'address'}, {'name': '_spender', 'type': 'address'}], 'name': 'allowance', 'outputs': [{'name': 'remaining', 'type': 'uint256'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'inputs': [{'name': '_initialAmount', 'type': 'uint256'}, {'name': '_tokenName', 'type': 'string'}, {'name': '_decimalUnits', 'type': 'uint8'}, {'name': '_tokenSymbol', 'type': 'string'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'constructor'}, {'payable': false, 'stateMutability': 'nonpayable', 'type': 'fallback'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': '_from', 'type': 'address'}, {'indexed': true, 'name': '_to', 'type': 'address'}, {'indexed': false, 'name': '_value', 'type': 'uint256'}], 'name': 'Transfer', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': '_owner', 'type': 'address'}, {'indexed': true, 'name': '_spender', 'type': 'address'}, {'indexed': false, 'name': '_value', 'type': 'uint256'}], 'name': 'Approval', 'type': 'event'}])
+ return humanstandardtokenContract.new(
+ _initialAmount,
+ _tokenName,
+ _decimalUnits,
+ _tokenSymbol,
+ {
+ from: web3.eth.accounts[0],
+ data: '0x60806040526040805190810160405280600481526020017f48302e3100000000000000000000000000000000000000000000000000000000815250600690805190602001906200005192919062000143565b503480156200005f57600080fd5b50604051620011f3380380620011f383398101806040528101908080519060200190929190805182019291906020018051906020019092919080518201929190505050836000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508360028190555082600390805190602001906200010492919062000143565b5081600460006101000a81548160ff021916908360ff16021790555080600590805190602001906200013892919062000143565b5050505050620001f2565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106200018657805160ff1916838001178555620001b7565b82800160010185558215620001b7579182015b82811115620001b657825182559160200191906001019062000199565b5b509050620001c69190620001ca565b5090565b620001ef91905b80821115620001eb576000816000905550600101620001d1565b5090565b90565b610ff180620002026000396000f3006080604052600436106100af576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806306fdde03146100c1578063095ea7b31461015157806318160ddd146101b657806323b872dd146101e1578063313ce5671461026657806354fd4d501461029757806370a082311461032757806395d89b411461037e578063a9059cbb1461040e578063cae9ca5114610473578063dd62ed3e1461051e575b3480156100bb57600080fd5b50600080fd5b3480156100cd57600080fd5b506100d6610595565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156101165780820151818401526020810190506100fb565b50505050905090810190601f1680156101435780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561015d57600080fd5b5061019c600480360381019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610633565b604051808215151515815260200191505060405180910390f35b3480156101c257600080fd5b506101cb610725565b6040518082815260200191505060405180910390f35b3480156101ed57600080fd5b5061024c600480360381019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff1690602001909291908035906020019092919050505061072b565b604051808215151515815260200191505060405180910390f35b34801561027257600080fd5b5061027b6109a4565b604051808260ff1660ff16815260200191505060405180910390f35b3480156102a357600080fd5b506102ac6109b7565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156102ec5780820151818401526020810190506102d1565b50505050905090810190601f1680156103195780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561033357600080fd5b50610368600480360381019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610a55565b6040518082815260200191505060405180910390f35b34801561038a57600080fd5b50610393610a9d565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156103d35780820151818401526020810190506103b8565b50505050905090810190601f1680156104005780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561041a57600080fd5b50610459600480360381019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610b3b565b604051808215151515815260200191505060405180910390f35b34801561047f57600080fd5b50610504600480360381019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190803590602001908201803590602001908080601f0160208091040260200160405190810160405280939291908181526020018383808284378201915050505050509192919290505050610ca1565b604051808215151515815260200191505060405180910390f35b34801561052a57600080fd5b5061057f600480360381019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610f3e565b6040518082815260200191505060405180910390f35b60038054600181600116156101000203166002900480601f01602080910402602001604051908101604052809291908181526020018280546001816001161561010002031660029004801561062b5780601f106106005761010080835404028352916020019161062b565b820191906000526020600020905b81548152906001019060200180831161060e57829003601f168201915b505050505081565b600081600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040518082815260200191505060405180910390a36001905092915050565b60025481565b6000816000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054101580156107f7575081600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410155b80156108035750600082115b1561099857816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282540192505081905550816000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828254039250508190555081600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825403925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a36001905061099d565b600090505b9392505050565b600460009054906101000a900460ff1681565b60068054600181600116156101000203166002900480601f016020809104026020016040519081016040528092919081815260200182805460018160011615610100020316600290048015610a4d5780601f10610a2257610100808354040283529160200191610a4d565b820191906000526020600020905b815481529060010190602001808311610a3057829003601f168201915b505050505081565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b60058054600181600116156101000203166002900480601f016020809104026020016040519081016040528092919081815260200182805460018160011615610100020316600290048015610b335780601f10610b0857610100808354040283529160200191610b33565b820191906000526020600020905b815481529060010190602001808311610b1657829003601f168201915b505050505081565b6000816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410158015610b8b5750600082115b15610c9657816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282540392505081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a360019050610c9b565b600090505b92915050565b600082600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508373ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925856040518082815260200191505060405180910390a38373ffffffffffffffffffffffffffffffffffffffff1660405180807f72656365697665417070726f76616c28616464726573732c75696e743235362c81526020017f616464726573732c627974657329000000000000000000000000000000000000815250602e01905060405180910390207c01000000000000000000000000000000000000000000000000000000009004338530866040518563ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018481526020018373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001828051906020019080838360005b83811015610ee2578082015181840152602081019050610ec7565b50505050905090810190601f168015610f0f5780820380516001836020036101000a031916815260200191505b509450505050506000604051808303816000875af1925050501515610f3357600080fd5b600190509392505050565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050929150505600a165627a7a723058200052919e0bc22b5adcd3d320be977df3a1dcc35d1a0160287383ba371900a1c50029',
+ gas: '4700000',
+ gasPrice: '20000000000',
+ }, function (e, contract) {
+ console.log(e, contract)
+ if (typeof contract.address !== 'undefined') {
+ console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash)
+
+ document.getElementById('tokenAddress').innerHTML = contract.address
+
+ transferTokens.addEventListener('click', function (event) {
+ console.log(`event`, event)
+ contract.transfer('0x2f318C334780961FB129D2a6c30D0763d9a5C970', '7', {
+ from: web3.eth.accounts[0],
+ to: contract.address,
+ data: '0xa9059cbb0000000000000000000000002f318C334780961FB129D2a6c30D0763d9a5C970000000000000000000000000000000000000000000000000000000000000000a',
+ gas: 60000,
+ gasPrice: '20000000000',
+ }, function (result) {
+ console.log('result', result)
})
+ })
- withdrawButton.addEventListener('click', function (event) {
- contract.withdraw('0xde0b6b3a7640000', { from: web3.eth.accounts[0] }, function (result) {
- console.log(result)
- })
+ approveTokens.addEventListener('click', function (event) {
+ contract.approve('0x2f318C334780961FB129D2a6c30D0763d9a5C970', '7', {
+ from: web3.eth.accounts[0],
+ to: contract.address,
+ data: '0x095ea7b30000000000000000000000002f318C334780961FB129D2a6c30D0763d9a5C9700000000000000000000000000000000000000000000000000000000000000005',
+ gas: 60000,
+ gasPrice: '20000000000',
+ }, function (result) {
+ console.log(result)
})
- }
- })
+ })
+ }
+ })
})
+
diff --git a/test/e2e/beta/contract-test/index.html b/test/e2e/beta/contract-test/index.html
index 0868633f7..0d63fd940 100644
--- a/test/e2e/beta/contract-test/index.html
+++ b/test/e2e/beta/contract-test/index.html
@@ -1,8 +1,33 @@
<html>
+<head>
+ <title>E2E Test Dapp</title>
+</head>
<body>
- <button id="deployButton">Deploy Contract</button>
- <button id="depositButton">Deposit</button>
- <button id="withdrawButton">Withdraw</button>
-</body>
+ <div style="display: flex; flex-flow: column;">
+ <div style="display: flex; font-size: 1.25rem;">Contract</div>
+ <div style="display: flex;">
+ <button id="deployButton">Deploy Contract</button>
+ <button id="depositButton">Deposit</button>
+ <button id="withdrawButton">Withdraw</button>
+ </div>
+ </div>
+ <div style="display: flex; flex-flow: column;">
+ <div style="display: flex; font-size: 1.25rem;">Send eth</div>
+ <div style="display: flex;">
+ <button id="sendButton">Send</button>
+ </div>
+ </div>
+ <div style="display: flex; flex-flow: column;">
+ <div style="display: flex; font-size: 1.25rem;">Send tokens</div>
+ <div id="tokenAddress"></div>
+ <div style="display: flex;">
+ <button id="createToken">Create Token</button>
+ <button id="transferTokens">Transfer Tokens</button>
+ <button id="approveTokens">Approve Tokens</button>
+ </div>
+ </div>
+
<script src="contract.js"></script>
+</body>
+
</html> \ No newline at end of file
diff --git a/test/e2e/beta/from-import-beta-ui.spec.js b/test/e2e/beta/from-import-beta-ui.spec.js
index 86a6c441c..11d28264c 100644
--- a/test/e2e/beta/from-import-beta-ui.spec.js
+++ b/test/e2e/beta/from-import-beta-ui.spec.js
@@ -385,4 +385,51 @@ describe('Using MetaMask with an existing account', function () {
})
})
+ describe('Connects to a Hardware wallet', () => {
+ it('choose Connect Hardware Wallet from the account menu', async () => {
+ await driver.findElement(By.css('.account-menu__icon')).click()
+ await delay(regularDelayMs)
+
+ const [connectAccount] = await findElements(driver, By.xpath(`//div[contains(text(), 'Connect Hardware Wallet')]`))
+ await connectAccount.click()
+ await delay(regularDelayMs)
+ })
+
+ it('should open the TREZOR Connect popup', async () => {
+ const connectButtons = await findElements(driver, By.xpath(`//button[contains(text(), 'Connect to Trezor')]`))
+ await connectButtons[0].click()
+ await delay(regularDelayMs)
+ const allWindows = await driver.getAllWindowHandles()
+ switch (process.env.SELENIUM_BROWSER) {
+ case 'chrome':
+ assert.equal(allWindows.length, 2)
+ break
+ default:
+ assert.equal(allWindows.length, 1)
+ }
+ })
+
+ it('should show the "Browser not supported" screen for non Chrome browsers', async () => {
+ if (process.env.SELENIUM_BROWSER !== 'chrome') {
+ const title = await findElements(driver, By.xpath(`//h3[contains(text(), 'Your Browser is not supported...')]`))
+ assert.equal(title.length, 1)
+
+ const downloadChromeButtons = await findElements(driver, By.xpath(`//button[contains(text(), 'Download Google Chrome')]`))
+ assert.equal(downloadChromeButtons.length, 1)
+
+ await downloadChromeButtons[0].click()
+ await delay(regularDelayMs)
+
+ const [newUITab, downloadChromeTab] = await driver.getAllWindowHandles()
+
+ await driver.switchTo().window(downloadChromeTab)
+ await delay(regularDelayMs)
+ const tabUrl = await driver.getCurrentUrl()
+ assert.equal(tabUrl, 'https://www.google.com/chrome/')
+ await driver.close()
+ await delay(regularDelayMs)
+ await driver.switchTo().window(newUITab)
+ }
+ })
+ })
})
diff --git a/test/e2e/beta/helpers.js b/test/e2e/beta/helpers.js
index 35015ab9c..84e578d3e 100644
--- a/test/e2e/beta/helpers.js
+++ b/test/e2e/beta/helpers.js
@@ -1,11 +1,34 @@
+const fs = require('fs')
+const mkdirp = require('mkdirp')
+const pify = require('pify')
+const assert = require('assert')
const { delay } = require('../func')
const { until } = require('selenium-webdriver')
module.exports = {
+ assertElementNotPresent,
checkBrowserForConsoleErrors,
+ closeAllWindowHandlesExcept,
findElement,
findElements,
+ loadExtension,
openNewPage,
+ switchToWindowWithTitle,
+ verboseReportOnFailure,
+ waitUntilXWindowHandles,
+}
+
+async function loadExtension (driver, extensionId) {
+ switch (process.env.SELENIUM_BROWSER) {
+ case 'chrome': {
+ await driver.get(`chrome-extension://${extensionId}/home.html`)
+ break
+ }
+ case 'firefox': {
+ await driver.get(`moz-extension://${extensionId}/home.html`)
+ break
+ }
+ }
}
async function checkBrowserForConsoleErrors (driver) {
@@ -26,6 +49,21 @@ async function checkBrowserForConsoleErrors (driver) {
return errorObjects.filter(entry => !ignoredErrorMessages.some(message => entry.message.includes(message)))
}
+async function verboseReportOnFailure (driver, test) {
+ let artifactDir
+ if (process.env.SELENIUM_BROWSER === 'chrome') {
+ artifactDir = `./test-artifacts/chrome/${test.title}`
+ } else if (process.env.SELENIUM_BROWSER === 'firefox') {
+ artifactDir = `./test-artifacts/firefox/${test.title}`
+ }
+ const filepathBase = `${artifactDir}/test-failure`
+ await pify(mkdirp)(artifactDir)
+ const screenshot = await driver.takeScreenshot()
+ await pify(fs.writeFile)(`${filepathBase}-screenshot.png`, screenshot, { encoding: 'base64' })
+ const htmlSource = await driver.getPageSource()
+ await pify(fs.writeFile)(`${filepathBase}-dom.html`, htmlSource)
+}
+
async function findElement (driver, by, timeout = 10000) {
return driver.wait(until.elementLocated(by), timeout)
}
@@ -39,9 +77,57 @@ async function openNewPage (driver, url) {
await delay(1000)
const handles = await driver.getAllWindowHandles()
- const secondHandle = handles[1]
- await driver.switchTo().window(secondHandle)
+ const lastHandle = handles[handles.length - 1]
+ await driver.switchTo().window(lastHandle)
await driver.get(url)
await delay(1000)
}
+
+async function waitUntilXWindowHandles (driver, x) {
+ const windowHandles = await driver.getAllWindowHandles()
+ if (windowHandles.length === x) return
+ await delay(1000)
+ return await waitUntilXWindowHandles(driver, x)
+}
+
+async function switchToWindowWithTitle (driver, title, windowHandles) {
+ if (!windowHandles) {
+ windowHandles = await driver.getAllWindowHandles()
+ } else if (windowHandles.length === 0) {
+ throw new Error('No window with title: ' + title)
+ }
+ const firstHandle = windowHandles[0]
+ await driver.switchTo().window(firstHandle)
+ const handleTitle = await driver.getTitle()
+
+ if (handleTitle === title) {
+ return firstHandle
+ } else {
+ return await switchToWindowWithTitle(driver, title, windowHandles.slice(1))
+ }
+}
+
+async function closeAllWindowHandlesExcept (driver, exceptions, windowHandles) {
+ exceptions = typeof exceptions === 'string' ? [ exceptions ] : exceptions
+ windowHandles = windowHandles || await driver.getAllWindowHandles()
+ const lastWindowHandle = windowHandles.pop()
+ if (!exceptions.includes(lastWindowHandle)) {
+ await driver.switchTo().window(lastWindowHandle)
+ await delay(1000)
+ await driver.close()
+ await delay(1000)
+ }
+ return windowHandles.length && await closeAllWindowHandlesExcept(driver, exceptions, windowHandles)
+}
+
+async function assertElementNotPresent (webdriver, driver, by) {
+ try {
+ const dataTab = await findElement(driver, by, 4000)
+ if (dataTab) {
+ assert(false, 'Data tab should not be present')
+ }
+ } catch (err) {
+ assert(err instanceof webdriver.error.NoSuchElementError)
+ }
+}
diff --git a/test/e2e/beta/metamask-beta-ui.spec.js b/test/e2e/beta/metamask-beta-ui.spec.js
index 45a22f0b3..37e556b55 100644
--- a/test/e2e/beta/metamask-beta-ui.spec.js
+++ b/test/e2e/beta/metamask-beta-ui.spec.js
@@ -9,10 +9,15 @@ const {
verboseReportOnFailure,
} = require('../func')
const {
+ assertElementNotPresent,
+ checkBrowserForConsoleErrors,
+ closeAllWindowHandlesExcept,
findElement,
findElements,
- checkBrowserForConsoleErrors,
+ loadExtension,
openNewPage,
+ switchToWindowWithTitle,
+ waitUntilXWindowHandles,
} = require('./helpers')
describe('MetaMask', function () {
@@ -22,7 +27,7 @@ describe('MetaMask', function () {
let tokenAddress
const testSeedPhrase = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent'
- const tinyDelayMs = 1000
+ const tinyDelayMs = 200
const regularDelayMs = tinyDelayMs * 2
const largeDelayMs = regularDelayMs * 2
@@ -67,21 +72,30 @@ describe('MetaMask', function () {
try {
networkSelector = await findElement(driver, By.css('#network_component'))
} catch (e) {
- await driver.get(extensionUri)
+ await loadExtension(driver, extensionUri)
+ await delay(largeDelayMs * 2)
+ networkSelector = await findElement(driver, By.css('#network_component'))
}
await delay(regularDelayMs)
})
- it('use the local network', async function () {
+ it('uses the local network', async function () {
await networkSelector.click()
await delay(regularDelayMs)
- const localhost = await findElement(driver, By.xpath(`//li[contains(text(), 'Localhost')]`))
+ const networks = await findElements(driver, By.css('.dropdown-menu-item'))
+ const localhost = networks[4]
+ await driver.wait(until.elementTextMatches(localhost, /Localhost/))
await localhost.click()
await delay(regularDelayMs)
})
it('selects the new UI option', async () => {
+ try {
+ const overlay = await findElement(driver, By.css('.full-flex-height'))
+ await driver.wait(until.stalenessOf(overlay))
+ } catch (e) {}
+
const button = await findElement(driver, By.xpath("//p[contains(text(), 'Try Beta Version')]"))
await button.click()
await delay(regularDelayMs)
@@ -176,8 +190,20 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
})
- async function retypeSeedPhrase (words) {
+ async function retypeSeedPhrase (words, wasReloaded) {
try {
+ if (wasReloaded) {
+ const byRevealButton = By.css('.backup-phrase__secret-blocker .backup-phrase__reveal-button')
+ await driver.wait(until.elementLocated(byRevealButton, 10000))
+ const revealSeedPhraseButton = await findElement(driver, byRevealButton, 10000)
+ await revealSeedPhraseButton.click()
+ await delay(regularDelayMs)
+
+ const nextScreen = await findElement(driver, By.css('.backup-phrase button'))
+ await nextScreen.click()
+ await delay(regularDelayMs)
+ }
+
const word0 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[0]}')]`), 10000)
await word0.click()
@@ -237,8 +263,8 @@ describe('MetaMask', function () {
await word11.click()
await delay(tinyDelayMs)
} catch (e) {
- await driver.get(extensionUri)
- await retypeSeedPhrase(words)
+ await loadExtension(driver, extensionUri)
+ await retypeSeedPhrase(words, true)
}
}
@@ -291,7 +317,7 @@ describe('MetaMask', function () {
it('accepts the account password after lock', async () => {
await driver.findElement(By.id('password')).sendKeys('correct horse battery staple')
await driver.findElement(By.id('password')).sendKeys(Key.ENTER)
- await delay(regularDelayMs * 4)
+ await delay(largeDelayMs * 4)
})
})
@@ -312,10 +338,10 @@ describe('MetaMask', function () {
const create = await findElement(driver, By.xpath(`//button[contains(text(), 'Create')]`))
await create.click()
- await delay(regularDelayMs)
+ await delay(largeDelayMs)
})
- it('should correct account name', async () => {
+ it('should display correct account name', async () => {
const accountName = await findElement(driver, By.css('.account-name'))
assert.equal(await accountName.getText(), '2nd account')
await delay(regularDelayMs)
@@ -343,16 +369,18 @@ describe('MetaMask', function () {
await seedTextArea.sendKeys(testSeedPhrase)
await delay(regularDelayMs)
- await driver.findElement(By.id('password-box')).sendKeys('correct horse battery staple')
- await driver.findElement(By.id('password-box-confirm')).sendKeys('correct horse battery staple')
- await driver.findElement(By.css('button:nth-child(2)')).click()
+ const passwordInputs = await driver.findElements(By.css('input'))
+ await delay(regularDelayMs)
+
+ passwordInputs[0].sendKeys('correct horse battery staple')
+ passwordInputs[1].sendKeys('correct horse battery staple')
+ await driver.findElement(By.css('.first-time-flow__button')).click()
await delay(regularDelayMs)
})
it('balance renders', async () => {
const balance = await findElement(driver, By.css('.balance-display .token-amount'))
- const tokenAmount = await balance.getText()
- assert.equal(tokenAmount, '100.000 ETH')
+ await driver.wait(until.elementTextMatches(balance, /100.+ETH/))
await delay(regularDelayMs)
})
})
@@ -368,6 +396,9 @@ describe('MetaMask', function () {
await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
await inputAmount.sendKeys('1')
+ const inputValue = await inputAmount.getAttribute('value')
+ assert.equal(inputValue, '1')
+
// Set the gas limit
const configureGas = await findElement(driver, By.css('.send-v2__gas-fee-display button'))
await configureGas.click()
@@ -389,59 +420,71 @@ describe('MetaMask', function () {
it('confirms the transaction', async function () {
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirmButton.click()
- await delay(regularDelayMs)
+ await delay(largeDelayMs)
})
it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.tx-list-item'))
assert.equal(transactions.length, 1)
- const txValues = await findElement(driver, By.css('.tx-list-value'))
- await driver.wait(until.elementTextMatches(txValues, /1\sETH/), 10000)
+ if (process.env.SELENIUM_BROWSER !== 'firefox') {
+ const txValues = await findElement(driver, By.css('.tx-list-value'))
+ await driver.wait(until.elementTextMatches(txValues, /1\sETH/), 10000)
+ }
})
})
- describe('Send ETH from Faucet', () => {
- it('starts a send transaction inside Faucet', async () => {
- await openNewPage(driver, 'https://faucet.metamask.io')
+ describe('Send ETH from dapp', () => {
+ it('starts a send transaction inside the dapp', async () => {
+ await openNewPage(driver, 'http://127.0.0.1:8080/')
+ await delay(regularDelayMs)
- const [extension, faucet] = await driver.getAllWindowHandles()
- await driver.switchTo().window(faucet)
+ await waitUntilXWindowHandles(driver, 2)
+ let windowHandles = await driver.getAllWindowHandles()
+ const extension = windowHandles[0]
+ const dapp = windowHandles[1]
- const faucetPageTitle = await findElement(driver, By.css('.container-fluid'))
- await driver.wait(until.elementTextMatches(faucetPageTitle, /MetaMask/))
+ await driver.switchTo().window(dapp)
await delay(regularDelayMs)
- const send1eth = await findElement(driver, By.xpath(`//button[contains(text(), '10 ether')]`), 14000)
- await send1eth.click()
+ const send3eth = await findElement(driver, By.xpath(`//button[contains(text(), 'Send')]`), 10000)
+ await send3eth.click()
await delay(regularDelayMs)
- await driver.switchTo().window(extension)
- await driver.get(extensionUri)
+ windowHandles = await driver.getAllWindowHandles()
+ await driver.switchTo().window(windowHandles[2])
await delay(regularDelayMs)
- const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`), 14000)
+ assertElementNotPresent(webdriver, driver, By.xpath(`//li[contains(text(), 'Data')]`))
+
+ const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`), 10000)
await confirmButton.click()
await delay(regularDelayMs)
- await driver.switchTo().window(faucet)
- await delay(regularDelayMs)
- await driver.close()
- await delay(regularDelayMs)
+ await waitUntilXWindowHandles(driver, 2)
await driver.switchTo().window(extension)
await delay(regularDelayMs)
- await driver.get(extensionUri)
- await delay(regularDelayMs)
+ })
+
+ it('finds the transaction in the transactions list', async function () {
+ const transactions = await findElements(driver, By.css('.tx-list-item'))
+ assert.equal(transactions.length, 2)
+
+ const txValues = await findElement(driver, By.css('.tx-list-value'))
+ await driver.wait(until.elementTextMatches(txValues, /3\sETH/), 10000)
})
})
describe('Deploy contract and call contract methods', () => {
let extension
- let contractTestPage
- it('confirms a deploy contract transaction', async () => {
- await openNewPage(driver, 'http://127.0.0.1:8080/');
+ let dapp
+ it('creates a deploy contract transaction', async () => {
+ const windowHandles = await driver.getAllWindowHandles()
+ extension = windowHandles[0]
+ dapp = windowHandles[1]
+ await delay(tinyDelayMs)
- [extension, contractTestPage] = await driver.getAllWindowHandles()
+ await driver.switchTo().window(dapp)
await delay(regularDelayMs)
const deployContractButton = await findElement(driver, By.css('#deployButton'))
@@ -451,10 +494,28 @@ describe('MetaMask', function () {
await driver.switchTo().window(extension)
await delay(regularDelayMs)
- const txListItem = await findElement(driver, By.css('.tx-list-item'))
+ const txListItem = await findElement(driver, By.xpath(`//span[contains(text(), 'Contract Deployment')]`))
await txListItem.click()
await delay(regularDelayMs)
+ })
+
+ it('displays the contract creation data', async () => {
+ const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`))
+ dataTab.click()
+ await delay(regularDelayMs)
+
+ await findElement(driver, By.xpath(`//div[contains(text(), '127.0.0.1')]`))
+
+ const confirmDataDiv = await findElement(driver, By.css('.confirm-page-container-content__data-box'))
+ const confirmDataText = await confirmDataDiv.getText()
+ assert.equal(confirmDataText.match(/0x608060405234801561001057600080fd5b5033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff/))
+
+ const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`))
+ detailsTab.click()
+ await delay(regularDelayMs)
+ })
+ it('confirms a deploy contract transaction', async () => {
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirmButton.click()
await delay(regularDelayMs)
@@ -464,10 +525,11 @@ describe('MetaMask', function () {
const txAccounts = await findElements(driver, By.css('.tx-list-account'))
assert.equal(await txAccounts[0].getText(), 'Contract Deployment')
+ await delay(regularDelayMs)
})
it('calls and confirms a contract method where ETH is sent', async () => {
- await driver.switchTo().window(contractTestPage)
+ await driver.switchTo().window(dapp)
await delay(regularDelayMs)
const depositButton = await findElement(driver, By.css('#depositButton'))
@@ -475,19 +537,21 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
await driver.switchTo().window(extension)
- await delay(regularDelayMs)
+ await delay(largeDelayMs)
- const txListItem = await findElement(driver, By.css('.tx-list-item'))
- await txListItem.click()
+ await findElements(driver, By.css('.tx-list-pending-item-container'))
+ const [txListValue] = await findElements(driver, By.css('.tx-list-value'))
+ await driver.wait(until.elementTextMatches(txListValue, /4\sETH/), 10000)
+ await txListValue.click()
await delay(regularDelayMs)
// Set the gas limit
- const configureGas = await findElement(driver, By.css('.sliders-icon-container'))
+ const configureGas = await findElement(driver, By.css('.confirm-detail-row__header-text--edit'))
await configureGas.click()
await delay(regularDelayMs)
const gasModal = await driver.findElement(By.css('span .modal'))
- await driver.wait(until.elementLocated(By.css('.send-v2__customize-gas__title')))
+ await driver.wait(until.elementLocated(By.css('.customize-gas__title')))
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.customize-gas-input'))
await gasPriceInput.clear()
@@ -509,7 +573,7 @@ describe('MetaMask', function () {
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
const txValues = await findElement(driver, By.css('.tx-list-value'))
- await driver.wait(until.elementTextMatches(txValues, /3\sETH/), 10000)
+ await driver.wait(until.elementTextMatches(txValues, /4\sETH/), 10000)
const txAccounts = await findElements(driver, By.css('.tx-list-account'))
const firstTxAddress = await txAccounts[0].getText()
@@ -517,7 +581,7 @@ describe('MetaMask', function () {
})
it('calls and confirms a contract method where ETH is received', async () => {
- await driver.switchTo().window(contractTestPage)
+ await driver.switchTo().window(dapp)
await delay(regularDelayMs)
const withdrawButton = await findElement(driver, By.css('#withdrawButton'))
@@ -541,38 +605,31 @@ describe('MetaMask', function () {
const txValues = await findElement(driver, By.css('.tx-list-value'))
await driver.wait(until.elementTextMatches(txValues, /0\sETH/), 10000)
- await driver.switchTo().window(contractTestPage)
- await driver.close()
+ await closeAllWindowHandlesExcept(driver, [extension, dapp])
await driver.switchTo().window(extension)
})
it('renders the correct ETH balance', async () => {
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
- await driver.wait(until.elementTextMatches(balance, /^86.*ETH.*$/), 10000)
- const tokenAmount = await balance.getText()
- assert.ok(/^86.*ETH.*$/.test(tokenAmount))
await delay(regularDelayMs)
+ if (process.env.SELENIUM_BROWSER !== 'firefox') {
+ await driver.wait(until.elementTextMatches(balance, /^92.*ETH.*$/), 10000)
+ const tokenAmount = await balance.getText()
+ assert.ok(/^92.*ETH.*$/.test(tokenAmount))
+ await delay(regularDelayMs)
+ }
})
})
- describe('Add a custom token from TokenFactory', () => {
+ describe('Add a custom token from a dapp', () => {
it('creates a new token', async () => {
- openNewPage(driver, 'https://tokenfactory.surge.sh/#/factory')
+ const windowHandles = await driver.getAllWindowHandles()
+ const extension = windowHandles[0]
+ const dapp = windowHandles[1]
+ await delay(regularDelayMs * 2)
- await delay(regularDelayMs * 10)
- const [extension, tokenFactory] = await driver.getAllWindowHandles()
-
- const [
- totalSupply,
- tokenName,
- tokenDecimal,
- tokenSymbol,
- ] = await findElements(driver, By.css('.form-control'))
-
- await totalSupply.sendKeys('100')
- await tokenName.sendKeys('Test')
- await tokenDecimal.sendKeys('0')
- await tokenSymbol.sendKeys('TST')
+ await driver.switchTo().window(dapp)
+ await delay(regularDelayMs)
const createToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Create Token')]`))
await createToken.click()
@@ -586,15 +643,16 @@ describe('MetaMask', function () {
await confirmButton.click()
await delay(regularDelayMs)
- await driver.switchTo().window(tokenFactory)
- await delay(regularDelayMs)
+ await driver.switchTo().window(dapp)
+ await delay(tinyDelayMs)
- const tokenContactAddress = await driver.findElement(By.css('div > div > div:nth-child(2) > span:nth-child(3)'))
- tokenAddress = await tokenContactAddress.getText()
+ const tokenContractAddress = await driver.findElement(By.css('#tokenAddress'))
+ await driver.wait(until.elementTextMatches(tokenContractAddress, /0x/))
+ tokenAddress = await tokenContractAddress.getText()
- await driver.close()
- await driver.switchTo().window(extension)
- await driver.get(extensionUri)
+ await delay(regularDelayMs)
+ await closeAllWindowHandlesExcept(driver, [extension, dapp])
+ await delay(regularDelayMs)
await driver.switchTo().window(extension)
await delay(regularDelayMs)
@@ -653,7 +711,7 @@ describe('MetaMask', function () {
gasModal = await driver.findElement(By.css('span .modal'))
})
- it('customizes gas', async () => {
+ it('opens customizes gas modal', async () => {
await driver.wait(until.elementLocated(By.css('.send-v2__customize-gas__title')))
const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`))
await save.click()
@@ -669,6 +727,24 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
})
+ it('displays the token transfer data', async () => {
+ const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`))
+ dataTab.click()
+ await delay(regularDelayMs)
+
+ const functionType = await findElement(driver, By.css('.confirm-page-container-content__function-type'))
+ const functionTypeText = await functionType.getText()
+ assert.equal(functionTypeText, 'Transfer')
+
+ const confirmDataDiv = await findElement(driver, By.css('.confirm-page-container-content__data-box'))
+ const confirmDataText = await confirmDataDiv.getText()
+ assert.equal(confirmDataText.match(/0xa9059cbb0000000000000000000000002f318c334780961fb129d2a6c30d0763d9a5c97/))
+
+ const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`))
+ detailsTab.click()
+ await delay(regularDelayMs)
+ })
+
it('submits the transaction', async function () {
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirmButton.click()
@@ -694,37 +770,33 @@ describe('MetaMask', function () {
})
})
- describe('Send a custom token from TokenFactory', () => {
+ describe('Send a custom token from dapp', () => {
let gasModal
it('sends an already created token', async () => {
- openNewPage(driver, `https://tokenfactory.surge.sh/#/token/${tokenAddress}`)
-
- const [extension] = await driver.getAllWindowHandles()
+ const windowHandles = await driver.getAllWindowHandles()
+ const extension = windowHandles[0]
+ const dapp = await switchToWindowWithTitle(driver, 'E2E Test Dapp', windowHandles)
+ await closeAllWindowHandlesExcept(driver, [extension, dapp])
+ await delay(regularDelayMs)
- const [
- transferToAddress,
- transferToAmount,
- ] = await findElements(driver, By.css('.form-control'))
+ await driver.switchTo().window(dapp)
+ await delay(tinyDelayMs)
- await transferToAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
- await transferToAmount.sendKeys('26')
+ const transferTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Transfer Tokens')]`))
+ await transferTokens.click()
- const transferAmountButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Transfer Amount')]`))
- await transferAmountButton.click()
- await delay(regularDelayMs)
-
- const [,, popup] = await driver.getAllWindowHandles()
- await driver.switchTo().window(popup)
- await driver.close()
+ await closeAllWindowHandlesExcept(driver, [extension, dapp])
await driver.switchTo().window(extension)
- await delay(regularDelayMs)
+ await delay(largeDelayMs)
- const [txListItem] = await findElements(driver, By.css('.tx-list-item'))
- await txListItem.click()
+ await findElements(driver, By.css('.tx-list-pending-item-container'))
+ const [txListValue] = await findElements(driver, By.css('.tx-list-value'))
+ await driver.wait(until.elementTextMatches(txListValue, /7\sTST/), 10000)
+ await txListValue.click()
await delay(regularDelayMs)
// Set the gas limit
- const configureGas = await driver.wait(until.elementLocated(By.css('.send-v2__gas-fee-display button')))
+ const configureGas = await driver.wait(until.elementLocated(By.css('.confirm-detail-row__header-text--edit')), 10000)
await configureGas.click()
await delay(regularDelayMs)
@@ -732,7 +804,7 @@ describe('MetaMask', function () {
})
it('customizes gas', async () => {
- await driver.wait(until.elementLocated(By.css('.send-v2__customize-gas__title')))
+ await driver.wait(until.elementLocated(By.css('.customize-gas__title')))
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.customize-gas-input'))
await gasPriceInput.clear()
@@ -751,12 +823,12 @@ describe('MetaMask', function () {
await gasLimitInput.sendKeys(Key.BACK_SPACE)
}
- const save = await findElement(driver, By.css('.send-v2__customize-gas__save'))
+ const save = await findElement(driver, By.css('.customize-gas__save'))
await save.click()
await driver.wait(until.stalenessOf(gasModal))
- const gasFeeInput = await findElement(driver, By.css('.currency-display__input'))
- assert.equal(await gasFeeInput.getAttribute('value'), 0.0006)
+ const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__eth'))
+ assert.equal(await gasFeeInputs[0].getText(), '♦ 0.0006')
})
it('submits the transaction', async function () {
@@ -770,7 +842,7 @@ describe('MetaMask', function () {
assert.equal(transactions.length, 2)
const txValues = await findElements(driver, By.css('.tx-list-value'))
- await driver.wait(until.elementTextMatches(txValues[0], /26\sTST/))
+ await driver.wait(until.elementTextMatches(txValues[0], /7\sTST/))
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
@@ -784,8 +856,107 @@ describe('MetaMask', function () {
// or possibly until we use latest version of firefox in the tests
if (process.env.SELENIUM_BROWSER !== 'firefox') {
const tokenBalanceAmount = await findElement(driver, By.css('.token-balance__amount'))
- assert.equal(await tokenBalanceAmount.getText(), '24')
+ assert.equal(await tokenBalanceAmount.getText(), '43')
+ }
+ })
+ })
+
+ describe('Approves a custom token from dapp', () => {
+ let gasModal
+ it('approves an already created token', async () => {
+ const windowHandles = await driver.getAllWindowHandles()
+ const extension = windowHandles[0]
+ const dapp = await switchToWindowWithTitle(driver, 'E2E Test Dapp', windowHandles)
+ await closeAllWindowHandlesExcept(driver, [extension, dapp])
+ await delay(regularDelayMs)
+
+ await driver.switchTo().window(dapp)
+ await delay(tinyDelayMs)
+
+ const transferTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Approve Tokens')]`))
+ await transferTokens.click()
+
+ await closeAllWindowHandlesExcept(driver, extension)
+ await driver.switchTo().window(extension)
+ await delay(regularDelayMs)
+
+ const [txListItem] = await findElements(driver, By.css('.tx-list-item'))
+ const [txListValue] = await findElements(driver, By.css('.tx-list-value'))
+ await driver.wait(until.elementTextMatches(txListValue, /0\sETH/))
+ await txListItem.click()
+ await delay(regularDelayMs)
+ })
+
+ it('displays the token approval data', async () => {
+ const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`))
+ dataTab.click()
+ await delay(regularDelayMs)
+
+ const functionType = await findElement(driver, By.css('.confirm-page-container-content__function-type'))
+ const functionTypeText = await functionType.getText()
+ assert.equal(functionTypeText, 'Approve')
+
+ const confirmDataDiv = await findElement(driver, By.css('.confirm-page-container-content__data-box'))
+ const confirmDataText = await confirmDataDiv.getText()
+ assert.equal(confirmDataText.match(/0x095ea7b30000000000000000000000002f318c334780961fb129d2a6c30d0763d9a5c97/))
+
+ const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`))
+ detailsTab.click()
+ await delay(regularDelayMs)
+
+ const approvalWarning = await findElement(driver, By.css('.confirm-page-container-warning__warning'))
+ const approvalWarningText = await approvalWarning.getText()
+ assert(approvalWarningText.match(/By approving this/))
+ await delay(regularDelayMs)
+ })
+
+ it('opens the gas edit modal', async () => {
+ const configureGas = await driver.wait(until.elementLocated(By.css('.confirm-detail-row__header-text--edit')))
+ await configureGas.click()
+ await delay(regularDelayMs)
+
+ gasModal = await driver.findElement(By.css('span .modal'))
+ })
+
+ it('customizes gas', async () => {
+ await driver.wait(until.elementLocated(By.css('.customize-gas__title')))
+
+ const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.customize-gas-input'))
+ await gasPriceInput.clear()
+ await delay(tinyDelayMs)
+ await gasPriceInput.sendKeys('10')
+ await delay(tinyDelayMs)
+ await gasLimitInput.clear()
+ await delay(tinyDelayMs)
+ await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'a'))
+ await gasLimitInput.sendKeys('60000')
+ await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'e'))
+
+ // Needed for different behaviour of input in different versions of firefox
+ const gasLimitInputValue = await gasLimitInput.getAttribute('value')
+ if (gasLimitInputValue === '600001') {
+ await gasLimitInput.sendKeys(Key.BACK_SPACE)
}
+
+ const save = await findElement(driver, By.css('.customize-gas__save'))
+ await save.click()
+ await driver.wait(until.stalenessOf(gasModal))
+
+ const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__eth'))
+ assert.equal(await gasFeeInputs[0].getText(), '♦ 0.0006')
+ })
+
+ it('submits the transaction', async function () {
+ const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
+ await confirmButton.click()
+ await delay(regularDelayMs)
+ })
+
+ it('finds the transaction in the transactions list', async function () {
+ const txValues = await findElements(driver, By.css('.tx-list-value'))
+ await driver.wait(until.elementTextMatches(txValues[0], /0\sETH/))
+ const txStatuses = await findElements(driver, By.css('.tx-list-status'))
+ await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
})
})
diff --git a/test/e2e/metamask.spec.js b/test/e2e/metamask.spec.js
index 72086326d..d26786ca6 100644
--- a/test/e2e/metamask.spec.js
+++ b/test/e2e/metamask.spec.js
@@ -219,7 +219,7 @@ describe('Metamask popup page', function () {
submitButton.click()
- await delay(500)
+ await delay(1500)
})
it('finds the transaction in the transactions list', async function () {
diff --git a/test/integration/lib/confirm-sig-requests.js b/test/integration/lib/confirm-sig-requests.js
index 5613c0dcb..dcc25c493 100644
--- a/test/integration/lib/confirm-sig-requests.js
+++ b/test/integration/lib/confirm-sig-requests.js
@@ -1,5 +1,6 @@
const reactTriggerChange = require('react-trigger-change')
const {
+ timeout,
queryAsync,
} = require('../../lib/util')
@@ -18,14 +19,14 @@ async function runConfirmSigRequestsTest (assert, done) {
selectState.val('confirm sig requests')
reactTriggerChange(selectState[0])
- // await timeout(1000000)
-
const pendingRequestItem = $.find('.tx-list-item.tx-list-pending-item-container.tx-list-clickable')
if (pendingRequestItem[0]) {
pendingRequestItem[0].click()
}
+ await timeout(1000)
+
let confirmSigHeadline = await queryAsync($, '.request-signature__headline')
assert.equal(confirmSigHeadline[0].textContent, 'Your signature is being requested')
@@ -37,7 +38,7 @@ async function runConfirmSigRequestsTest (assert, done) {
let confirmSigSignButton = await queryAsync($, 'button.btn-primary.btn--large')
confirmSigSignButton[0].click()
-
+ await timeout(1000)
confirmSigHeadline = await queryAsync($, '.request-signature__headline')
assert.equal(confirmSigHeadline[0].textContent, 'Your signature is being requested')
@@ -46,7 +47,7 @@ async function runConfirmSigRequestsTest (assert, done) {
confirmSigSignButton = await queryAsync($, 'button.btn-primary.btn--large')
confirmSigSignButton[0].click()
-
+ await timeout(1000)
confirmSigHeadline = await queryAsync($, '.request-signature__headline')
assert.equal(confirmSigHeadline[0].textContent, 'Your signature is being requested')
@@ -57,6 +58,5 @@ async function runConfirmSigRequestsTest (assert, done) {
confirmSigSignButton = await queryAsync($, 'button.btn-primary.btn--large')
confirmSigSignButton[0].click()
- const txView = await queryAsync($, '.tx-view')
- assert.ok(txView[0], 'Should return to the account details screen after confirming')
+ await timeout(2000)
}
diff --git a/test/integration/lib/currency-localization.js b/test/integration/lib/currency-localization.js
index 3ad1a23e5..d42b7495d 100644
--- a/test/integration/lib/currency-localization.js
+++ b/test/integration/lib/currency-localization.js
@@ -19,6 +19,7 @@ async function runCurrencyLocalizationTest (assert, done) {
console.log('*** start runCurrencyLocalizationTest')
const selectState = await queryAsync($, 'select')
selectState.val('currency localization')
+ await timeout(1000)
reactTriggerChange(selectState[0])
await timeout(1000)
const txView = await queryAsync($, '.tx-view')
diff --git a/test/integration/lib/send-new-ui.js b/test/integration/lib/send-new-ui.js
index 241358135..406863ca6 100644
--- a/test/integration/lib/send-new-ui.js
+++ b/test/integration/lib/send-new-ui.js
@@ -112,19 +112,8 @@ async function runSendFlowTest (assert, done) {
errorMessage = $('.send-v2__error')
assert.equal(errorMessage.length, 0, 'send should stop rendering amount error message after amount is corrected')
- const sendGasField = await queryAsync($, '.send-v2__gas-fee-display')
- assert.equal(
- sendGasField.find('.currency-display__input-wrapper > input').val(),
- '0.000021',
- 'send gas field should show estimated gas total'
- )
- assert.equal(
- sendGasField.find('.currency-display__converted-value')[0].textContent,
- '$0.03 USD',
- 'send gas field should show estimated gas total converted to USD'
- )
-
await customizeGas(assert, 0, 21000, '0', '$0.00 USD')
+ await customizeGas(assert, 1, 21000, '0.000021', '$0.03 USD')
await customizeGas(assert, 500, 60000, '0.03', '$36.03 USD')
const sendButton = await queryAsync($, 'button.btn-primary.btn--large.page-container__footer-button')
@@ -136,18 +125,18 @@ async function runSendFlowTest (assert, done) {
reactTriggerChange(selectState[0])
const confirmFromName = (await queryAsync($, '.sender-to-recipient__sender-name')).first()
- assert.equal(confirmFromName[0].textContent, 'Send Account 2', 'confirm screen should show correct from name')
+ assert.equal(confirmFromName[0].textContent, 'Send Account 4', 'confirm screen should show correct from name')
const confirmToName = (await queryAsync($, '.sender-to-recipient__recipient-name')).last()
assert.equal(confirmToName[0].textContent, 'Send Account 3', 'confirm screen should show correct to name')
- const confirmScreenRows = await queryAsync($, '.confirm-screen-rows')
- const confirmScreenGas = confirmScreenRows.find('.currency-display__converted-value')[0]
- assert.equal(confirmScreenGas.textContent, '$3.60 USD', 'confirm screen should show correct gas')
- const confirmScreenTotal = confirmScreenRows.find('.confirm-screen-row-info')[2]
- assert.equal(confirmScreenTotal.textContent, '$2,405.36 USD', 'confirm screen should show correct total')
+ const confirmScreenRowFiats = await queryAsync($, '.confirm-detail-row__fiat')
+ const confirmScreenGas = confirmScreenRowFiats[0]
+ assert.equal(confirmScreenGas.textContent, '$3.60', 'confirm screen should show correct gas')
+ const confirmScreenTotal = confirmScreenRowFiats[1]
+ assert.equal(confirmScreenTotal.textContent, '$2,405.36', 'confirm screen should show correct total')
- const confirmScreenBackButton = await queryAsync($, '.page-container__back-button')
+ const confirmScreenBackButton = await queryAsync($, '.confirm-page-container-header__back-button')
confirmScreenBackButton[0].click()
const sendFromFieldItemInEdit = await queryAsync($, '.account-list-item')
diff --git a/test/integration/lib/tx-list-items.js b/test/integration/lib/tx-list-items.js
index 6b67b1d2e..b7aca44d5 100644
--- a/test/integration/lib/tx-list-items.js
+++ b/test/integration/lib/tx-list-items.js
@@ -31,8 +31,8 @@ async function runTxListItemsTest (assert, done) {
assert.equal($(unapprovedTx).hasClass('tx-list-pending-item-container'), true, 'unapprovedTx has the correct class')
const retryTx = txListItems[1]
- const retryTxLink = await findAsync($(retryTx), '.tx-list-item-retry-link')
- assert.equal(retryTxLink[0].textContent, 'Increase the gas price on your transaction', 'retryTx has expected link')
+ const retryTxLink = await findAsync($(retryTx), '.tx-list-item-retry-container span')
+ assert.equal(retryTxLink[0].textContent, 'Taking too long? Increase the gas price on your transaction', 'retryTx has expected link')
const approvedTx = txListItems[2]
const approvedTxRenderedStatus = await findAsync($(approvedTx), '.tx-list-status')
diff --git a/test/lib/migrations/002.json b/test/lib/migrations/002.json
index 9ad3d4cfe..15820ded5 100644
--- a/test/lib/migrations/002.json
+++ b/test/lib/migrations/002.json
@@ -1 +1 @@
-{"meta":{"version":20},"data":{"config":{},"NetworkController":{"provider":{"type":"mainnet","rpcTarget":"https://mainnet.infura.io/metamask"},"network":"1"},"firstTimeInfo":{"version":"3.12.1","date":1517351427287},"NoticeController":{"noticesList":[{"read":false,"date":"Thu Feb 09 2017","title":"Terms of Use","body":"# Terms of Use #\n\n**THIS AGREEMENT IS SUBJECT TO BINDING ARBITRATION AND A WAIVER OF CLASS ACTION RIGHTS AS DETAILED IN SECTION 13. PLEASE READ THE AGREEMENT CAREFULLY.**\n\n_Our Terms of Use have been updated as of September 5, 2016_\n\n## 1. Acceptance of Terms ##\n\nMetaMask provides a platform for managing Ethereum (or \"ETH\") accounts, and allowing ordinary websites to interact with the Ethereum blockchain, while keeping the user in control over what transactions they approve, through our website located at[ ](http://metamask.io)[https://metamask.io/](https://metamask.io/) and browser plugin (the \"Site\") — which includes text, images, audio, code and other materials (collectively, the “Content”) and all of the features, and services provided. The Site, and any other features, tools, materials, or other services offered from time to time by MetaMask are referred to here as the “Service.” Please read these Terms of Use (the “Terms” or “Terms of Use”) carefully before using the Service. By using or otherwise accessing the Services, or clicking to accept or agree to these Terms where that option is made available, you (1) accept and agree to these Terms (2) consent to the collection, use, disclosure and other handling of information as described in our Privacy Policy and (3) any additional terms, rules and conditions of participation issued by MetaMask from time to time. If you do not agree to the Terms, then you may not access or use the Content or Services.\n\n## 2. Modification of Terms of Use ##\n\nExcept for Section 13, providing for binding arbitration and waiver of class action rights, MetaMask reserves the right, at its sole discretion, to modify or replace the Terms of Use at any time. The most current version of these Terms will be posted on our Site. You shall be responsible for reviewing and becoming familiar with any such modifications. Use of the Services by you after any modification to the Terms constitutes your acceptance of the Terms of Use as modified.\n\n\n\n## 3. Eligibility ##\n\nYou hereby represent and warrant that you are fully able and competent to enter into the terms, conditions, obligations, affirmations, representations and warranties set forth in these Terms and to abide by and comply with these Terms.\n\nMetaMask is a global platform and by accessing the Content or Services, you are representing and warranting that, you are of the legal age of majority in your jurisdiction as is required to access such Services and Content and enter into arrangements as provided by the Service. You further represent that you are otherwise legally permitted to use the service in your jurisdiction including owning cryptographic tokens of value, and interacting with the Services or Content in any way. You further represent you are responsible for ensuring compliance with the laws of your jurisdiction and acknowledge that MetaMask is not liable for your compliance with such laws.\n\n## 4 Account Password and Security ##\n\nWhen setting up an account within MetaMask, you will be responsible for keeping your own account secrets, which may be a twelve-word seed phrase, an account file, or other locally stored secret information. MetaMask encrypts this information locally with a password you provide, that we never send to our servers. You agree to (a) never use the same password for MetaMask that you have ever used outside of this service; (b) keep your secret information and password confidential and do not share them with anyone else; (c) immediately notify MetaMask of any unauthorized use of your account or breach of security. MetaMask cannot and will not be liable for any loss or damage arising from your failure to comply with this section.\n\n## 5. Representations, Warranties, and Risks ##\n\n### 5.1. Warranty Disclaimer ###\n\nYou expressly understand and agree that your use of the Service is at your sole risk. The Service (including the Service and the Content) are provided on an \"AS IS\" and \"as available\" basis, without warranties of any kind, either express or implied, including, without limitation, implied warranties of merchantability, fitness for a particular purpose or non-infringement. You acknowledge that MetaMask has no control over, and no duty to take any action regarding: which users gain access to or use the Service; what effects the Content may have on you; how you may interpret or use the Content; or what actions you may take as a result of having been exposed to the Content. You release MetaMask from all liability for you having acquired or not acquired Content through the Service. MetaMask makes no representations concerning any Content contained in or accessed through the Service, and MetaMask will not be responsible or liable for the accuracy, copyright compliance, legality or decency of material contained in or accessed through the Service.\n\n### 5.2 Sophistication and Risk of Cryptographic Systems ###\n\nBy utilizing the Service or interacting with the Content or platform in any way, you represent that you understand the inherent risks associated with cryptographic systems; and warrant that you have an understanding of the usage and intricacies of native cryptographic tokens, like Ether (ETH) and Bitcoin (BTC), smart contract based tokens such as those that follow the Ethereum Token Standard (https://github.com/ethereum/EIPs/issues/20), and blockchain-based software systems.\n\n### 5.3 Risk of Regulatory Actions in One or More Jurisdictions ###\n\nMetaMask and ETH could be impacted by one or more regulatory inquiries or regulatory action, which could impede or limit the ability of MetaMask to continue to develop, or which could impede or limit your ability to access or use the Service or Ethereum blockchain.\n\n### 5.4 Risk of Weaknesses or Exploits in the Field of Cryptography ###\n\nYou acknowledge and understand that Cryptography is a progressing field. Advances in code cracking or technical advances such as the development of quantum computers may present risks to cryptocurrencies and Services of Content, which could result in the theft or loss of your cryptographic tokens or property. To the extent possible, MetaMask intends to update the protocol underlying Services to account for any advances in cryptography and to incorporate additional security measures, but does not guarantee or otherwise represent full security of the system. By using the Service or accessing Content, you acknowledge these inherent risks.\n\n### 5.5 Volatility of Crypto Currencies ###\n\nYou understand that Ethereum and other blockchain technologies and associated currencies or tokens are highly volatile due to many factors including but not limited to adoption, speculation, technology and security risks. You also acknowledge that the cost of transacting on such technologies is variable and may increase at any time causing impact to any activities taking place on the Ethereum blockchain. You acknowledge these risks and represent that MetaMask cannot be held liable for such fluctuations or increased costs.\n\n### 5.6 Application Security ###\n\nYou acknowledge that Ethereum applications are code subject to flaws and acknowledge that you are solely responsible for evaluating any code provided by the Services or Content and the trustworthiness of any third-party websites, products, smart-contracts, or Content you access or use through the Service. You further expressly acknowledge and represent that Ethereum applications can be written maliciously or negligently, that MetaMask cannot be held liable for your interaction with such applications and that such applications may cause the loss of property or even identity. This warning and others later provided by MetaMask in no way evidence or represent an on-going duty to alert you to all of the potential risks of utilizing the Service or Content.\n\n## 6. Indemnity ##\n\nYou agree to release and to indemnify, defend and hold harmless MetaMask and its parents, subsidiaries, affiliates and agencies, as well as the officers, directors, employees, shareholders and representatives of any of the foregoing entities, from and against any and all losses, liabilities, expenses, damages, costs (including attorneys’ fees and court costs) claims or actions of any kind whatsoever arising or resulting from your use of the Service, your violation of these Terms of Use, and any of your acts or omissions that implicate publicity rights, defamation or invasion of privacy. MetaMask reserves the right, at its own expense, to assume exclusive defense and control of any matter otherwise subject to indemnification by you and, in such case, you agree to cooperate with MetaMask in the defense of such matter.\n\n## 7. Limitation on liability ##\n\nYOU ACKNOWLEDGE AND AGREE THAT YOU ASSUME FULL RESPONSIBILITY FOR YOUR USE OF THE SITE AND SERVICE. YOU ACKNOWLEDGE AND AGREE THAT ANY INFORMATION YOU SEND OR RECEIVE DURING YOUR USE OF THE SITE AND SERVICE MAY NOT BE SECURE AND MAY BE INTERCEPTED OR LATER ACQUIRED BY UNAUTHORIZED PARTIES. YOU ACKNOWLEDGE AND AGREE THAT YOUR USE OF THE SITE AND SERVICE IS AT YOUR OWN RISK. RECOGNIZING SUCH, YOU UNDERSTAND AND AGREE THAT, TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, NEITHER METAMASK NOR ITS SUPPLIERS OR LICENSORS WILL BE LIABLE TO YOU FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY OR OTHER DAMAGES OF ANY KIND, INCLUDING WITHOUT LIMITATION DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA OR OTHER TANGIBLE OR INTANGIBLE LOSSES OR ANY OTHER DAMAGES BASED ON CONTRACT, TORT, STRICT LIABILITY OR ANY OTHER THEORY (EVEN IF METAMASK HAD BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES), RESULTING FROM THE SITE OR SERVICE; THE USE OR THE INABILITY TO USE THE SITE OR SERVICE; UNAUTHORIZED ACCESS TO OR ALTERATION OF YOUR TRANSMISSIONS OR DATA; STATEMENTS OR CONDUCT OF ANY THIRD PARTY ON THE SITE OR SERVICE; ANY ACTIONS WE TAKE OR FAIL TO TAKE AS A RESULT OF COMMUNICATIONS YOU SEND TO US; HUMAN ERRORS; TECHNICAL MALFUNCTIONS; FAILURES, INCLUDING PUBLIC UTILITY OR TELEPHONE OUTAGES; OMISSIONS, INTERRUPTIONS, LATENCY, DELETIONS OR DEFECTS OF ANY DEVICE OR NETWORK, PROVIDERS, OR SOFTWARE (INCLUDING, BUT NOT LIMITED TO, THOSE THAT DO NOT PERMIT PARTICIPATION IN THE SERVICE); ANY INJURY OR DAMAGE TO COMPUTER EQUIPMENT; INABILITY TO FULLY ACCESS THE SITE OR SERVICE OR ANY OTHER WEBSITE; THEFT, TAMPERING, DESTRUCTION, OR UNAUTHORIZED ACCESS TO, IMAGES OR OTHER CONTENT OF ANY KIND; DATA THAT IS PROCESSED LATE OR INCORRECTLY OR IS INCOMPLETE OR LOST; TYPOGRAPHICAL, PRINTING OR OTHER ERRORS, OR ANY COMBINATION THEREOF; OR ANY OTHER MATTER RELATING TO THE SITE OR SERVICE.\n\nSOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF CERTAIN WARRANTIES OR THE LIMITATION OR EXCLUSION OF LIABILITY FOR INCIDENTAL OR CONSEQUENTIAL DAMAGES. ACCORDINGLY, SOME OF THE ABOVE LIMITATIONS MAY NOT APPLY TO YOU.\n\n## 8. Our Proprietary Rights ##\n\nAll title, ownership and intellectual property rights in and to the Service are owned by MetaMask or its licensors. You acknowledge and agree that the Service contains proprietary and confidential information that is protected by applicable intellectual property and other laws. Except as expressly authorized by MetaMask, you agree not to copy, modify, rent, lease, loan, sell, distribute, perform, display or create derivative works based on the Service, in whole or in part. MetaMask issues a license for MetaMask, found [here](https://github.com/MetaMask/metamask-plugin/blob/master/LICENSE). For information on other licenses utilized in the development of MetaMask, please see our attribution page at: [https://metamask.io/attributions.html](https://metamask.io/attributions.html)\n\n## 9. Links ##\n\nThe Service provides, or third parties may provide, links to other World Wide Web or accessible sites, applications or resources. Because MetaMask has no control over such sites, applications and resources, you acknowledge and agree that MetaMask is not responsible for the availability of such external sites, applications or resources, and does not endorse and is not responsible or liable for any content, advertising, products or other materials on or available from such sites or resources. You further acknowledge and agree that MetaMask shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such content, goods or services available on or through any such site or resource.\n\n## 10. Termination and Suspension ##\n\nMetaMask may terminate or suspend all or part of the Service and your MetaMask access immediately, without prior notice or liability, if you breach any of the terms or conditions of the Terms. Upon termination of your access, your right to use the Service will immediately cease.\n\nThe following provisions of the Terms survive any termination of these Terms: INDEMNITY; WARRANTY DISCLAIMERS; LIMITATION ON LIABILITY; OUR PROPRIETARY RIGHTS; LINKS; TERMINATION; NO THIRD PARTY BENEFICIARIES; BINDING ARBITRATION AND CLASS ACTION WAIVER; GENERAL INFORMATION.\n\n## 11. No Third Party Beneficiaries ##\n\nYou agree that, except as otherwise expressly provided in these Terms, there shall be no third party beneficiaries to the Terms.\n\n## 12. Notice and Procedure For Making Claims of Copyright Infringement ##\n\nIf you believe that your copyright or the copyright of a person on whose behalf you are authorized to act has been infringed, please provide MetaMask’s Copyright Agent a written Notice containing the following information:\n\n· an electronic or physical signature of the person authorized to act on behalf of the owner of the copyright or other intellectual property interest;\n\n· a description of the copyrighted work or other intellectual property that you claim has been infringed;\n\n· a description of where the material that you claim is infringing is located on the Service;\n\n· your address, telephone number, and email address;\n\n· a statement by you that you have a good faith belief that the disputed use is not authorized by the copyright owner, its agent, or the law;\n\n· a statement by you, made under penalty of perjury, that the above information in your Notice is accurate and that you are the copyright or intellectual property owner or authorized to act on the copyright or intellectual property owner's behalf.\n\nMetaMask’s Copyright Agent can be reached at:\n\nEmail: copyright [at] metamask [dot] io\n\nMail:\n\nAttention:\n\nMetaMask Copyright ℅ ConsenSys\n\n49 Bogart Street\n\nBrooklyn, NY 11206\n\n## 13. Binding Arbitration and Class Action Waiver ##\n\nPLEASE READ THIS SECTION CAREFULLY – IT MAY SIGNIFICANTLY AFFECT YOUR LEGAL RIGHTS, INCLUDING YOUR RIGHT TO FILE A LAWSUIT IN COURT\n\n### 13.1 Initial Dispute Resolution ###\n\nThe parties shall use their best efforts to engage directly to settle any dispute, claim, question, or disagreement and engage in good faith negotiations which shall be a condition to either party initiating a lawsuit or arbitration.\n\n### 13.2 Binding Arbitration ###\n\nIf the parties do not reach an agreed upon solution within a period of 30 days from the time informal dispute resolution under the Initial Dispute Resolution provision begins, then either party may initiate binding arbitration as the sole means to resolve claims, subject to the terms set forth below. Specifically, all claims arising out of or relating to these Terms (including their formation, performance and breach), the parties’ relationship with each other and/or your use of the Service shall be finally settled by binding arbitration administered by the American Arbitration Association in accordance with the provisions of its Commercial Arbitration Rules and the supplementary procedures for consumer related disputes of the American Arbitration Association (the \"AAA\"), excluding any rules or procedures governing or permitting class actions.\n\nThe arbitrator, and not any federal, state or local court or agency, shall have exclusive authority to resolve all disputes arising out of or relating to the interpretation, applicability, enforceability or formation of these Terms, including, but not limited to any claim that all or any part of these Terms are void or voidable, or whether a claim is subject to arbitration. The arbitrator shall be empowered to grant whatever relief would be available in a court under law or in equity. The arbitrator’s award shall be written, and binding on the parties and may be entered as a judgment in any court of competent jurisdiction.\n\nThe parties understand that, absent this mandatory provision, they would have the right to sue in court and have a jury trial. They further understand that, in some instances, the costs of arbitration could exceed the costs of litigation and the right to discovery may be more limited in arbitration than in court.\n\n### 13.3 Location ###\n\nBinding arbitration shall take place in New York. You agree to submit to the personal jurisdiction of any federal or state court in New York County, New York, in order to compel arbitration, to stay proceedings pending arbitration, or to confirm, modify, vacate or enter judgment on the award entered by the arbitrator.\n\n### 13.4 Class Action Waiver ###\n\nThe parties further agree that any arbitration shall be conducted in their individual capacities only and not as a class action or other representative action, and the parties expressly waive their right to file a class action or seek relief on a class basis. YOU AND METAMASK AGREE THAT EACH MAY BRING CLAIMS AGAINST THE OTHER ONLY IN YOUR OR ITS INDIVIDUAL CAPACITY, AND NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED CLASS OR REPRESENTATIVE PROCEEDING. If any court or arbitrator determines that the class action waiver set forth in this paragraph is void or unenforceable for any reason or that an arbitration can proceed on a class basis, then the arbitration provision set forth above shall be deemed null and void in its entirety and the parties shall be deemed to have not agreed to arbitrate disputes.\n\n### 13.5 Exception - Litigation of Intellectual Property and Small Claims Court Claims ###\n\nNotwithstanding the parties' decision to resolve all disputes through arbitration, either party may bring an action in state or federal court to protect its intellectual property rights (\"intellectual property rights\" means patents, copyrights, moral rights, trademarks, and trade secrets, but not privacy or publicity rights). Either party may also seek relief in a small claims court for disputes or claims within the scope of that court’s jurisdiction.\n\n### 13.6 30-Day Right to Opt Out ###\n\nYou have the right to opt-out and not be bound by the arbitration and class action waiver provisions set forth above by sending written notice of your decision to opt-out to the following address: MetaMask ℅ ConsenSys, 49 Bogart Street, Brooklyn NY 11206 and via email at legal-opt@metamask.io. The notice must be sent within 30 days of September 6, 2016 or your first use of the Service, whichever is later, otherwise you shall be bound to arbitrate disputes in accordance with the terms of those paragraphs. If you opt-out of these arbitration provisions, MetaMask also will not be bound by them.\n\n### 13.7 Changes to This Section ###\n\nMetaMask will provide 60-days’ notice of any changes to this section. Changes will become effective on the 60th day, and will apply prospectively only to any claims arising after the 60th day.\n\nFor any dispute not subject to arbitration you and MetaMask agree to submit to the personal and exclusive jurisdiction of and venue in the federal and state courts located in New York, New York. You further agree to accept service of process by mail, and hereby waive any and all jurisdictional and venue defenses otherwise available.\n\nThe Terms and the relationship between you and MetaMask shall be governed by the laws of the State of New York without regard to conflict of law provisions.\n\n## 14. General Information ##\n\n### 14.1 Entire Agreement ###\n\nThese Terms (and any additional terms, rules and conditions of participation that MetaMask may post on the Service) constitute the entire agreement between you and MetaMask with respect to the Service and supersedes any prior agreements, oral or written, between you and MetaMask. In the event of a conflict between these Terms and the additional terms, rules and conditions of participation, the latter will prevail over the Terms to the extent of the conflict.\n\n### 14.2 Waiver and Severability of Terms ###\n\nThe failure of MetaMask to exercise or enforce any right or provision of the Terms shall not constitute a waiver of such right or provision. If any provision of the Terms is found by an arbitrator or court of competent jurisdiction to be invalid, the parties nevertheless agree that the arbitrator or court should endeavor to give effect to the parties' intentions as reflected in the provision, and the other provisions of the Terms remain in full force and effect.\n\n### 14.3 Statute of Limitations ###\n\nYou agree that regardless of any statute or law to the contrary, any claim or cause of action arising out of or related to the use of the Service or the Terms must be filed within one (1) year after such claim or cause of action arose or be forever barred.\n\n### 14.4 Section Titles ###\n\nThe section titles in the Terms are for convenience only and have no legal or contractual effect.\n\n### 14.5 Communications ###\n\nUsers with questions, complaints or claims with respect to the Service may contact us using the relevant contact information set forth above and at communications@metamask.io.\n\n## 15 Related Links ##\n\n**[Terms of Use](https://metamask.io/terms.html)**\n\n**[Privacy](https://metamask.io/privacy.html)**\n\n**[Attributions](https://metamask.io/attributions.html)**\n\n","id":0},{"read":false,"date":"Mon May 08 2017","title":"Privacy Notice","body":"MetaMask is beta software. \n\nWhen you log in to MetaMask, your current account is visible to every new site you visit.\n\nFor your privacy, for now, please sign out of MetaMask when you're done using a site.\n\nAlso, by default, you will be signed in to a test network. To use real Ether, you must connect to the main network manually in the top left network menu.\n\n","id":2}]},"BlacklistController":{"phishing":{"version":2,"tolerance":2,"fuzzylist":["metamask.io","myetherwallet.com","cryptokitties.co"],"whitelist":["metahash.io","metahash.net","metahash.org","cryptotitties.com","cryptocities.net","cryptoshitties.co","cryptotitties.fun","cryptokitties.forsale","cryptokitties.care","metamate.cc","metamesh.tech","ico.nexus.social","metamesh.org","metatask.io","metmask.com","metarasa.com","metapack.com","metacase.com","metafas.nl","metamako.com","metamast.com","metamax.ru","metadesk.io","metadisk.com","metallsk.ru","metamag.fr","metamaks.ru","metamap.ru","metamaps.cc","metamats.com","metamax.by","metamax.com","metamax.io","metamuse.net","metarank.com","metaxas.com","megamas2.ru","metamask.io","myetherwallet.com","myethlerwallet.com","ethereum.org","myetheroll.com","myetherapi.com","ledgerwallet.com","databrokerdao.com","etherscan.io","etherid.org","ether.cards","etheroll.com","ethnews.com","ethex.market","ethereumdev.io","ethereumdev.kr","dether.io","ethermine.org","slaask.com","etherbtc.io","ethereal.capital","etherisc.com","m.famalk.net","etherecho.com","ethereum.os.tc","theethereum.wiki","metajack.im","etherhub.io","ethereum.network","ethereum.link","ethereum.com","prethereum.org","ethereumj.io","etheraus.com","ethereum.dev","1ethereum.ru","ethereum.nz","nethereum.com","metabank.com","metamas.com","aventus.io","metabase.com","etherdelta.com","metabase.one","cryptokitties.co"],"blacklist":["myetherwallet.uk.com","kodakone.cc","nyeihitervvallet.com","xn--myeterwalet-cm8eoi.com","nucleus.foundation","beetoken-ico.com","data-token.com","tron-labs.com","ocoin.tech","aionfoundation.com","ico-telegram.org","nyeihitervvallat.com","telegramcoin.us","daddi.cloud","daditoken.com","blockarray.org","dadi-cloud.net","wanchainfunding.org","ico-telegram.io","iconfoundation.site","iost.co","beetoken-ico.eu","cindicator.network","wanchainetwork.org","wamchain.org","wanchainltd.org","wanchainalliance.org","nucleus-vision.net","ledgerwallet.by","nucleuss.vision","myenhterswailct.com","cobin-hood.com","wanchainfoundation.org","xn--polniex-ex4c.com","xn--polniex-s1a.com","xn--polonex-ieb.com","xn--polonex-sza.com","xn--polonex-zw4c.com","xn--polonix-ws4c.com","xn--polonix-y8a.com","xn--pooniex-ojb.com","gramico.info","dimnsions.network","www-gemini.com","login-kucoin.net","venchain.foundation","grampreico.com","tgram.cc","ton-gramico.com","wwwpaywithink.com","coniomi.com","paywithnk.com","paywithlnk.com","iluminatto.com.br","pundix.eu","xn--bttrx-esay.com","xn--bttrex-w8a.com","xn--bnance-bwa.com","xn--shpeshift-11a.com","xn--shapeshif-ts6d.com","xn--shapshift-yf7d.com","wwwbluzelle.com","bluzelie.com","nucleus-vision.org","omisegonetwork.site","etlherzero.com","etlherdelta.com","xn--condesk-0ya.com","xn--condesk-sfb.com","xn--coindsk-vs4c.com","iexecplatform.com","tongramico.com","nucleus-vision.eu","intchain.network","wanchain.cloud","bluzelle-ico.com","ethzero-wallet.com","xn--metherwalle-jb9et7d.com","xn--coinesk-jo3c.com","venchainfoundation.com","myenhtersvvailot.com","ether-zero.net","ins.foundation","nastoken.org","telcointoken.com","ether0.org","eterzero.org","bluzelle-ico.eu","bleuzelle.com","appcoinstoken.org","xn--quanstamp-8s6d.com","myehntersvvailct.com","myeherwalllet.com","ico-bluzelle.com","bluzelle.im","bluzelle.one","bluzele.sale","bluzele.co","sether.ws","xn--myetherwalet-6gf.com","xn--rnyethewaliet-om1g.com","rnyethervailet.com","mvetherwaliet.com","rnyetherwailet.com","myethervaliet.com","rnyethervaliet.com","mvetherwalilet.com","xn--myethewalie-3ic0947g.com","xn--mthrwallet-z6ac3y.com","xn--myeherwalie-vici.com","xn--myethervvalie-8vc.com","xn--mythrwallt-06acf.com","xn--mtherwallet-y9a6y.com","myetherwallet.applytoken.tk","ethereum-zero.com","quanstamptoken.tk","bluzelle.network","ether-wallet.org","tron-wallet.info","appcoinsproject.com","vechain.foundation","tronlab.site","tronlabs.network","bluzelle.cc","ethblender.com","ethpaperwallet.net","waltontoken.org","icoselfkey.org","etherzeroclaim.com","etherzero.promo","bluzelle.pro","token-selfkey.org","xn--etherdlta-0f7d.com","sether.in","xn--ttrex-ysa9423c.com","bluzelle.eu","bluzelle.site","gifto.tech","xn--os-g7s.com","selfkey.co","xn--myeherwalet-ns8exy.com","xn--coinelegraph-wk5f.com","dai-stablecoin.com","eos-token.org","venchain.org","gatcoins.io","deepbrainchain.co","myetherwalililet.info","myehvterwallet.com","myehterumswallet.com","nucleusico.com","tronlab.tech","0x-project.com","gift-token-events.mywebcommunity.org","funfairtoken.org","breadtokenapp.com","cloudpetstore.com","myethwalilet.com","selfkeys.org","wallet-ethereum.com","xn--methrwallt-26ar0z.com","xn--mytherwllet-r8a0c.com","bluzelle.promo","tokensale.bluzelle.promo","cedarlake.org","marketingleads4u.com","cashaa.co","xn--inance-hrb.com","wanchain.tech","zenprolocol.com","ethscan.io","etherscan.in","props-project.com","zilliaq.com","reqestnetwork.com","etherdelta.pw","ethereum-giveaway.org","mysimpletoken.org","binancc.com","blnance.org","elherdelta.io","xn--hapeshit-ez9c2y.com","tenxwallet.co","singularitynet.info","mytlherwaliet.info","iconmainnet.ml","tokenselfkey.org","xn--myetewallet-cm8e5y.com","envione.org","myetherwalletet.com","claimbcd.com","ripiocreditnetwork.in","xn--yeterwallet-ml8euo.com","ethclassicwallet.info","myltherwallet.ru.com","etherdella.com","xn--yeterwallet-bm8ewn.com","singularty.net","cloudkitties.co","iconfoundation.io","kittystat.com","gatscoin.io","singularitynet.in","sale.canay.io","canay.io","wabicoin.co","envion.top","sirinslabs.com","tronlab.co","paxful.com.ng","changellyli.com","ethereum-code.com","xn--plonex-6va6c.com","envion.co","envion.cc","envion.site","ethereumchain.info","xn--envon-1sa.org","xn--btstamp-rfb.net","envlon.org","envion-ico.org","spectivvr.org","sirinlbs.com","ethereumdoubler.life","xn--myetherwllet-fnb.com","sirin-labs.com","sirin-labs.org","envion.one","envion.live","propsproject.org","propsprojects.com","decentralland.org","xn--metherwalet-ns8ep4b.com","redpulsetoken.co","propsproject.tech","xn--myeterwalet-nl8emj.com","powrerledger.com","cryptokitties.com","sirinlabs.pro","sirinlabs.co","sirnlabs.com","superbitcoin-blockchain.info","hellobloom.me","mobus.network","powrrledger.com","xn--myeherwalet-ms8eyy.com","qlink-ico.com","gatcoin.in","tokensale.gamefllp.com","gamefllp.com","xn--myeherwalle-vici.com","xn--myetherwalet-39b.com","xn--polonex-ffb.com","xn--birex-leba.com","raiden-network.org","sirintabs.com","xn--metherwallt-79a30a.com","xn--myethrwllet-2kb3p.com","myethlerwallet.eu","xn--btrex-b4a.com","powerrledger.com","xn--cointeegraph-wz4f.com","myerherwalet.com","qauntstanp.com","myetherermwallet.com","xn--myethewalet-ns8eqq.com","xn--nvion-hza.org","nnyetherwallelt.ru.com","ico-wacoin.com","xn--myeterwalet-nl8enj.com","bitcoinsilver.io","t0zero.com","tokensale.gizer.in","gizer.in","wabitoken.com","gladius.ws","xn--metherwallt-8bb4w.com","quanttstamp.com","gladius.im","ethereumstorage.net","powerledgerr.com","xn--myeherwallet-4j5f.com","quamtstamp.com","quntstamp.com","xn--changely-j59c.com","shapeshlft.com","coinbasenews.co.uk","xn--metherwallet-hmb.com","envoin.org","powerledger.com","bitstannp.net","xn--myetherallet-4k5fwn.com","xn--coinbas-pya.com","requestt.network","oracls.network","sirinlabs.website","powrledger.io","slackconfirm.com","shape-shift.io","oracles-network.org","xn--myeherwalle-zb9eia.com","blockstack.one","urtust.io","bittrex.one","t0-ico.com","xn--cinbase-90a.com","xn--metherwalet-ns8ez1g.com","tzero-ico.com","tzero.su","tzero.website","blockstack.network","ico-tzero.com","spectre.site","tzero.pw","spectre-ai.net","xn--waxtokn-y8a.com","dmarket.pro","bittrex.com11648724328774.cf","bittrex.com1987465798.ga","autcus.org","t-zero.org","xn--zero-zxb.com","myetherwalletfork.com","blokclbain.info","datum.sale","spectre-ai.org","powerledgr.com","simpletoken.live","sale.simpletoken.live","qauntstamp.com","raiden-network.com","metalpayme.com","quantstamp-ico.com","myetherwailetclient.com","biockchain.biz","wallets-blockchain.com","golemairdrop.com","omisegoairdrop.net","blodkchainwallet.info","walton-chain.org","elite888-ico.com","bitflyerjp.com","chainlinksmartcontract.com","stormtoken.eu","omise-go.tech","saltending.com","stormltoken.com","xn--quanttamp-42b.com","stormtoken.co","storntoken.com","stromtoken.com","storm-token.com","stormtokens.io","ether-delta.com","ethconnect.live","ethconnect.trade","xn--bttrex-3va.net","quantstamp.com.co","wancha.in","augur-network.com","quantstamp.com.ua","myetherwalletmew.com","myetherumwalletts.com","xn--quanstamp-tmd.com","quantsstamps.com","changellyl.net","xn--myetherwalet-1fb.com","myethereumwallets.com","xn--myetherwalet-e9b.com","quantslamp.com","metelpay.com","xn--eterdelta-m75d.com","linksmartcontract.com","myetherwalletaccess.com","myetherwalletcheck.com","myetherwalletcheck.info","myetherwalletconf.com","myetherwalleteal.com","myetherwalletec.com","myetherwalletgeth.com","myetherwalletmetamask.com","myetherwalletmm.com","myetherwalletmy.com","myetherwalletnh.com","myetherwalletnod.com","myetherwalletrr.com","myetherwalletrty.com","myetherwalletsec.com","myetherwalletsecure.com","myetherwalletutc.com","myetherwalletver.info","myetherwalletview.com","myetherwalletview.info","myetherwalletvrf.com","myetherwalletmist.com","myetherwalletext.com","myetherwalletjson.com","mettalpay.com","bricklblock.io","bittrexy.com","utrust.so","myethierwallet.org","metallpay.com","kraken-wallet.com","dmarkt.io","etherdeltla.com","unlversa.io","universa.sale","mercuryprotocol.live","ripiocredlt.network","myetlherwa11et.com","dentacoin.in","rdrtg.com","myetherwallet.com.rdrgh.com","rdrgh.com","ripiocreditnetwork.co","riaden.network","hydrominer.biz","rdrblock.com","reqest.network","senstoken.com","myetherwallat.services","ripiocredit.net","xn--metherwallet-c06f.com","ico.ripiocredits.com","ripiocredits.com","raidens.network","artoken.co","myetherwalletlgn.com","etherblog.click","stormtoken.site","httpmyetherwallet.com","myetherwalletverify.com","byzantiumfork.com","myetherwallet.com.byzantiumfork.com","www-myethervvallet.com","ether24.info","block-v.io","bittrex.cash","shapishift.io","ripiocerdit.network","rnyetherwa11et.com","claimether.com","enigmatokensale.com","ethereum-org.com","mvetnerwallet.com","myctherwallet.com","myetherwaltet.com","myetherwatlet.com","privatix.me","myetherwalletcnf.com","myetherwalletver.com","privatix.top","privatix.pro","privatex.io","stormtoken.cc","raiden.online","stormstoken.com","myetereumwallet.com","stormtokens.net","myetherwalletconf.info","storrntoken.com","worldofbattles.io","ico.worldofbattles.io","privatix.live","riden.network","raidan.network","ralden.network","mymyetherwallet.com","myetherwallets.net","myetherwalletverify.info","stormxtoken.com","myethereum-wallet.com","myetherwallet-forkprep.pagedemo.co","myetnerwailet.com","www-mvetherwallet.com","etheirdelta.com","myetherwalletiu.com","myetherwaiiett.com","xn--mytherwalet-cbb87i.com","xn--myethrwallet-ivb.co","xn--myeterwallet-f1b.com","myehterwaliet.com","omegaone.co","myetherwaiietw.com","slack.com.ru","polkodot.network","request-network.net","requestnetwork.live","binancie.com","first-eth.info","myewerthwalliet.com","enjincoin.pw","xn--bitrex-k17b.com","alrswap.io","www-request.network","myetnenwallet.com","www-enigma.co","cryptoinsidenews.com","air-swap.tech","launch.airswap.cc","airswap.cc","airswaptoken.com","launch.airswap.in","airswap.in","security-steemit.com.mx","blockchalnwallet.com","blodkchainwallet.com","blodkchaln.com","myethereumwaiiet.com","myethereumwaliet.com","myethereumwalilet.com","myetherswailet.com","myetherswaliet.com","myetherswalilet.com","myetherwalilett.com","myetherwalletl.com","myetherwalletww.com","myethereunwallet.com","myethereumwallct.com","myetherwaiieti.com","myetherwaiiete.com","upfirng.com","paypie.net","paypie.tech","soam.co","myetherwaiict.com","numerai-token.com","www-bankera.com","vvanchain.org","omisegoairdrop.com","xn--enjncoin-41a.io","suncontract.su","myetherwaiietr.com","shapeshiff.io","warchain.org","myethwallett.com","myethervvaliet.com","wanchains.org","etherparty.in","enjincoin.me","etiam.io","invest.smartlands.tech","smartlands.tech","enijncoin.io","wanchain.network","nimiq.su","enjincoin.sale","tenxwallet.io","golem-network.net","myyethwallet.ml","mywetherwailiet.com","omg-omise.com","district0x.tech","centra-token.com","etherdetla.com","etnerparty.io","etherdelta.su","myetherwallett.neocities.org","myetherwallet-secure.com","myethereumwalletntw.info","real-markets.io","wallet-ethereum.org","request-network.com","shapeshifth.io","shiapeshift.in","coin.red-puise.com","ibittreix.com","coinkbase.com","cindicator.pro","myetherwallet.com.ailogin.me","eventchain.co","kinkik.in","myetherumwalletview.com","protostokenhub.com","coinrbase.com","myetherwalletlogin.com","omisegotoken.com","myethereumwalletntw.com","reall.markets","cobinhood.org","cobinhood.io","happy-coin.org","bitfinex.com.co","bitfienex.com","iconn.foundation","centra.vip","smartcontract.live","icon.community","air-token.com","centra.credit","myetherwallet-singin.com","smartcontractlink.com","shapesshift.io","0xtoken.io","augurproject.co","ethereumus.one","myetherumwalet.com","myetherwalletsignin.com","change-bank.org","charge-bank.com","myetherwalletsingin.com","myetherwalletcontract.com","change-bank.io","chainlink.tech","myetherwallet-confirm.com","tokensale.kybernet.network","kybernet.network","kyberr.network","kybernetwork.io","myetherwalletconfirm.com","kvnuke.github.io","kin.kikpro.co","myethereumwallet.co.uk","tokensale-kyber.network","kyber-network.co","tokensale.kyber-network.co","pyro0.github.io","tokensale.kyber.digital","kyber.digital","omise-go.me","my.etherwallet.com.de","bepartof.change-bank.co","change-bank.co","enigma-tokens.co","coinbase.com.eslogin.co","xn--bittrx-mva.com","ethrdelta.github.io","etherdellta.com","ico-nexus.social","red-pulse.tech","bitj0b.io","xn--bttrex-bwa.com","kin-klk.com","kin-crowdsale.com","ethedelta.com","coindash.su","myethwallet.co.uk","swarm.credit","myethereumwallet.uk","iconexu.social","wanchain.co","enigrna.co","linknetwork.co","qtum-token.com","omisego.com.co","rivetzintl.org","etherdelta.one","the-ether.pro","etherdelta.gitnub.io","kirkik.com","monetha.ltd","vlberate.io","ethereumwallet-kr.info","omise-go.org","iconexus.social","bittirrex.com","aventus.pro","atlant.solutions","aventus.group","metamak.io","omise.com.co","herotokens.io","starbase.pro","etherdelta.githulb.io","herotoken.co","kinico.net","dmarket.ltd","etherdelta.gilthub.io","golem-network.com","etnerscan.io","bllttriex.com","monetha.me","monetha.co","monetha-crowdsale.com","starbase.tech","aventus-crowdsale.com","shapeshift.pro","bllttrex.com","kickico.co","statustoken.im","bilttrex.com","tenxpay.io","bittrex.ltd","metalpay.im","aragon.im","coindash.tech","decentraland.tech","decentraland.pro","status-token.com","bittrex.cam","enigmatoken.com","unocoin.company","unocoin.fund","0xproject.io","0xtoken.com","numerai.tech","decentraiand.org","blockcrein.info","blockchealn.info","bllookchain.info","blockcbhain.info","myetherwallet.com.ethpromonodes.com","mettamask.io","tokenswap.org","netherum.com","etherexx.org","etherume.io","ethereum.plus","ehtereum.org","etereurm.org","etheream.com","ethererum.org","ethereum.io","etherdelta-glthub.com","cryptoalliance.herokuapp.com","bitspark2.com","indorsetoken.com","iconexus.tk","iconexus.ml","iconexus.ga","iconexus.cf","etherwallet.online","wallet-ethereum.net","bitsdigit.com","etherswap.org","eos.ac","uasfwallet.com","ziber.io","multiply-ethereum.info","bittrex.comze.com","karbon.vacau.com","etherdelta.gitlhub.io","etherdelta.glthub.io","digitaldevelopersfund.vacau.com","district-0x.io","coin-dash.com","coindash.ru","district0x.net","aragonproject.io","coin-wallet.info","coinswallet.info","contribute-status.im","ether-api.com","ether-wall.com","mycoinwallet.net","ethereumchamber.com","ethereumchamber.net","ethereumchest.com","ethewallet.com","myetherwallet.com.vc","myetherwallet.com.pe","myetherwallet.us.com","myetherwallet.com.u0387831.cp.regruhosting.ru","myethereumwallet.su","myetherweb.com.de","myetherieumwallet.com","myetehrwallet.com","myeterwalet.com","myetherwaiiet.com","myetherwallet.info","myetherwallet.ch","myetherwallet.om","myethervallet.com","myetherwallet.com.cm","myetherwallet.com.co","myetherwallet.com.de","myetherwallet.com.gl","myetherwallet.com.im","myetherwallet.com.ua","secure-myetherwallet.com","update-myetherwallet.com","wwwmyetherwallet.com","myeatherwallet.com","myetharwallet.com","myelherwallel.com","myetherwaillet.com","myetherwaliet.com","myetherwallel.com","myetherwallet.cam","myetherwallet.cc","myetherwallet.co","myetherwallet.cm","myetherwallet.cz","myetherwallet.org","myetherwallet.tech","myetherwallet.top","myetherwallet.net","myetherwallet.ru.com","myetherwallet.com.ru","metherwallet.com","myetrerwallet.com","myetlerwallet.com","myethterwallet.com","myethwallet.io","myethterwallet.co","myehterwallet.co","myaetherwallet.com","myetthterwallet.com","myetherwallet.one","myelterwallet.com","myetherwallet.gdn","myetherwallt.com","myeterwallet.com","myeteherwallet.com","myethearwailet.com","myetherwallelt.com","myetherwallett.com","etherwallet.org","myetherewallet.com","myeherwallet.com","myethcrwallet.com","myetherwallet.link","myetherwallets.com","myethearwaillet.com","myethearwallet.com","myetherawllet.com","myethereallet.com","myetherswallet.com","myetherwalet.com","myetherwaller.com","myetherwalliet.com","myetherwllet.com","etherwallet.io","myetherwallet.ca","myetherwallet.me","myetherwallet.ru","myetherwallet.xyz","myetherwallte.com","myethirwallet.com","myethrewallet.com","etherwallet.net","maetherwallet.com","meyetherwallet.com","my.ether-wallet.pw","myehterwallet.com","myeitherwallet.com","myelherwallet.com","myeltherwallet.com","myerherwallet.com","myethearwalet.com","myetherewalle.com","myethervvallet.com","myetherwallent.com","myetherwallet.fm","myetherwalllet.com","myetherwalltet.com","myetherwollet.com","myetlherwalet.com","myetlherwallet.com","rnyetherwallet.com","etherclassicwallet.com","omg-omise.co","omise-go.com","omise-go.net","omise-omg.com","omise-go.io","tenx-tech.com","bitclaive.com","tokensale-tenx.tech","ubiqcoin.org","metamask.com","ethtrade.io","myetcwallet.com","account-kigo.net","bitcoin-wallet.net","blocklichan.info","bloclkicihan.info","coindash.ml","eos-bonus.com","eos-io.info","ether-wallet.net","ethereum-wallet.info","ethereum-wallet.net","ethereumchest.net","reservations-kigo.net","reservations-lodgix.com","secure-liverez.com","secure-onerooftop.com","settings-liverez.com","software-liverez.com","software-lodgix.com","unhackableetherwallets.com","www-myetherwallet.com","etherwallet.co.za","etherwalletchain.com","etherwallets.net","etherwallets.nl","my-ethwallet.com","my.ether-wallet.co","myetherwallet.com.am","myetherwallet.com.ht","myetherwalletcom.com","myehterwailet.com","xn--myetherwalle-xoc.com","xn--myetherwalle-44i.com","xn--myetherwalle-xhk.com","xn--myetherwallt-cfb.com","xn--myetherwallt-6tb.com","xn--myetherwallt-xub.com","xn--myetherwallt-ovb.com","xn--myetherwallt-fwb.com","xn--myetherwallt-5wb.com","xn--myetherwallt-jzi.com","xn--myetherwallt-2ck.com","xn--myetherwallt-lok.com","xn--myetherwallt-lsl.com","xn--myetherwallt-ce6f.com","xn--myetherwalet-mcc.com","xn--myetherwalet-xhf.com","xn--myetherwalet-lcc.com","xn--myetherwaet-15ba.com","xn--myetherwalet-whf.com","xn--myetherwaet-v2ea.com","xn--myetherwllet-59a.com","xn--myetherwllet-jbb.com","xn--myetherwllet-wbb.com","xn--myetherwllet-9bb.com","xn--myetherwllet-ncb.com","xn--myetherwllet-0cb.com","xn--myetherwllet-5nb.com","xn--myetherwllet-ktd.com","xn--myetherwllet-mre.com","xn--myetherwllet-76e.com","xn--myetherwllet-o0l.com","xn--myetherwllet-c45f.com","xn--myetherallet-ejn.com","xn--myethewallet-4nf.com","xn--myethewallet-iof.com","xn--myethewallet-mpf.com","xn--myethewallet-6bk.com","xn--myethewallet-i31f.com","xn--myethrwallet-feb.com","xn--myethrwallt-fbbf.com","xn--myethrwallet-seb.com","xn--myethrwallt-rbbf.com","xn--myethrwallet-5eb.com","xn--myethrwallt-3bbf.com","xn--myethrwallet-0tb.com","xn--myethrwallt-tpbf.com","xn--myethrwallet-rub.com","xn--myethrwallt-iqbf.com","xn--myethrwallet-ivb.com","xn--myethrwallt-6qbf.com","xn--myethrwallet-8vb.com","xn--myethrwallt-vrbf.com","xn--myethrwallet-zwb.com","xn--myethrwallt-ksbf.com","xn--myethrwallet-dzi.com","xn--myethrwallt-wbif.com","xn--myethrwallet-wck.com","xn--myethrwallt-skjf.com","xn--myethrwallet-fok.com","xn--myethrwallt-fvjf.com","xn--myethrwallet-fsl.com","xn--myethrwallt-fwkf.com","xn--myethrwallet-5d6f.com","xn--myethrwallt-319ef.com","xn--myeterwallet-ufk.com","xn--myeterwallet-nrl.com","xn--myeterwallet-von.com","xn--myeterwallet-jl6c.com","xn--myeherwallet-ooc.com","xn--myeherwalle-6hci.com","xn--myeherwallet-v4i.com","xn--myeherwalle-zgii.com","xn--myeherwallet-ohk.com","xn--myeherwalle-6oji.com","xn--mytherwallet-ceb.com","xn--mythrwallet-cbbc.com","xn--mythrwallt-c7acf.com","xn--mytherwallet-peb.com","xn--mythrwallet-obbc.com","xn--mythrwallt-n7acf.com","xn--mytherwallet-2eb.com","xn--mythrwallet-0bbc.com","xn--mythrwallt-y7acf.com","xn--mytherwallet-xtb.com","xn--mythrwallet-qpbc.com","xn--mythrwallt-jlbcf.com","xn--mytherwallet-oub.com","xn--mythrwallet-fqbc.com","xn--mythrwallt-5lbcf.com","xn--mythrwallet-3qbc.com","xn--mythrwallt-smbcf.com","xn--mytherwallet-5vb.com","xn--mythrwallet-srbc.com","xn--mythrwallt-fnbcf.com","xn--mytherwallet-wwb.com","xn--mythrwallet-hsbc.com","xn--mythrwallt-1nbcf.com","xn--mytherwallet-9yi.com","xn--mythrwallet-tbic.com","xn--mythrwallt-dnhcf.com","xn--mytherwallet-tck.com","xn--mythrwallet-pkjc.com","xn--mythrwallt-lsicf.com","xn--mytherwallet-cok.com","xn--mythrwallet-cvjc.com","xn--mythrwallt-c2icf.com","xn--mytherwallet-csl.com","xn--mythrwallet-cwkc.com","xn--mythrwallt-c0jcf.com","xn--mytherwallet-2d6f.com","xn--mythrwallet-019ec.com","xn--mythrwallt-yq3ecf.com","xn--metherwallet-qlb.com","xn--metherwallet-1uf.com","xn--metherwallet-iyi.com","xn--metherwallet-zhk.com","xn--metherwallet-3ml.com","xn--mytherwallet-fvb.com","xn--myetherwallt-7db.com","xn--myetherwallt-leb.com","xn--myetherwallt-yeb.com","xn--yetherwallet-vjf.com","xn--yetherwallet-dfk.com","xn--yetherwallet-1t1f.com","xn--yetherwallet-634f.com","xn--myeherwallet-fpc.com","xn--myethewallt-crb.com","xn--metherwallet-1vc.com","xn--myeherwallt-kbb8039g.com","xn--myeherwallet-vk5f.com","xn--yethewallet-iw8ejl.com","xn--bittrx-th8b.com","xn--polniex-n0a.com","thekey.vin","thekey-vip.com","digitexftures.com","ethzero-wallet.org","zeepln.io","wepowers.network","wepower.vision"]}},"CurrencyController":{"currentCurrency":"usd","conversionRate":1112,"conversionDate":1517351401}}} \ No newline at end of file
+{"meta":{"version":20},"data":{"config":{},"NetworkController":{"provider":{"type":"mainnet","rpcTarget":"https://mainnet.infura.io/metamask"},"network":"1"},"firstTimeInfo":{"version":"3.12.1","date":1517351427287},"NoticeController":{"noticesList":[{"read":false,"date":"Thu Feb 09 2017","title":"Terms of Use","body":"# Terms of Use #\n\n**THIS AGREEMENT IS SUBJECT TO BINDING ARBITRATION AND A WAIVER OF CLASS ACTION RIGHTS AS DETAILED IN SECTION 13. PLEASE READ THE AGREEMENT CAREFULLY.**\n\n_Our Terms of Use have been updated as of September 5, 2016_\n\n## 1. Acceptance of Terms ##\n\nMetaMask provides a platform for managing Ethereum (or \"ETH\") accounts, and allowing ordinary websites to interact with the Ethereum blockchain, while keeping the user in control over what transactions they approve, through our website located at[ ](http://metamask.io)[https://metamask.io/](https://metamask.io/) and browser plugin (the \"Site\") — which includes text, images, audio, code and other materials (collectively, the “Content”) and all of the features, and services provided. The Site, and any other features, tools, materials, or other services offered from time to time by MetaMask are referred to here as the “Service.” Please read these Terms of Use (the “Terms” or “Terms of Use”) carefully before using the Service. By using or otherwise accessing the Services, or clicking to accept or agree to these Terms where that option is made available, you (1) accept and agree to these Terms (2) consent to the collection, use, disclosure and other handling of information as described in our Privacy Policy and (3) any additional terms, rules and conditions of participation issued by MetaMask from time to time. If you do not agree to the Terms, then you may not access or use the Content or Services.\n\n## 2. Modification of Terms of Use ##\n\nExcept for Section 13, providing for binding arbitration and waiver of class action rights, MetaMask reserves the right, at its sole discretion, to modify or replace the Terms of Use at any time. The most current version of these Terms will be posted on our Site. You shall be responsible for reviewing and becoming familiar with any such modifications. Use of the Services by you after any modification to the Terms constitutes your acceptance of the Terms of Use as modified.\n\n\n\n## 3. Eligibility ##\n\nYou hereby represent and warrant that you are fully able and competent to enter into the terms, conditions, obligations, affirmations, representations and warranties set forth in these Terms and to abide by and comply with these Terms.\n\nMetaMask is a global platform and by accessing the Content or Services, you are representing and warranting that, you are of the legal age of majority in your jurisdiction as is required to access such Services and Content and enter into arrangements as provided by the Service. You further represent that you are otherwise legally permitted to use the service in your jurisdiction including owning cryptographic tokens of value, and interacting with the Services or Content in any way. You further represent you are responsible for ensuring compliance with the laws of your jurisdiction and acknowledge that MetaMask is not liable for your compliance with such laws.\n\n## 4 Account Password and Security ##\n\nWhen setting up an account within MetaMask, you will be responsible for keeping your own account secrets, which may be a twelve-word seed phrase, an account file, or other locally stored secret information. MetaMask encrypts this information locally with a password you provide, that we never send to our servers. You agree to (a) never use the same password for MetaMask that you have ever used outside of this service; (b) keep your secret information and password confidential and do not share them with anyone else; (c) immediately notify MetaMask of any unauthorized use of your account or breach of security. MetaMask cannot and will not be liable for any loss or damage arising from your failure to comply with this section.\n\n## 5. Representations, Warranties, and Risks ##\n\n### 5.1. Warranty Disclaimer ###\n\nYou expressly understand and agree that your use of the Service is at your sole risk. The Service (including the Service and the Content) are provided on an \"AS IS\" and \"as available\" basis, without warranties of any kind, either express or implied, including, without limitation, implied warranties of merchantability, fitness for a particular purpose or non-infringement. You acknowledge that MetaMask has no control over, and no duty to take any action regarding: which users gain access to or use the Service; what effects the Content may have on you; how you may interpret or use the Content; or what actions you may take as a result of having been exposed to the Content. You release MetaMask from all liability for you having acquired or not acquired Content through the Service. MetaMask makes no representations concerning any Content contained in or accessed through the Service, and MetaMask will not be responsible or liable for the accuracy, copyright compliance, legality or decency of material contained in or accessed through the Service.\n\n### 5.2 Sophistication and Risk of Cryptographic Systems ###\n\nBy utilizing the Service or interacting with the Content or platform in any way, you represent that you understand the inherent risks associated with cryptographic systems; and warrant that you have an understanding of the usage and intricacies of native cryptographic tokens, like Ether (ETH) and Bitcoin (BTC), smart contract based tokens such as those that follow the Ethereum Token Standard (https://github.com/ethereum/EIPs/issues/20), and blockchain-based software systems.\n\n### 5.3 Risk of Regulatory Actions in One or More Jurisdictions ###\n\nMetaMask and ETH could be impacted by one or more regulatory inquiries or regulatory action, which could impede or limit the ability of MetaMask to continue to develop, or which could impede or limit your ability to access or use the Service or Ethereum blockchain.\n\n### 5.4 Risk of Weaknesses or Exploits in the Field of Cryptography ###\n\nYou acknowledge and understand that Cryptography is a progressing field. Advances in code cracking or technical advances such as the development of quantum computers may present risks to cryptocurrencies and Services of Content, which could result in the theft or loss of your cryptographic tokens or property. To the extent possible, MetaMask intends to update the protocol underlying Services to account for any advances in cryptography and to incorporate additional security measures, but does not guarantee or otherwise represent full security of the system. By using the Service or accessing Content, you acknowledge these inherent risks.\n\n### 5.5 Volatility of Crypto Currencies ###\n\nYou understand that Ethereum and other blockchain technologies and associated currencies or tokens are highly volatile due to many factors including but not limited to adoption, speculation, technology and security risks. You also acknowledge that the cost of transacting on such technologies is variable and may increase at any time causing impact to any activities taking place on the Ethereum blockchain. You acknowledge these risks and represent that MetaMask cannot be held liable for such fluctuations or increased costs.\n\n### 5.6 Application Security ###\n\nYou acknowledge that Ethereum applications are code subject to flaws and acknowledge that you are solely responsible for evaluating any code provided by the Services or Content and the trustworthiness of any third-party websites, products, smart-contracts, or Content you access or use through the Service. You further expressly acknowledge and represent that Ethereum applications can be written maliciously or negligently, that MetaMask cannot be held liable for your interaction with such applications and that such applications may cause the loss of property or even identity. This warning and others later provided by MetaMask in no way evidence or represent an on-going duty to alert you to all of the potential risks of utilizing the Service or Content.\n\n## 6. Indemnity ##\n\nYou agree to release and to indemnify, defend and hold harmless MetaMask and its parents, subsidiaries, affiliates and agencies, as well as the officers, directors, employees, shareholders and representatives of any of the foregoing entities, from and against any and all losses, liabilities, expenses, damages, costs (including attorneys’ fees and court costs) claims or actions of any kind whatsoever arising or resulting from your use of the Service, your violation of these Terms of Use, and any of your acts or omissions that implicate publicity rights, defamation or invasion of privacy. MetaMask reserves the right, at its own expense, to assume exclusive defense and control of any matter otherwise subject to indemnification by you and, in such case, you agree to cooperate with MetaMask in the defense of such matter.\n\n## 7. Limitation on liability ##\n\nYOU ACKNOWLEDGE AND AGREE THAT YOU ASSUME FULL RESPONSIBILITY FOR YOUR USE OF THE SITE AND SERVICE. YOU ACKNOWLEDGE AND AGREE THAT ANY INFORMATION YOU SEND OR RECEIVE DURING YOUR USE OF THE SITE AND SERVICE MAY NOT BE SECURE AND MAY BE INTERCEPTED OR LATER ACQUIRED BY UNAUTHORIZED PARTIES. YOU ACKNOWLEDGE AND AGREE THAT YOUR USE OF THE SITE AND SERVICE IS AT YOUR OWN RISK. RECOGNIZING SUCH, YOU UNDERSTAND AND AGREE THAT, TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, NEITHER METAMASK NOR ITS SUPPLIERS OR LICENSORS WILL BE LIABLE TO YOU FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY OR OTHER DAMAGES OF ANY KIND, INCLUDING WITHOUT LIMITATION DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA OR OTHER TANGIBLE OR INTANGIBLE LOSSES OR ANY OTHER DAMAGES BASED ON CONTRACT, TORT, STRICT LIABILITY OR ANY OTHER THEORY (EVEN IF METAMASK HAD BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES), RESULTING FROM THE SITE OR SERVICE; THE USE OR THE INABILITY TO USE THE SITE OR SERVICE; UNAUTHORIZED ACCESS TO OR ALTERATION OF YOUR TRANSMISSIONS OR DATA; STATEMENTS OR CONDUCT OF ANY THIRD PARTY ON THE SITE OR SERVICE; ANY ACTIONS WE TAKE OR FAIL TO TAKE AS A RESULT OF COMMUNICATIONS YOU SEND TO US; HUMAN ERRORS; TECHNICAL MALFUNCTIONS; FAILURES, INCLUDING PUBLIC UTILITY OR TELEPHONE OUTAGES; OMISSIONS, INTERRUPTIONS, LATENCY, DELETIONS OR DEFECTS OF ANY DEVICE OR NETWORK, PROVIDERS, OR SOFTWARE (INCLUDING, BUT NOT LIMITED TO, THOSE THAT DO NOT PERMIT PARTICIPATION IN THE SERVICE); ANY INJURY OR DAMAGE TO COMPUTER EQUIPMENT; INABILITY TO FULLY ACCESS THE SITE OR SERVICE OR ANY OTHER WEBSITE; THEFT, TAMPERING, DESTRUCTION, OR UNAUTHORIZED ACCESS TO, IMAGES OR OTHER CONTENT OF ANY KIND; DATA THAT IS PROCESSED LATE OR INCORRECTLY OR IS INCOMPLETE OR LOST; TYPOGRAPHICAL, PRINTING OR OTHER ERRORS, OR ANY COMBINATION THEREOF; OR ANY OTHER MATTER RELATING TO THE SITE OR SERVICE.\n\nSOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF CERTAIN WARRANTIES OR THE LIMITATION OR EXCLUSION OF LIABILITY FOR INCIDENTAL OR CONSEQUENTIAL DAMAGES. ACCORDINGLY, SOME OF THE ABOVE LIMITATIONS MAY NOT APPLY TO YOU.\n\n## 8. Our Proprietary Rights ##\n\nAll title, ownership and intellectual property rights in and to the Service are owned by MetaMask or its licensors. You acknowledge and agree that the Service contains proprietary and confidential information that is protected by applicable intellectual property and other laws. Except as expressly authorized by MetaMask, you agree not to copy, modify, rent, lease, loan, sell, distribute, perform, display or create derivative works based on the Service, in whole or in part. MetaMask issues a license for MetaMask, found [here](https://github.com/MetaMask/metamask-plugin/blob/master/LICENSE). For information on other licenses utilized in the development of MetaMask, please see our attribution page at: [https://metamask.io/attributions.html](https://metamask.io/attributions.html)\n\n## 9. Links ##\n\nThe Service provides, or third parties may provide, links to other World Wide Web or accessible sites, applications or resources. Because MetaMask has no control over such sites, applications and resources, you acknowledge and agree that MetaMask is not responsible for the availability of such external sites, applications or resources, and does not endorse and is not responsible or liable for any content, advertising, products or other materials on or available from such sites or resources. You further acknowledge and agree that MetaMask shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such content, goods or services available on or through any such site or resource.\n\n## 10. Termination and Suspension ##\n\nMetaMask may terminate or suspend all or part of the Service and your MetaMask access immediately, without prior notice or liability, if you breach any of the terms or conditions of the Terms. Upon termination of your access, your right to use the Service will immediately cease.\n\nThe following provisions of the Terms survive any termination of these Terms: INDEMNITY; WARRANTY DISCLAIMERS; LIMITATION ON LIABILITY; OUR PROPRIETARY RIGHTS; LINKS; TERMINATION; NO THIRD PARTY BENEFICIARIES; BINDING ARBITRATION AND CLASS ACTION WAIVER; GENERAL INFORMATION.\n\n## 11. No Third Party Beneficiaries ##\n\nYou agree that, except as otherwise expressly provided in these Terms, there shall be no third party beneficiaries to the Terms.\n\n## 12. Notice and Procedure For Making Claims of Copyright Infringement ##\n\nIf you believe that your copyright or the copyright of a person on whose behalf you are authorized to act has been infringed, please provide MetaMask’s Copyright Agent a written Notice containing the following information:\n\n· an electronic or physical signature of the person authorized to act on behalf of the owner of the copyright or other intellectual property interest;\n\n· a description of the copyrighted work or other intellectual property that you claim has been infringed;\n\n· a description of where the material that you claim is infringing is located on the Service;\n\n· your address, telephone number, and email address;\n\n· a statement by you that you have a good faith belief that the disputed use is not authorized by the copyright owner, its agent, or the law;\n\n· a statement by you, made under penalty of perjury, that the above information in your Notice is accurate and that you are the copyright or intellectual property owner or authorized to act on the copyright or intellectual property owner's behalf.\n\nMetaMask’s Copyright Agent can be reached at:\n\nEmail: copyright [at] metamask [dot] io\n\nMail:\n\nAttention:\n\nMetaMask Copyright ℅ ConsenSys\n\n49 Bogart Street\n\nBrooklyn, NY 11206\n\n## 13. Binding Arbitration and Class Action Waiver ##\n\nPLEASE READ THIS SECTION CAREFULLY – IT MAY SIGNIFICANTLY AFFECT YOUR LEGAL RIGHTS, INCLUDING YOUR RIGHT TO FILE A LAWSUIT IN COURT\n\n### 13.1 Initial Dispute Resolution ###\n\nThe parties shall use their best efforts to engage directly to settle any dispute, claim, question, or disagreement and engage in good faith negotiations which shall be a condition to either party initiating a lawsuit or arbitration.\n\n### 13.2 Binding Arbitration ###\n\nIf the parties do not reach an agreed upon solution within a period of 30 days from the time informal dispute resolution under the Initial Dispute Resolution provision begins, then either party may initiate binding arbitration as the sole means to resolve claims, subject to the terms set forth below. Specifically, all claims arising out of or relating to these Terms (including their formation, performance and breach), the parties’ relationship with each other and/or your use of the Service shall be finally settled by binding arbitration administered by the American Arbitration Association in accordance with the provisions of its Commercial Arbitration Rules and the supplementary procedures for consumer related disputes of the American Arbitration Association (the \"AAA\"), excluding any rules or procedures governing or permitting class actions.\n\nThe arbitrator, and not any federal, state or local court or agency, shall have exclusive authority to resolve all disputes arising out of or relating to the interpretation, applicability, enforceability or formation of these Terms, including, but not limited to any claim that all or any part of these Terms are void or voidable, or whether a claim is subject to arbitration. The arbitrator shall be empowered to grant whatever relief would be available in a court under law or in equity. The arbitrator’s award shall be written, and binding on the parties and may be entered as a judgment in any court of competent jurisdiction.\n\nThe parties understand that, absent this mandatory provision, they would have the right to sue in court and have a jury trial. They further understand that, in some instances, the costs of arbitration could exceed the costs of litigation and the right to discovery may be more limited in arbitration than in court.\n\n### 13.3 Location ###\n\nBinding arbitration shall take place in New York. You agree to submit to the personal jurisdiction of any federal or state court in New York County, New York, in order to compel arbitration, to stay proceedings pending arbitration, or to confirm, modify, vacate or enter judgment on the award entered by the arbitrator.\n\n### 13.4 Class Action Waiver ###\n\nThe parties further agree that any arbitration shall be conducted in their individual capacities only and not as a class action or other representative action, and the parties expressly waive their right to file a class action or seek relief on a class basis. YOU AND METAMASK AGREE THAT EACH MAY BRING CLAIMS AGAINST THE OTHER ONLY IN YOUR OR ITS INDIVIDUAL CAPACITY, AND NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED CLASS OR REPRESENTATIVE PROCEEDING. If any court or arbitrator determines that the class action waiver set forth in this paragraph is void or unenforceable for any reason or that an arbitration can proceed on a class basis, then the arbitration provision set forth above shall be deemed null and void in its entirety and the parties shall be deemed to have not agreed to arbitrate disputes.\n\n### 13.5 Exception - Litigation of Intellectual Property and Small Claims Court Claims ###\n\nNotwithstanding the parties' decision to resolve all disputes through arbitration, either party may bring an action in state or federal court to protect its intellectual property rights (\"intellectual property rights\" means patents, copyrights, moral rights, trademarks, and trade secrets, but not privacy or publicity rights). Either party may also seek relief in a small claims court for disputes or claims within the scope of that court’s jurisdiction.\n\n### 13.6 30-Day Right to Opt Out ###\n\nYou have the right to opt-out and not be bound by the arbitration and class action waiver provisions set forth above by sending written notice of your decision to opt-out to the following address: MetaMask ℅ ConsenSys, 49 Bogart Street, Brooklyn NY 11206 and via email at support@metamask.io. The notice must be sent within 30 days of September 6, 2016 or your first use of the Service, whichever is later, otherwise you shall be bound to arbitrate disputes in accordance with the terms of those paragraphs. If you opt-out of these arbitration provisions, MetaMask also will not be bound by them.\n\n### 13.7 Changes to This Section ###\n\nMetaMask will provide 60-days’ notice of any changes to this section. Changes will become effective on the 60th day, and will apply prospectively only to any claims arising after the 60th day.\n\nFor any dispute not subject to arbitration you and MetaMask agree to submit to the personal and exclusive jurisdiction of and venue in the federal and state courts located in New York, New York. You further agree to accept service of process by mail, and hereby waive any and all jurisdictional and venue defenses otherwise available.\n\nThe Terms and the relationship between you and MetaMask shall be governed by the laws of the State of New York without regard to conflict of law provisions.\n\n## 14. General Information ##\n\n### 14.1 Entire Agreement ###\n\nThese Terms (and any additional terms, rules and conditions of participation that MetaMask may post on the Service) constitute the entire agreement between you and MetaMask with respect to the Service and supersedes any prior agreements, oral or written, between you and MetaMask. In the event of a conflict between these Terms and the additional terms, rules and conditions of participation, the latter will prevail over the Terms to the extent of the conflict.\n\n### 14.2 Waiver and Severability of Terms ###\n\nThe failure of MetaMask to exercise or enforce any right or provision of the Terms shall not constitute a waiver of such right or provision. If any provision of the Terms is found by an arbitrator or court of competent jurisdiction to be invalid, the parties nevertheless agree that the arbitrator or court should endeavor to give effect to the parties' intentions as reflected in the provision, and the other provisions of the Terms remain in full force and effect.\n\n### 14.3 Statute of Limitations ###\n\nYou agree that regardless of any statute or law to the contrary, any claim or cause of action arising out of or related to the use of the Service or the Terms must be filed within one (1) year after such claim or cause of action arose or be forever barred.\n\n### 14.4 Section Titles ###\n\nThe section titles in the Terms are for convenience only and have no legal or contractual effect.\n\n### 14.5 Communications ###\n\nUsers with questions, complaints or claims with respect to the Service may contact us using the relevant contact information set forth above and at communications@metamask.io.\n\n## 15 Related Links ##\n\n**[Terms of Use](https://metamask.io/terms.html)**\n\n**[Privacy](https://metamask.io/privacy.html)**\n\n**[Attributions](https://metamask.io/attributions.html)**\n\n","id":0},{"read":false,"date":"Mon May 08 2017","title":"Privacy Notice","body":"MetaMask is beta software. \n\nWhen you log in to MetaMask, your current account is visible to every new site you visit.\n\nFor your privacy, for now, please sign out of MetaMask when you're done using a site.\n\nAlso, by default, you will be signed in to a test network. To use real Ether, you must connect to the main network manually in the top left network menu.\n\n","id":2}]},"BlacklistController":{"phishing":{"version":2,"tolerance":2,"fuzzylist":["metamask.io","myetherwallet.com","cryptokitties.co"],"whitelist":["metahash.io","metahash.net","metahash.org","cryptotitties.com","cryptocities.net","cryptoshitties.co","cryptotitties.fun","cryptokitties.forsale","cryptokitties.care","metamate.cc","metamesh.tech","ico.nexus.social","metamesh.org","metatask.io","metmask.com","metarasa.com","metapack.com","metacase.com","metafas.nl","metamako.com","metamast.com","metamax.ru","metadesk.io","metadisk.com","metallsk.ru","metamag.fr","metamaks.ru","metamap.ru","metamaps.cc","metamats.com","metamax.by","metamax.com","metamax.io","metamuse.net","metarank.com","metaxas.com","megamas2.ru","metamask.io","myetherwallet.com","myethlerwallet.com","ethereum.org","myetheroll.com","myetherapi.com","ledgerwallet.com","databrokerdao.com","etherscan.io","etherid.org","ether.cards","etheroll.com","ethnews.com","ethex.market","ethereumdev.io","ethereumdev.kr","dether.io","ethermine.org","slaask.com","etherbtc.io","ethereal.capital","etherisc.com","m.famalk.net","etherecho.com","ethereum.os.tc","theethereum.wiki","metajack.im","etherhub.io","ethereum.network","ethereum.link","ethereum.com","prethereum.org","ethereumj.io","etheraus.com","ethereum.dev","1ethereum.ru","ethereum.nz","nethereum.com","metabank.com","metamas.com","aventus.io","metabase.com","etherdelta.com","metabase.one","cryptokitties.co"],"blacklist":["myetherwallet.uk.com","kodakone.cc","nyeihitervvallet.com","xn--myeterwalet-cm8eoi.com","nucleus.foundation","beetoken-ico.com","data-token.com","tron-labs.com","ocoin.tech","aionfoundation.com","ico-telegram.org","nyeihitervvallat.com","telegramcoin.us","daddi.cloud","daditoken.com","blockarray.org","dadi-cloud.net","wanchainfunding.org","ico-telegram.io","iconfoundation.site","iost.co","beetoken-ico.eu","cindicator.network","wanchainetwork.org","wamchain.org","wanchainltd.org","wanchainalliance.org","nucleus-vision.net","ledgerwallet.by","nucleuss.vision","myenhterswailct.com","cobin-hood.com","wanchainfoundation.org","xn--polniex-ex4c.com","xn--polniex-s1a.com","xn--polonex-ieb.com","xn--polonex-sza.com","xn--polonex-zw4c.com","xn--polonix-ws4c.com","xn--polonix-y8a.com","xn--pooniex-ojb.com","gramico.info","dimnsions.network","www-gemini.com","login-kucoin.net","venchain.foundation","grampreico.com","tgram.cc","ton-gramico.com","wwwpaywithink.com","coniomi.com","paywithnk.com","paywithlnk.com","iluminatto.com.br","pundix.eu","xn--bttrx-esay.com","xn--bttrex-w8a.com","xn--bnance-bwa.com","xn--shpeshift-11a.com","xn--shapeshif-ts6d.com","xn--shapshift-yf7d.com","wwwbluzelle.com","bluzelie.com","nucleus-vision.org","omisegonetwork.site","etlherzero.com","etlherdelta.com","xn--condesk-0ya.com","xn--condesk-sfb.com","xn--coindsk-vs4c.com","iexecplatform.com","tongramico.com","nucleus-vision.eu","intchain.network","wanchain.cloud","bluzelle-ico.com","ethzero-wallet.com","xn--metherwalle-jb9et7d.com","xn--coinesk-jo3c.com","venchainfoundation.com","myenhtersvvailot.com","ether-zero.net","ins.foundation","nastoken.org","telcointoken.com","ether0.org","eterzero.org","bluzelle-ico.eu","bleuzelle.com","appcoinstoken.org","xn--quanstamp-8s6d.com","myehntersvvailct.com","myeherwalllet.com","ico-bluzelle.com","bluzelle.im","bluzelle.one","bluzele.sale","bluzele.co","sether.ws","xn--myetherwalet-6gf.com","xn--rnyethewaliet-om1g.com","rnyethervailet.com","mvetherwaliet.com","rnyetherwailet.com","myethervaliet.com","rnyethervaliet.com","mvetherwalilet.com","xn--myethewalie-3ic0947g.com","xn--mthrwallet-z6ac3y.com","xn--myeherwalie-vici.com","xn--myethervvalie-8vc.com","xn--mythrwallt-06acf.com","xn--mtherwallet-y9a6y.com","myetherwallet.applytoken.tk","ethereum-zero.com","quanstamptoken.tk","bluzelle.network","ether-wallet.org","tron-wallet.info","appcoinsproject.com","vechain.foundation","tronlab.site","tronlabs.network","bluzelle.cc","ethblender.com","ethpaperwallet.net","waltontoken.org","icoselfkey.org","etherzeroclaim.com","etherzero.promo","bluzelle.pro","token-selfkey.org","xn--etherdlta-0f7d.com","sether.in","xn--ttrex-ysa9423c.com","bluzelle.eu","bluzelle.site","gifto.tech","xn--os-g7s.com","selfkey.co","xn--myeherwalet-ns8exy.com","xn--coinelegraph-wk5f.com","dai-stablecoin.com","eos-token.org","venchain.org","gatcoins.io","deepbrainchain.co","myetherwalililet.info","myehvterwallet.com","myehterumswallet.com","nucleusico.com","tronlab.tech","0x-project.com","gift-token-events.mywebcommunity.org","funfairtoken.org","breadtokenapp.com","cloudpetstore.com","myethwalilet.com","selfkeys.org","wallet-ethereum.com","xn--methrwallt-26ar0z.com","xn--mytherwllet-r8a0c.com","bluzelle.promo","tokensale.bluzelle.promo","cedarlake.org","marketingleads4u.com","cashaa.co","xn--inance-hrb.com","wanchain.tech","zenprolocol.com","ethscan.io","etherscan.in","props-project.com","zilliaq.com","reqestnetwork.com","etherdelta.pw","ethereum-giveaway.org","mysimpletoken.org","binancc.com","blnance.org","elherdelta.io","xn--hapeshit-ez9c2y.com","tenxwallet.co","singularitynet.info","mytlherwaliet.info","iconmainnet.ml","tokenselfkey.org","xn--myetewallet-cm8e5y.com","envione.org","myetherwalletet.com","claimbcd.com","ripiocreditnetwork.in","xn--yeterwallet-ml8euo.com","ethclassicwallet.info","myltherwallet.ru.com","etherdella.com","xn--yeterwallet-bm8ewn.com","singularty.net","cloudkitties.co","iconfoundation.io","kittystat.com","gatscoin.io","singularitynet.in","sale.canay.io","canay.io","wabicoin.co","envion.top","sirinslabs.com","tronlab.co","paxful.com.ng","changellyli.com","ethereum-code.com","xn--plonex-6va6c.com","envion.co","envion.cc","envion.site","ethereumchain.info","xn--envon-1sa.org","xn--btstamp-rfb.net","envlon.org","envion-ico.org","spectivvr.org","sirinlbs.com","ethereumdoubler.life","xn--myetherwllet-fnb.com","sirin-labs.com","sirin-labs.org","envion.one","envion.live","propsproject.org","propsprojects.com","decentralland.org","xn--metherwalet-ns8ep4b.com","redpulsetoken.co","propsproject.tech","xn--myeterwalet-nl8emj.com","powrerledger.com","cryptokitties.com","sirinlabs.pro","sirinlabs.co","sirnlabs.com","superbitcoin-blockchain.info","hellobloom.me","mobus.network","powrrledger.com","xn--myeherwalet-ms8eyy.com","qlink-ico.com","gatcoin.in","tokensale.gamefllp.com","gamefllp.com","xn--myeherwalle-vici.com","xn--myetherwalet-39b.com","xn--polonex-ffb.com","xn--birex-leba.com","raiden-network.org","sirintabs.com","xn--metherwallt-79a30a.com","xn--myethrwllet-2kb3p.com","myethlerwallet.eu","xn--btrex-b4a.com","powerrledger.com","xn--cointeegraph-wz4f.com","myerherwalet.com","qauntstanp.com","myetherermwallet.com","xn--myethewalet-ns8eqq.com","xn--nvion-hza.org","nnyetherwallelt.ru.com","ico-wacoin.com","xn--myeterwalet-nl8enj.com","bitcoinsilver.io","t0zero.com","tokensale.gizer.in","gizer.in","wabitoken.com","gladius.ws","xn--metherwallt-8bb4w.com","quanttstamp.com","gladius.im","ethereumstorage.net","powerledgerr.com","xn--myeherwallet-4j5f.com","quamtstamp.com","quntstamp.com","xn--changely-j59c.com","shapeshlft.com","coinbasenews.co.uk","xn--metherwallet-hmb.com","envoin.org","powerledger.com","bitstannp.net","xn--myetherallet-4k5fwn.com","xn--coinbas-pya.com","requestt.network","oracls.network","sirinlabs.website","powrledger.io","slackconfirm.com","shape-shift.io","oracles-network.org","xn--myeherwalle-zb9eia.com","blockstack.one","urtust.io","bittrex.one","t0-ico.com","xn--cinbase-90a.com","xn--metherwalet-ns8ez1g.com","tzero-ico.com","tzero.su","tzero.website","blockstack.network","ico-tzero.com","spectre.site","tzero.pw","spectre-ai.net","xn--waxtokn-y8a.com","dmarket.pro","bittrex.com11648724328774.cf","bittrex.com1987465798.ga","autcus.org","t-zero.org","xn--zero-zxb.com","myetherwalletfork.com","blokclbain.info","datum.sale","spectre-ai.org","powerledgr.com","simpletoken.live","sale.simpletoken.live","qauntstamp.com","raiden-network.com","metalpayme.com","quantstamp-ico.com","myetherwailetclient.com","biockchain.biz","wallets-blockchain.com","golemairdrop.com","omisegoairdrop.net","blodkchainwallet.info","walton-chain.org","elite888-ico.com","bitflyerjp.com","chainlinksmartcontract.com","stormtoken.eu","omise-go.tech","saltending.com","stormltoken.com","xn--quanttamp-42b.com","stormtoken.co","storntoken.com","stromtoken.com","storm-token.com","stormtokens.io","ether-delta.com","ethconnect.live","ethconnect.trade","xn--bttrex-3va.net","quantstamp.com.co","wancha.in","augur-network.com","quantstamp.com.ua","myetherwalletmew.com","myetherumwalletts.com","xn--quanstamp-tmd.com","quantsstamps.com","changellyl.net","xn--myetherwalet-1fb.com","myethereumwallets.com","xn--myetherwalet-e9b.com","quantslamp.com","metelpay.com","xn--eterdelta-m75d.com","linksmartcontract.com","myetherwalletaccess.com","myetherwalletcheck.com","myetherwalletcheck.info","myetherwalletconf.com","myetherwalleteal.com","myetherwalletec.com","myetherwalletgeth.com","myetherwalletmetamask.com","myetherwalletmm.com","myetherwalletmy.com","myetherwalletnh.com","myetherwalletnod.com","myetherwalletrr.com","myetherwalletrty.com","myetherwalletsec.com","myetherwalletsecure.com","myetherwalletutc.com","myetherwalletver.info","myetherwalletview.com","myetherwalletview.info","myetherwalletvrf.com","myetherwalletmist.com","myetherwalletext.com","myetherwalletjson.com","mettalpay.com","bricklblock.io","bittrexy.com","utrust.so","myethierwallet.org","metallpay.com","kraken-wallet.com","dmarkt.io","etherdeltla.com","unlversa.io","universa.sale","mercuryprotocol.live","ripiocredlt.network","myetlherwa11et.com","dentacoin.in","rdrtg.com","myetherwallet.com.rdrgh.com","rdrgh.com","ripiocreditnetwork.co","riaden.network","hydrominer.biz","rdrblock.com","reqest.network","senstoken.com","myetherwallat.services","ripiocredit.net","xn--metherwallet-c06f.com","ico.ripiocredits.com","ripiocredits.com","raidens.network","artoken.co","myetherwalletlgn.com","etherblog.click","stormtoken.site","httpmyetherwallet.com","myetherwalletverify.com","byzantiumfork.com","myetherwallet.com.byzantiumfork.com","www-myethervvallet.com","ether24.info","block-v.io","bittrex.cash","shapishift.io","ripiocerdit.network","rnyetherwa11et.com","claimether.com","enigmatokensale.com","ethereum-org.com","mvetnerwallet.com","myctherwallet.com","myetherwaltet.com","myetherwatlet.com","privatix.me","myetherwalletcnf.com","myetherwalletver.com","privatix.top","privatix.pro","privatex.io","stormtoken.cc","raiden.online","stormstoken.com","myetereumwallet.com","stormtokens.net","myetherwalletconf.info","storrntoken.com","worldofbattles.io","ico.worldofbattles.io","privatix.live","riden.network","raidan.network","ralden.network","mymyetherwallet.com","myetherwallets.net","myetherwalletverify.info","stormxtoken.com","myethereum-wallet.com","myetherwallet-forkprep.pagedemo.co","myetnerwailet.com","www-mvetherwallet.com","etheirdelta.com","myetherwalletiu.com","myetherwaiiett.com","xn--mytherwalet-cbb87i.com","xn--myethrwallet-ivb.co","xn--myeterwallet-f1b.com","myehterwaliet.com","omegaone.co","myetherwaiietw.com","slack.com.ru","polkodot.network","request-network.net","requestnetwork.live","binancie.com","first-eth.info","myewerthwalliet.com","enjincoin.pw","xn--bitrex-k17b.com","alrswap.io","www-request.network","myetnenwallet.com","www-enigma.co","cryptoinsidenews.com","air-swap.tech","launch.airswap.cc","airswap.cc","airswaptoken.com","launch.airswap.in","airswap.in","security-steemit.com.mx","blockchalnwallet.com","blodkchainwallet.com","blodkchaln.com","myethereumwaiiet.com","myethereumwaliet.com","myethereumwalilet.com","myetherswailet.com","myetherswaliet.com","myetherswalilet.com","myetherwalilett.com","myetherwalletl.com","myetherwalletww.com","myethereunwallet.com","myethereumwallct.com","myetherwaiieti.com","myetherwaiiete.com","upfirng.com","paypie.net","paypie.tech","soam.co","myetherwaiict.com","numerai-token.com","www-bankera.com","vvanchain.org","omisegoairdrop.com","xn--enjncoin-41a.io","suncontract.su","myetherwaiietr.com","shapeshiff.io","warchain.org","myethwallett.com","myethervvaliet.com","wanchains.org","etherparty.in","enjincoin.me","etiam.io","invest.smartlands.tech","smartlands.tech","enijncoin.io","wanchain.network","nimiq.su","enjincoin.sale","tenxwallet.io","golem-network.net","myyethwallet.ml","mywetherwailiet.com","omg-omise.com","district0x.tech","centra-token.com","etherdetla.com","etnerparty.io","etherdelta.su","myetherwallett.neocities.org","myetherwallet-secure.com","myethereumwalletntw.info","real-markets.io","wallet-ethereum.org","request-network.com","shapeshifth.io","shiapeshift.in","coin.red-puise.com","ibittreix.com","coinkbase.com","cindicator.pro","myetherwallet.com.ailogin.me","eventchain.co","kinkik.in","myetherumwalletview.com","protostokenhub.com","coinrbase.com","myetherwalletlogin.com","omisegotoken.com","myethereumwalletntw.com","reall.markets","cobinhood.org","cobinhood.io","happy-coin.org","bitfinex.com.co","bitfienex.com","iconn.foundation","centra.vip","smartcontract.live","icon.community","air-token.com","centra.credit","myetherwallet-singin.com","smartcontractlink.com","shapesshift.io","0xtoken.io","augurproject.co","ethereumus.one","myetherumwalet.com","myetherwalletsignin.com","change-bank.org","charge-bank.com","myetherwalletsingin.com","myetherwalletcontract.com","change-bank.io","chainlink.tech","myetherwallet-confirm.com","tokensale.kybernet.network","kybernet.network","kyberr.network","kybernetwork.io","myetherwalletconfirm.com","kvnuke.github.io","kin.kikpro.co","myethereumwallet.co.uk","tokensale-kyber.network","kyber-network.co","tokensale.kyber-network.co","pyro0.github.io","tokensale.kyber.digital","kyber.digital","omise-go.me","my.etherwallet.com.de","bepartof.change-bank.co","change-bank.co","enigma-tokens.co","coinbase.com.eslogin.co","xn--bittrx-mva.com","ethrdelta.github.io","etherdellta.com","ico-nexus.social","red-pulse.tech","bitj0b.io","xn--bttrex-bwa.com","kin-klk.com","kin-crowdsale.com","ethedelta.com","coindash.su","myethwallet.co.uk","swarm.credit","myethereumwallet.uk","iconexu.social","wanchain.co","enigrna.co","linknetwork.co","qtum-token.com","omisego.com.co","rivetzintl.org","etherdelta.one","the-ether.pro","etherdelta.gitnub.io","kirkik.com","monetha.ltd","vlberate.io","ethereumwallet-kr.info","omise-go.org","iconexus.social","bittirrex.com","aventus.pro","atlant.solutions","aventus.group","metamak.io","omise.com.co","herotokens.io","starbase.pro","etherdelta.githulb.io","herotoken.co","kinico.net","dmarket.ltd","etherdelta.gilthub.io","golem-network.com","etnerscan.io","bllttriex.com","monetha.me","monetha.co","monetha-crowdsale.com","starbase.tech","aventus-crowdsale.com","shapeshift.pro","bllttrex.com","kickico.co","statustoken.im","bilttrex.com","tenxpay.io","bittrex.ltd","metalpay.im","aragon.im","coindash.tech","decentraland.tech","decentraland.pro","status-token.com","bittrex.cam","enigmatoken.com","unocoin.company","unocoin.fund","0xproject.io","0xtoken.com","numerai.tech","decentraiand.org","blockcrein.info","blockchealn.info","bllookchain.info","blockcbhain.info","myetherwallet.com.ethpromonodes.com","mettamask.io","tokenswap.org","netherum.com","etherexx.org","etherume.io","ethereum.plus","ehtereum.org","etereurm.org","etheream.com","ethererum.org","ethereum.io","etherdelta-glthub.com","cryptoalliance.herokuapp.com","bitspark2.com","indorsetoken.com","iconexus.tk","iconexus.ml","iconexus.ga","iconexus.cf","etherwallet.online","wallet-ethereum.net","bitsdigit.com","etherswap.org","eos.ac","uasfwallet.com","ziber.io","multiply-ethereum.info","bittrex.comze.com","karbon.vacau.com","etherdelta.gitlhub.io","etherdelta.glthub.io","digitaldevelopersfund.vacau.com","district-0x.io","coin-dash.com","coindash.ru","district0x.net","aragonproject.io","coin-wallet.info","coinswallet.info","contribute-status.im","ether-api.com","ether-wall.com","mycoinwallet.net","ethereumchamber.com","ethereumchamber.net","ethereumchest.com","ethewallet.com","myetherwallet.com.vc","myetherwallet.com.pe","myetherwallet.us.com","myetherwallet.com.u0387831.cp.regruhosting.ru","myethereumwallet.su","myetherweb.com.de","myetherieumwallet.com","myetehrwallet.com","myeterwalet.com","myetherwaiiet.com","myetherwallet.info","myetherwallet.ch","myetherwallet.om","myethervallet.com","myetherwallet.com.cm","myetherwallet.com.co","myetherwallet.com.de","myetherwallet.com.gl","myetherwallet.com.im","myetherwallet.com.ua","secure-myetherwallet.com","update-myetherwallet.com","wwwmyetherwallet.com","myeatherwallet.com","myetharwallet.com","myelherwallel.com","myetherwaillet.com","myetherwaliet.com","myetherwallel.com","myetherwallet.cam","myetherwallet.cc","myetherwallet.co","myetherwallet.cm","myetherwallet.cz","myetherwallet.org","myetherwallet.tech","myetherwallet.top","myetherwallet.net","myetherwallet.ru.com","myetherwallet.com.ru","metherwallet.com","myetrerwallet.com","myetlerwallet.com","myethterwallet.com","myethwallet.io","myethterwallet.co","myehterwallet.co","myaetherwallet.com","myetthterwallet.com","myetherwallet.one","myelterwallet.com","myetherwallet.gdn","myetherwallt.com","myeterwallet.com","myeteherwallet.com","myethearwailet.com","myetherwallelt.com","myetherwallett.com","etherwallet.org","myetherewallet.com","myeherwallet.com","myethcrwallet.com","myetherwallet.link","myetherwallets.com","myethearwaillet.com","myethearwallet.com","myetherawllet.com","myethereallet.com","myetherswallet.com","myetherwalet.com","myetherwaller.com","myetherwalliet.com","myetherwllet.com","etherwallet.io","myetherwallet.ca","myetherwallet.me","myetherwallet.ru","myetherwallet.xyz","myetherwallte.com","myethirwallet.com","myethrewallet.com","etherwallet.net","maetherwallet.com","meyetherwallet.com","my.ether-wallet.pw","myehterwallet.com","myeitherwallet.com","myelherwallet.com","myeltherwallet.com","myerherwallet.com","myethearwalet.com","myetherewalle.com","myethervvallet.com","myetherwallent.com","myetherwallet.fm","myetherwalllet.com","myetherwalltet.com","myetherwollet.com","myetlherwalet.com","myetlherwallet.com","rnyetherwallet.com","etherclassicwallet.com","omg-omise.co","omise-go.com","omise-go.net","omise-omg.com","omise-go.io","tenx-tech.com","bitclaive.com","tokensale-tenx.tech","ubiqcoin.org","metamask.com","ethtrade.io","myetcwallet.com","account-kigo.net","bitcoin-wallet.net","blocklichan.info","bloclkicihan.info","coindash.ml","eos-bonus.com","eos-io.info","ether-wallet.net","ethereum-wallet.info","ethereum-wallet.net","ethereumchest.net","reservations-kigo.net","reservations-lodgix.com","secure-liverez.com","secure-onerooftop.com","settings-liverez.com","software-liverez.com","software-lodgix.com","unhackableetherwallets.com","www-myetherwallet.com","etherwallet.co.za","etherwalletchain.com","etherwallets.net","etherwallets.nl","my-ethwallet.com","my.ether-wallet.co","myetherwallet.com.am","myetherwallet.com.ht","myetherwalletcom.com","myehterwailet.com","xn--myetherwalle-xoc.com","xn--myetherwalle-44i.com","xn--myetherwalle-xhk.com","xn--myetherwallt-cfb.com","xn--myetherwallt-6tb.com","xn--myetherwallt-xub.com","xn--myetherwallt-ovb.com","xn--myetherwallt-fwb.com","xn--myetherwallt-5wb.com","xn--myetherwallt-jzi.com","xn--myetherwallt-2ck.com","xn--myetherwallt-lok.com","xn--myetherwallt-lsl.com","xn--myetherwallt-ce6f.com","xn--myetherwalet-mcc.com","xn--myetherwalet-xhf.com","xn--myetherwalet-lcc.com","xn--myetherwaet-15ba.com","xn--myetherwalet-whf.com","xn--myetherwaet-v2ea.com","xn--myetherwllet-59a.com","xn--myetherwllet-jbb.com","xn--myetherwllet-wbb.com","xn--myetherwllet-9bb.com","xn--myetherwllet-ncb.com","xn--myetherwllet-0cb.com","xn--myetherwllet-5nb.com","xn--myetherwllet-ktd.com","xn--myetherwllet-mre.com","xn--myetherwllet-76e.com","xn--myetherwllet-o0l.com","xn--myetherwllet-c45f.com","xn--myetherallet-ejn.com","xn--myethewallet-4nf.com","xn--myethewallet-iof.com","xn--myethewallet-mpf.com","xn--myethewallet-6bk.com","xn--myethewallet-i31f.com","xn--myethrwallet-feb.com","xn--myethrwallt-fbbf.com","xn--myethrwallet-seb.com","xn--myethrwallt-rbbf.com","xn--myethrwallet-5eb.com","xn--myethrwallt-3bbf.com","xn--myethrwallet-0tb.com","xn--myethrwallt-tpbf.com","xn--myethrwallet-rub.com","xn--myethrwallt-iqbf.com","xn--myethrwallet-ivb.com","xn--myethrwallt-6qbf.com","xn--myethrwallet-8vb.com","xn--myethrwallt-vrbf.com","xn--myethrwallet-zwb.com","xn--myethrwallt-ksbf.com","xn--myethrwallet-dzi.com","xn--myethrwallt-wbif.com","xn--myethrwallet-wck.com","xn--myethrwallt-skjf.com","xn--myethrwallet-fok.com","xn--myethrwallt-fvjf.com","xn--myethrwallet-fsl.com","xn--myethrwallt-fwkf.com","xn--myethrwallet-5d6f.com","xn--myethrwallt-319ef.com","xn--myeterwallet-ufk.com","xn--myeterwallet-nrl.com","xn--myeterwallet-von.com","xn--myeterwallet-jl6c.com","xn--myeherwallet-ooc.com","xn--myeherwalle-6hci.com","xn--myeherwallet-v4i.com","xn--myeherwalle-zgii.com","xn--myeherwallet-ohk.com","xn--myeherwalle-6oji.com","xn--mytherwallet-ceb.com","xn--mythrwallet-cbbc.com","xn--mythrwallt-c7acf.com","xn--mytherwallet-peb.com","xn--mythrwallet-obbc.com","xn--mythrwallt-n7acf.com","xn--mytherwallet-2eb.com","xn--mythrwallet-0bbc.com","xn--mythrwallt-y7acf.com","xn--mytherwallet-xtb.com","xn--mythrwallet-qpbc.com","xn--mythrwallt-jlbcf.com","xn--mytherwallet-oub.com","xn--mythrwallet-fqbc.com","xn--mythrwallt-5lbcf.com","xn--mythrwallet-3qbc.com","xn--mythrwallt-smbcf.com","xn--mytherwallet-5vb.com","xn--mythrwallet-srbc.com","xn--mythrwallt-fnbcf.com","xn--mytherwallet-wwb.com","xn--mythrwallet-hsbc.com","xn--mythrwallt-1nbcf.com","xn--mytherwallet-9yi.com","xn--mythrwallet-tbic.com","xn--mythrwallt-dnhcf.com","xn--mytherwallet-tck.com","xn--mythrwallet-pkjc.com","xn--mythrwallt-lsicf.com","xn--mytherwallet-cok.com","xn--mythrwallet-cvjc.com","xn--mythrwallt-c2icf.com","xn--mytherwallet-csl.com","xn--mythrwallet-cwkc.com","xn--mythrwallt-c0jcf.com","xn--mytherwallet-2d6f.com","xn--mythrwallet-019ec.com","xn--mythrwallt-yq3ecf.com","xn--metherwallet-qlb.com","xn--metherwallet-1uf.com","xn--metherwallet-iyi.com","xn--metherwallet-zhk.com","xn--metherwallet-3ml.com","xn--mytherwallet-fvb.com","xn--myetherwallt-7db.com","xn--myetherwallt-leb.com","xn--myetherwallt-yeb.com","xn--yetherwallet-vjf.com","xn--yetherwallet-dfk.com","xn--yetherwallet-1t1f.com","xn--yetherwallet-634f.com","xn--myeherwallet-fpc.com","xn--myethewallt-crb.com","xn--metherwallet-1vc.com","xn--myeherwallt-kbb8039g.com","xn--myeherwallet-vk5f.com","xn--yethewallet-iw8ejl.com","xn--bittrx-th8b.com","xn--polniex-n0a.com","thekey.vin","thekey-vip.com","digitexftures.com","ethzero-wallet.org","zeepln.io","wepowers.network","wepower.vision"]}},"CurrencyController":{"currentCurrency":"usd","conversionRate":1112,"conversionDate":1517351401}}}
diff --git a/test/unit/actions/tx_test.js b/test/unit/actions/tx_test.js
index c110f71fc..160cd4552 100644
--- a/test/unit/actions/tx_test.js
+++ b/test/unit/actions/tx_test.js
@@ -1,74 +1,54 @@
-// var jsdom = require('mocha-jsdom')
var assert = require('assert')
-var freeze = require('deep-freeze-strict')
var path = require('path')
-var sinon = require('sinon')
-var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js'))
-var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js'))
+import configureMockStore from 'redux-mock-store'
+import thunk from 'redux-thunk'
-describe('tx confirmation screen', function () {
- beforeEach(function () {
- this.sinon = sinon.createSandbox()
- })
-
- afterEach(function () {
- this.sinon.restore()
- })
+const actions = require(path.join(__dirname, '../../../ui/app/actions.js'))
- var initialState, result
+const middlewares = [thunk]
+const mockStore = configureMockStore(middlewares)
- describe('when there is only one tx', function () {
- var firstTxId = 1457634084250832
-
- beforeEach(function () {
- initialState = {
- appState: {
- currentView: {
- name: 'confTx',
- },
- },
- metamask: {
- unapprovedTxs: {
- '1457634084250832': {
- id: 1457634084250832,
- status: 'unconfirmed',
- time: 1457634084250,
- },
- },
+describe('tx confirmation screen', function () {
+ const txId = 1457634084250832
+ const initialState = {
+ appState: {
+ currentView: {
+ name: 'confTx',
+ },
+ },
+ metamask: {
+ unapprovedTxs: {
+ [txId]: {
+ id: txId,
+ status: 'unconfirmed',
+ time: 1457634084250,
},
- }
- freeze(initialState)
+ },
+ },
+ }
+
+ const store = mockStore(initialState)
+
+ describe('cancelTx', function () {
+ before(function (done) {
+ actions._setBackgroundConnection({
+ approveTransaction (txId, cb) { cb('An error!') },
+ cancelTransaction (txId, cb) { cb() },
+ clearSeedWordCache (cb) { cb() },
+ getState (cb) { cb() },
+ })
+ done()
})
- describe('cancelTx', function () {
- before(function (done) {
- actions._setBackgroundConnection({
- approveTransaction (txId, cb) { cb('An error!') },
- cancelTransaction (txId, cb) { cb() },
- clearSeedWordCache (cb) { cb() },
+ it('creates COMPLETED_TX with the cancelled transaction ID', function (done) {
+ store.dispatch(actions.cancelTx({ id: txId }))
+ .then(() => {
+ const storeActions = store.getActions()
+ const completedTxAction = storeActions.find(({ type }) => type === actions.COMPLETED_TX)
+ assert.equal(completedTxAction.value, txId)
+ done()
})
-
- actions.cancelTx({value: firstTxId})((action) => {
- result = reducers(initialState, action)
- })
- done()
- })
-
- it('should transition to the account detail view', function () {
- assert.equal(result.appState.currentView.name, 'accountDetail')
- })
-
- it('should have no unconfirmed txs remaining', function () {
- var count = getUnconfirmedTxCount(result)
- assert.equal(count, 0)
- })
})
})
})
-
-function getUnconfirmedTxCount (state) {
- var txs = state.metamask.unapprovedTxs
- var count = Object.keys(txs).length
- return count
-}
diff --git a/test/unit/app/controllers/detect-tokens-test.js b/test/unit/app/controllers/detect-tokens-test.js
new file mode 100644
index 000000000..426ffe23a
--- /dev/null
+++ b/test/unit/app/controllers/detect-tokens-test.js
@@ -0,0 +1,141 @@
+const assert = require('assert')
+const sinon = require('sinon')
+const ObservableStore = require('obs-store')
+const DetectTokensController = require('../../../../app/scripts/controllers/detect-tokens')
+const NetworkController = require('../../../../app/scripts/controllers/network/network')
+const PreferencesController = require('../../../../app/scripts/controllers/preferences')
+
+describe('DetectTokensController', () => {
+ const sandbox = sinon.createSandbox()
+ let clock
+ let keyringMemStore
+ before(async () => {
+ keyringMemStore = new ObservableStore({ isUnlocked: false})
+ })
+ after(() => {
+ sandbox.restore()
+ })
+
+ it('should poll on correct interval', async () => {
+ const stub = sinon.stub(global, 'setInterval')
+ new DetectTokensController({ interval: 1337 }) // eslint-disable-line no-new
+ assert.strictEqual(stub.getCall(0).args[1], 1337)
+ stub.restore()
+ })
+
+ it('should be called on every polling period', async () => {
+ clock = sandbox.useFakeTimers()
+ const network = new NetworkController()
+ network.setProviderType('mainnet')
+ const preferences = new PreferencesController()
+ const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
+ controller.isOpen = true
+ controller.isUnlocked = true
+
+ var stub = sandbox.stub(controller, 'detectNewTokens')
+
+ clock.tick(1)
+ sandbox.assert.notCalled(stub)
+ clock.tick(180000)
+ sandbox.assert.called(stub)
+ clock.tick(180000)
+ sandbox.assert.calledTwice(stub)
+ clock.tick(180000)
+ sandbox.assert.calledThrice(stub)
+ })
+
+ it('should not check tokens while in test network', async () => {
+ const network = new NetworkController()
+ network.setProviderType('rinkeby')
+ const preferences = new PreferencesController()
+ const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
+ controller.isOpen = true
+ controller.isUnlocked = true
+
+ var stub = sandbox.stub(controller, 'detectTokenBalance')
+ .withArgs('0x0D262e5dC4A06a0F1c90cE79C7a60C09DfC884E4').returns(true)
+ .withArgs('0xBC86727E770de68B1060C91f6BB6945c73e10388').returns(true)
+
+ await controller.detectNewTokens()
+ sandbox.assert.notCalled(stub)
+ })
+
+ it('should only check and add tokens while in main network', async () => {
+ const network = new NetworkController()
+ network.setProviderType('mainnet')
+ const preferences = new PreferencesController()
+ const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
+ controller.isOpen = true
+ controller.isUnlocked = true
+
+ sandbox.stub(controller, 'detectTokenBalance')
+ .withArgs('0x0D262e5dC4A06a0F1c90cE79C7a60C09DfC884E4')
+ .returns(preferences.addToken('0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', 'J8T', 8))
+ .withArgs('0xBC86727E770de68B1060C91f6BB6945c73e10388')
+ .returns(preferences.addToken('0xbc86727e770de68b1060c91f6bb6945c73e10388', 'XNK', 18))
+
+ await controller.detectNewTokens()
+ assert.deepEqual(preferences.store.getState().tokens, [{address: '0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', decimals: 8, symbol: 'J8T'},
+ {address: '0xbc86727e770de68b1060c91f6bb6945c73e10388', decimals: 18, symbol: 'XNK'}])
+ })
+
+ it('should not detect same token while in main network', async () => {
+ const network = new NetworkController()
+ network.setProviderType('mainnet')
+ const preferences = new PreferencesController()
+ preferences.addToken('0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', 'J8T', 8)
+ const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
+ controller.isOpen = true
+ controller.isUnlocked = true
+
+ sandbox.stub(controller, 'detectTokenBalance')
+ .withArgs('0x0D262e5dC4A06a0F1c90cE79C7a60C09DfC884E4')
+ .returns(preferences.addToken('0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', 'J8T', 8))
+ .withArgs('0xBC86727E770de68B1060C91f6BB6945c73e10388')
+ .returns(preferences.addToken('0xbc86727e770de68b1060c91f6bb6945c73e10388', 'XNK', 18))
+
+ await controller.detectNewTokens()
+ assert.deepEqual(preferences.store.getState().tokens, [{address: '0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', decimals: 8, symbol: 'J8T'},
+ {address: '0xbc86727e770de68b1060c91f6bb6945c73e10388', decimals: 18, symbol: 'XNK'}])
+ })
+
+ it('should trigger detect new tokens when change address', async () => {
+ const network = new NetworkController()
+ network.setProviderType('mainnet')
+ const preferences = new PreferencesController()
+ const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
+ controller.isOpen = true
+ controller.isUnlocked = true
+ var stub = sandbox.stub(controller, 'detectNewTokens')
+ await preferences.setSelectedAddress('0xbc86727e770de68b1060c91f6bb6945c73e10388')
+ sandbox.assert.called(stub)
+ })
+
+ it('should trigger detect new tokens when submit password', async () => {
+ const network = new NetworkController()
+ network.setProviderType('mainnet')
+ const preferences = new PreferencesController()
+ const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
+ controller.isOpen = true
+ controller.selectedAddress = '0x0'
+ var stub = sandbox.stub(controller, 'detectNewTokens')
+ await controller._keyringMemStore.updateState({ isUnlocked: true })
+ sandbox.assert.called(stub)
+ })
+
+ it('should not trigger detect new tokens when not open or not unlocked', async () => {
+ const network = new NetworkController()
+ network.setProviderType('mainnet')
+ const preferences = new PreferencesController()
+ const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore })
+ controller.isOpen = true
+ controller.isUnlocked = false
+ var stub = sandbox.stub(controller, 'detectTokenBalance')
+ clock.tick(180000)
+ sandbox.assert.notCalled(stub)
+ controller.isOpen = false
+ controller.isUnlocked = true
+ clock.tick(180000)
+ sandbox.assert.notCalled(stub)
+ })
+})
diff --git a/test/unit/app/controllers/metamask-controller-test.js b/test/unit/app/controllers/metamask-controller-test.js
index cbd949e69..0f51ed6cb 100644
--- a/test/unit/app/controllers/metamask-controller-test.js
+++ b/test/unit/app/controllers/metamask-controller-test.js
@@ -224,6 +224,129 @@ describe('MetaMaskController', function () {
})
})
+ describe('connectHardware', function () {
+
+ it('should throw if it receives an unknown device name', async function () {
+ try {
+ await metamaskController.connectHardware('Some random device name', 0)
+ } catch (e) {
+ assert.equal(e, 'Error: MetamaskController:connectHardware - Unknown device')
+ }
+ })
+
+ it('should add the Trezor Hardware keyring', async function () {
+ sinon.spy(metamaskController.keyringController, 'addNewKeyring')
+ await metamaskController.connectHardware('trezor', 0).catch((e) => null)
+ const keyrings = await metamaskController.keyringController.getKeyringsByType(
+ 'Trezor Hardware'
+ )
+ assert.equal(metamaskController.keyringController.addNewKeyring.getCall(0).args, 'Trezor Hardware')
+ assert.equal(keyrings.length, 1)
+ })
+
+ })
+
+ describe('checkHardwareStatus', function () {
+ it('should throw if it receives an unknown device name', async function () {
+ try {
+ await metamaskController.checkHardwareStatus('Some random device name')
+ } catch (e) {
+ assert.equal(e, 'Error: MetamaskController:checkHardwareStatus - Unknown device')
+ }
+ })
+
+ it('should be locked by default', async function () {
+ await metamaskController.connectHardware('trezor', 0).catch((e) => null)
+ const status = await metamaskController.checkHardwareStatus('trezor')
+ assert.equal(status, false)
+ })
+ })
+
+ describe('forgetDevice', function () {
+ it('should throw if it receives an unknown device name', async function () {
+ try {
+ await metamaskController.forgetDevice('Some random device name')
+ } catch (e) {
+ assert.equal(e, 'Error: MetamaskController:forgetDevice - Unknown device')
+ }
+ })
+
+ it('should wipe all the keyring info', async function () {
+ await metamaskController.connectHardware('trezor', 0).catch((e) => null)
+ await metamaskController.forgetDevice('trezor')
+ const keyrings = await metamaskController.keyringController.getKeyringsByType(
+ 'Trezor Hardware'
+ )
+
+ assert.deepEqual(keyrings[0].accounts, [])
+ assert.deepEqual(keyrings[0].page, 0)
+ assert.deepEqual(keyrings[0].isUnlocked(), false)
+ })
+ })
+
+ describe('unlockTrezorAccount', function () {
+ let accountToUnlock
+ let windowOpenStub
+ let addNewAccountStub
+ let getAccountsStub
+ beforeEach(async function () {
+ accountToUnlock = 10
+ windowOpenStub = sinon.stub(window, 'open')
+ windowOpenStub.returns(noop)
+
+ addNewAccountStub = sinon.stub(metamaskController.keyringController, 'addNewAccount')
+ addNewAccountStub.returns({})
+
+ getAccountsStub = sinon.stub(metamaskController.keyringController, 'getAccounts')
+ // Need to return different address to mock the behavior of
+ // adding a new account from the keyring
+ getAccountsStub.onCall(0).returns(Promise.resolve(['0x1']))
+ getAccountsStub.onCall(1).returns(Promise.resolve(['0x2']))
+ getAccountsStub.onCall(2).returns(Promise.resolve(['0x3']))
+ getAccountsStub.onCall(3).returns(Promise.resolve(['0x4']))
+ sinon.spy(metamaskController.preferencesController, 'setAddresses')
+ sinon.spy(metamaskController.preferencesController, 'setSelectedAddress')
+ sinon.spy(metamaskController.preferencesController, 'setAccountLabel')
+ await metamaskController.connectHardware('trezor', 0).catch((e) => null)
+ await metamaskController.unlockTrezorAccount(accountToUnlock).catch((e) => null)
+ })
+
+ afterEach(function () {
+ metamaskController.keyringController.addNewAccount.restore()
+ window.open.restore()
+ })
+
+ it('should set accountToUnlock in the keyring', async function () {
+ const keyrings = await metamaskController.keyringController.getKeyringsByType(
+ 'Trezor Hardware'
+ )
+ assert.equal(keyrings[0].unlockedAccount, accountToUnlock)
+ })
+
+
+ it('should call keyringController.addNewAccount', async function () {
+ assert(metamaskController.keyringController.addNewAccount.calledOnce)
+ })
+
+ it('should call keyringController.getAccounts ', async function () {
+ assert(metamaskController.keyringController.getAccounts.called)
+ })
+
+ it('should call preferencesController.setAddresses', async function () {
+ assert(metamaskController.preferencesController.setAddresses.calledOnce)
+ })
+
+ it('should call preferencesController.setSelectedAddress', async function () {
+ assert(metamaskController.preferencesController.setSelectedAddress.calledOnce)
+ })
+
+ it('should call preferencesController.setAccountLabel', async function () {
+ assert(metamaskController.preferencesController.setAccountLabel.calledOnce)
+ })
+
+
+ })
+
describe('#setCustomRpc', function () {
let rpcTarget
@@ -355,6 +478,39 @@ describe('MetaMaskController', function () {
})
})
+ describe('#removeAccount', function () {
+ let ret
+ const addressToRemove = '0x1'
+
+ beforeEach(async function () {
+ sinon.stub(metamaskController.preferencesController, 'removeAddress')
+ sinon.stub(metamaskController.accountTracker, 'removeAccount')
+ sinon.stub(metamaskController.keyringController, 'removeAccount')
+
+ ret = await metamaskController.removeAccount(addressToRemove)
+
+ })
+
+ afterEach(function () {
+ metamaskController.keyringController.removeAccount.restore()
+ metamaskController.accountTracker.removeAccount.restore()
+ metamaskController.preferencesController.removeAddress.restore()
+ })
+
+ it('should call preferencesController.removeAddress', async function () {
+ assert(metamaskController.preferencesController.removeAddress.calledWith(addressToRemove))
+ })
+ it('should call accountTracker.removeAccount', async function () {
+ assert(metamaskController.accountTracker.removeAccount.calledWith(addressToRemove))
+ })
+ it('should call keyringController.removeAccount', async function () {
+ assert(metamaskController.keyringController.removeAccount.calledWith(addressToRemove))
+ })
+ it('should return address', async function () {
+ assert.equal(ret, '0x1')
+ })
+ })
+
describe('#clearSeedWordCache', function () {
it('should have set seed words', function () {
diff --git a/test/unit/app/controllers/preferences-controller-test.js b/test/unit/app/controllers/preferences-controller-test.js
index e5e751b57..e055500b1 100644
--- a/test/unit/app/controllers/preferences-controller-test.js
+++ b/test/unit/app/controllers/preferences-controller-test.js
@@ -52,6 +52,31 @@ describe('preferences controller', function () {
})
})
+ describe('removeAddress', function () {
+ it('should remove an address from state', function () {
+ preferencesController.setAddresses([
+ '0xda22le',
+ '0x7e57e2',
+ ])
+
+ preferencesController.removeAddress('0xda22le')
+
+ assert.equal(preferencesController.store.getState().identities['0xda22le'], undefined)
+ })
+
+ it('should switch accounts if the selected address is removed', function () {
+ preferencesController.setAddresses([
+ '0xda22le',
+ '0x7e57e2',
+ ])
+
+ preferencesController.setSelectedAddress('0x7e57e2')
+ preferencesController.removeAddress('0x7e57e2')
+
+ assert.equal(preferencesController.getSelectedAddress(), '0xda22le')
+ })
+ })
+
describe('setAccountLabel', function () {
it('should update a label for the given account', function () {
preferencesController.setAddresses([
diff --git a/test/unit/app/controllers/transactions/tx-controller-test.js b/test/unit/app/controllers/transactions/tx-controller-test.js
index 7eb111cfb..b62499a70 100644
--- a/test/unit/app/controllers/transactions/tx-controller-test.js
+++ b/test/unit/app/controllers/transactions/tx-controller-test.js
@@ -354,9 +354,16 @@ describe('Transaction Controller', function () {
])
})
- it('should set the transaction to rejected from unapproved', async function () {
- await txController.cancelTransaction(0)
- assert.equal(txController.txStateManager.getTx(0).status, 'rejected')
+ it('should emit a status change to rejected', function (done) {
+ txController.once('tx:status-update', (txId, status) => {
+ try {
+ assert.equal(status, 'rejected', 'status should e rejected')
+ assert.equal(txId, 0, 'id should e 0')
+ done()
+ } catch (e) { done(e) }
+ })
+
+ txController.cancelTransaction(0)
})
})
diff --git a/test/unit/app/controllers/transactions/tx-state-manager-test.js b/test/unit/app/controllers/transactions/tx-state-manager-test.js
index 2f91b1545..88bdaa60e 100644
--- a/test/unit/app/controllers/transactions/tx-state-manager-test.js
+++ b/test/unit/app/controllers/transactions/tx-state-manager-test.js
@@ -43,14 +43,13 @@ describe('TransactionStateManager', function () {
})
describe('#setTxStatusRejected', function () {
- it('sets the tx status to rejected', function () {
+ it('sets the tx status to rejected and removes it from history', function () {
const tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
txStateManager.addTx(tx)
txStateManager.setTxStatusRejected(1)
const result = txStateManager.getTxList()
assert.ok(Array.isArray(result))
- assert.equal(result.length, 1)
- assert.equal(result[0].status, 'rejected')
+ assert.equal(result.length, 0)
})
it('should emit a rejected event to signal the exciton of callback', (done) => {
@@ -287,4 +286,18 @@ describe('TransactionStateManager', function () {
})
})
+
+ describe('#_removeTx', function () {
+ it('should remove the transaction from the storage', () => {
+ txStateManager._saveTxList([ {id: 1} ])
+ txStateManager._removeTx(1)
+ assert(!txStateManager.getFullTxList().length, 'txList should be empty')
+ })
+
+ it('should only remove the transaction with ID 1 from the storage', () => {
+ txStateManager._saveTxList([ {id: 1}, {id: 2} ])
+ txStateManager._removeTx(1)
+ assert.equal(txStateManager.getFullTxList()[0].id, 2, 'txList should have a id of 2')
+ })
+ })
})
diff --git a/test/unit/components/pending-tx-test.js b/test/unit/components/pending-tx-test.js
deleted file mode 100644
index c68e013ac..000000000
--- a/test/unit/components/pending-tx-test.js
+++ /dev/null
@@ -1,67 +0,0 @@
-const assert = require('assert')
-const h = require('react-hyperscript')
-const PendingTx = require('../../../ui/app/components/pending-tx')
-const ethUtil = require('ethereumjs-util')
-
-const { createMockStore } = require('redux-test-utils')
-const { shallowWithStore } = require('../../lib/shallow-with-store')
-
-const identities = { abc: {}, def: {} }
-const mockState = {
- metamask: {
- accounts: { abc: {} },
- identities,
- conversionRate: 10,
- selectedAddress: 'abc',
- },
-}
-
-describe('PendingTx', function () {
- const gasPrice = '0x4A817C800' // 20 Gwei
- const txData = {
- 'id': 5021615666270214,
- 'time': 1494458763011,
- 'status': 'unapproved',
- 'metamaskNetworkId': '1494442339676',
- 'txParams': {
- 'from': '0xfdea65c8e26263f6d9a1b5de9555d2931a33b826',
- 'to': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
- 'value': '0xde0b6b3a7640000',
- gasPrice,
- 'gas': '0x7b0c',
- },
- 'gasLimitSpecified': false,
- 'estimatedGas': '0x5208',
- }
- const newGasPrice = '0x77359400'
-
- const computedBalances = {}
- computedBalances[Object.keys(identities)[0]] = {
- ethBalance: '0x00000000000000056bc75e2d63100000',
- }
- const props = {
- txData,
- computedBalances,
- sendTransaction: (txMeta, event) => {
- // Assert changes:
- const result = ethUtil.addHexPrefix(txMeta.txParams.gasPrice)
- assert.notEqual(result, gasPrice, 'gas price should change')
- assert.equal(result, newGasPrice, 'gas price assigned.')
- },
- }
-
- let pendingTxComponent
- let store
- let component
- beforeEach(function () {
- store = createMockStore(mockState)
- component = shallowWithStore(h(PendingTx, props), store)
- pendingTxComponent = component
- })
-
- it('should render correctly', function (done) {
- assert.equal(pendingTxComponent.props().identities, identities)
- done()
- })
-})
-
diff --git a/test/unit/migrations/027-test.js b/test/unit/migrations/027-test.js
new file mode 100644
index 000000000..3ec9f0c0e
--- /dev/null
+++ b/test/unit/migrations/027-test.js
@@ -0,0 +1,50 @@
+const assert = require('assert')
+const migration27 = require('../../../app/scripts/migrations/027')
+
+const oldStorage = {
+ 'meta': {},
+ 'data': {
+ 'TransactionController': {
+ 'transactions': [
+ ],
+ },
+ },
+}
+
+const transactions = []
+
+
+while (transactions.length < 9) {
+ transactions.push({status: 'rejected'})
+ transactions.push({status: 'unapproved'})
+ transactions.push({status: 'approved'})
+}
+
+
+oldStorage.data.TransactionController.transactions = transactions
+
+describe('migration #27', () => {
+ it('should remove rejected transactions', (done) => {
+ migration27.migrate(oldStorage)
+ .then((newStorage) => {
+ const newTransactions = newStorage.data.TransactionController.transactions
+ assert.equal(newTransactions.length, 6, 'transactions is expected to have the length of 6')
+ newTransactions.forEach((txMeta) => {
+ if (txMeta.status === 'rejected') done(new Error('transaction was found with a status of rejected'))
+ })
+ done()
+ })
+ .catch(done)
+ })
+
+ it('should successfully migrate first time state', (done) => {
+ migration27.migrate({
+ meta: {},
+ data: require('../../../app/scripts/first-time-state'),
+ })
+ .then((migratedData) => {
+ assert.equal(migratedData.meta.version, migration27.version)
+ done()
+ }).catch(done)
+ })
+})
diff --git a/ui/app/actions.js b/ui/app/actions.js
index 41fc3c504..7a8d9667d 100644
--- a/ui/app/actions.js
+++ b/ui/app/actions.js
@@ -6,11 +6,12 @@ const {
calcGasTotal,
calcTokenBalance,
estimateGas,
- estimateGasPriceFromRecentBlocks,
-} = require('./components/send_/send.utils')
+} = require('./components/send/send.utils')
const ethUtil = require('ethereumjs-util')
const { fetchLocale } = require('../i18n-helper')
const log = require('loglevel')
+const { ENVIRONMENT_TYPE_NOTIFICATION } = require('../../app/scripts/lib/enums')
+const { hasUnconfirmedTransactions } = require('./helpers/confirm-transaction/util')
var actions = {
_setBackgroundConnection: _setBackgroundConnection,
@@ -27,6 +28,11 @@ var actions = {
SIDEBAR_CLOSE: 'UI_SIDEBAR_CLOSE',
showSidebar: showSidebar,
hideSidebar: hideSidebar,
+ // sidebar state
+ ALERT_OPEN: 'UI_ALERT_OPEN',
+ ALERT_CLOSE: 'UI_ALERT_CLOSE',
+ showAlert: showAlert,
+ hideAlert: hideAlert,
// network dropdown open
NETWORK_DROPDOWN_OPEN: 'UI_NETWORK_DROPDOWN_OPEN',
NETWORK_DROPDOWN_CLOSE: 'UI_NETWORK_DROPDOWN_CLOSE',
@@ -79,9 +85,14 @@ var actions = {
addNewKeyring,
importNewAccount,
addNewAccount,
+ connectHardware,
+ checkHardwareStatus,
+ forgetDevice,
+ unlockTrezorAccount,
NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN',
navigateToNewAccountScreen,
resetAccount,
+ removeAccount,
showNewVaultSeed: showNewVaultSeed,
showInfoPage: showInfoPage,
CLOSE_WELCOME_SCREEN: 'CLOSE_WELCOME_SCREEN',
@@ -165,6 +176,7 @@ var actions = {
UPDATE_GAS_PRICE: 'UPDATE_GAS_PRICE',
UPDATE_GAS_TOTAL: 'UPDATE_GAS_TOTAL',
UPDATE_SEND_FROM: 'UPDATE_SEND_FROM',
+ UPDATE_SEND_HEX_DATA: 'UPDATE_SEND_HEX_DATA',
UPDATE_SEND_TOKEN_BALANCE: 'UPDATE_SEND_TOKEN_BALANCE',
UPDATE_SEND_TO: 'UPDATE_SEND_TO',
UPDATE_SEND_AMOUNT: 'UPDATE_SEND_AMOUNT',
@@ -184,6 +196,7 @@ var actions = {
setSendTokenBalance,
updateSendTokenBalance,
updateSendFrom,
+ updateSendHexData,
updateSendTo,
updateSendAmount,
updateSendMemo,
@@ -534,6 +547,26 @@ function resetAccount () {
}
}
+function removeAccount (address) {
+ return dispatch => {
+ dispatch(actions.showLoadingIndication())
+
+ return new Promise((resolve, reject) => {
+ background.removeAccount(address, (err, account) => {
+ dispatch(actions.hideLoadingIndication())
+ if (err) {
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ log.info('Account removed: ' + account)
+ dispatch(actions.showAccountsPage())
+ resolve()
+ })
+ })
+ }
+}
+
function addNewKeyring (type, opts) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
@@ -600,6 +633,88 @@ function addNewAccount () {
}
}
+function checkHardwareStatus (deviceName) {
+ log.debug(`background.checkHardwareStatus`, deviceName)
+ return (dispatch, getState) => {
+ dispatch(actions.showLoadingIndication())
+ return new Promise((resolve, reject) => {
+ background.checkHardwareStatus(deviceName, (err, unlocked) => {
+ if (err) {
+ log.error(err)
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ dispatch(actions.hideLoadingIndication())
+
+ forceUpdateMetamaskState(dispatch)
+ return resolve(unlocked)
+ })
+ })
+ }
+}
+
+function forgetDevice (deviceName) {
+ log.debug(`background.forgetDevice`, deviceName)
+ return (dispatch, getState) => {
+ dispatch(actions.showLoadingIndication())
+ return new Promise((resolve, reject) => {
+ background.forgetDevice(deviceName, (err, response) => {
+ if (err) {
+ log.error(err)
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ dispatch(actions.hideLoadingIndication())
+
+ forceUpdateMetamaskState(dispatch)
+ return resolve()
+ })
+ })
+ }
+}
+
+function connectHardware (deviceName, page) {
+ log.debug(`background.connectHardware`, deviceName, page)
+ return (dispatch, getState) => {
+ dispatch(actions.showLoadingIndication())
+ return new Promise((resolve, reject) => {
+ background.connectHardware(deviceName, page, (err, accounts) => {
+ if (err) {
+ log.error(err)
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ dispatch(actions.hideLoadingIndication())
+
+ forceUpdateMetamaskState(dispatch)
+ return resolve(accounts)
+ })
+ })
+ }
+}
+
+function unlockTrezorAccount (index) {
+ log.debug(`background.unlockTrezorAccount`, index)
+ return (dispatch, getState) => {
+ dispatch(actions.showLoadingIndication())
+ return new Promise((resolve, reject) => {
+ background.unlockTrezorAccount(index, (err, accounts) => {
+ if (err) {
+ log.error(err)
+ dispatch(actions.displayWarning(err.message))
+ return reject(err)
+ }
+
+ dispatch(actions.hideLoadingIndication())
+ return resolve()
+ })
+ })
+ }
+}
+
function showInfoPage () {
return {
type: actions.SHOW_INFO_PAGE,
@@ -630,7 +745,7 @@ function setCurrentCurrency (currencyCode) {
function signMsg (msgData) {
log.debug('action - signMsg')
- return (dispatch) => {
+ return (dispatch, getState) => {
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
@@ -647,6 +762,12 @@ function signMsg (msgData) {
}
dispatch(actions.completedTx(msgData.metamaskId))
+
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
+ !hasUnconfirmedTransactions(getState())) {
+ return global.platform.closeCurrentWindow()
+ }
+
return resolve(msgData)
})
})
@@ -655,7 +776,7 @@ function signMsg (msgData) {
function signPersonalMsg (msgData) {
log.debug('action - signPersonalMsg')
- return dispatch => {
+ return (dispatch, getState) => {
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
@@ -672,6 +793,12 @@ function signPersonalMsg (msgData) {
}
dispatch(actions.completedTx(msgData.metamaskId))
+
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
+ !hasUnconfirmedTransactions(getState())) {
+ return global.platform.closeCurrentWindow()
+ }
+
return resolve(msgData)
})
})
@@ -680,7 +807,7 @@ function signPersonalMsg (msgData) {
function signTypedMsg (msgData) {
log.debug('action - signTypedMsg')
- return (dispatch) => {
+ return (dispatch, getState) => {
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
@@ -697,6 +824,12 @@ function signTypedMsg (msgData) {
}
dispatch(actions.completedTx(msgData.metamaskId))
+
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
+ !hasUnconfirmedTransactions(getState())) {
+ return global.platform.closeCurrentWindow()
+ }
+
return resolve(msgData)
})
})
@@ -705,11 +838,10 @@ function signTypedMsg (msgData) {
function signTx (txData) {
return (dispatch) => {
- dispatch(actions.showLoadingIndication())
global.ethQuery.sendTransaction(txData, (err, data) => {
- dispatch(actions.hideLoadingIndication())
- if (err) return dispatch(actions.displayWarning(err.message))
- dispatch(actions.hideWarning())
+ if (err) {
+ return dispatch(actions.displayWarning(err.message))
+ }
})
dispatch(actions.showConfTxPage({}))
}
@@ -746,19 +878,26 @@ function updateGasData ({
}) {
return (dispatch) => {
dispatch(actions.gasLoadingStarted())
- const estimatedGasPrice = estimateGasPriceFromRecentBlocks(recentBlocks)
- return Promise.all([
- Promise.resolve(estimatedGasPrice),
- estimateGas({
- estimateGasMethod: background.estimateGas,
- blockGasLimit,
- selectedAddress,
- selectedToken,
- to,
- value,
- gasPrice: estimatedGasPrice,
- }),
- ])
+ return new Promise((resolve, reject) => {
+ background.getGasPrice((err, data) => {
+ if (err) return reject(err)
+ return resolve(data)
+ })
+ })
+ .then(estimateGasPrice => {
+ return Promise.all([
+ Promise.resolve(estimateGasPrice),
+ estimateGas({
+ estimateGasMethod: background.estimateGas,
+ blockGasLimit,
+ selectedAddress,
+ selectedToken,
+ to,
+ value,
+ estimateGasPrice,
+ }),
+ ])
+ })
.then(([gasPrice, gas]) => {
dispatch(actions.setGasPrice(gasPrice))
dispatch(actions.setGasLimit(gas))
@@ -833,6 +972,13 @@ function updateSendFrom (from) {
}
}
+function updateSendHexData (value) {
+ return {
+ type: actions.UPDATE_SEND_HEX_DATA,
+ value,
+ }
+}
+
function updateSendTo (to, nickname = '') {
return {
type: actions.UPDATE_SEND_TO,
@@ -877,7 +1023,7 @@ function clearSend () {
function sendTx (txData) {
log.info(`actions - sendTx: ${JSON.stringify(txData.txParams)}`)
- return (dispatch) => {
+ return (dispatch, getState) => {
log.debug(`actions calling background.approveTransaction`)
background.approveTransaction(txData.id, (err) => {
if (err) {
@@ -885,6 +1031,11 @@ function sendTx (txData) {
return log.error(err.message)
}
dispatch(actions.completedTx(txData.id))
+
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
+ !hasUnconfirmedTransactions(getState())) {
+ return global.platform.closeCurrentWindow()
+ }
})
}
}
@@ -904,29 +1055,41 @@ function signTokenTx (tokenAddress, toAddress, amount, txData) {
function updateTransaction (txData) {
log.info('actions: updateTx: ' + JSON.stringify(txData))
- return (dispatch) => {
+ return dispatch => {
log.debug(`actions calling background.updateTx`)
- background.updateTransaction(txData, (err) => {
- dispatch(actions.hideLoadingIndication())
- dispatch(actions.updateTransactionParams(txData.id, txData.txParams))
- if (err) {
- dispatch(actions.txError(err))
- dispatch(actions.goHome())
- return log.error(err.message)
- }
- dispatch(actions.showConfTxPage({ id: txData.id }))
+ dispatch(actions.showLoadingIndication())
+
+ return new Promise((resolve, reject) => {
+ background.updateTransaction(txData, (err) => {
+ dispatch(actions.updateTransactionParams(txData.id, txData.txParams))
+ if (err) {
+ dispatch(actions.txError(err))
+ dispatch(actions.goHome())
+ log.error(err.message)
+ return reject(err)
+ }
+
+ resolve(txData)
+ })
})
+ .then(() => updateMetamaskStateFromBackground())
+ .then(newState => dispatch(actions.updateMetamaskState(newState)))
+ .then(() => {
+ dispatch(actions.showConfTxPage({ id: txData.id }))
+ dispatch(actions.hideLoadingIndication())
+ return txData
+ })
}
}
function updateAndApproveTx (txData) {
log.info('actions: updateAndApproveTx: ' + JSON.stringify(txData))
- return (dispatch) => {
+ return (dispatch, getState) => {
log.debug(`actions calling background.updateAndApproveTx`)
+ dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
background.updateAndApproveTransaction(txData, err => {
- dispatch(actions.hideLoadingIndication())
dispatch(actions.updateTransactionParams(txData.id, txData.txParams))
dispatch(actions.clearSend())
@@ -937,10 +1100,23 @@ function updateAndApproveTx (txData) {
reject(err)
}
- dispatch(actions.completedTx(txData.id))
resolve(txData)
})
})
+ .then(() => updateMetamaskStateFromBackground())
+ .then(newState => dispatch(actions.updateMetamaskState(newState)))
+ .then(() => {
+ dispatch(actions.clearSend())
+ dispatch(actions.completedTx(txData.id))
+ dispatch(actions.hideLoadingIndication())
+
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
+ !hasUnconfirmedTransactions(getState())) {
+ return global.platform.closeCurrentWindow()
+ }
+
+ return txData
+ })
}
}
@@ -967,7 +1143,7 @@ function txError (err) {
}
function cancelMsg (msgData) {
- return dispatch => {
+ return (dispatch, getState) => {
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
@@ -981,6 +1157,12 @@ function cancelMsg (msgData) {
}
dispatch(actions.completedTx(msgData.id))
+
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
+ !hasUnconfirmedTransactions(getState())) {
+ return global.platform.closeCurrentWindow()
+ }
+
return resolve(msgData)
})
})
@@ -988,7 +1170,7 @@ function cancelMsg (msgData) {
}
function cancelPersonalMsg (msgData) {
- return dispatch => {
+ return (dispatch, getState) => {
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
@@ -1002,6 +1184,12 @@ function cancelPersonalMsg (msgData) {
}
dispatch(actions.completedTx(id))
+
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
+ !hasUnconfirmedTransactions(getState())) {
+ return global.platform.closeCurrentWindow()
+ }
+
return resolve(msgData)
})
})
@@ -1009,7 +1197,7 @@ function cancelPersonalMsg (msgData) {
}
function cancelTypedMsg (msgData) {
- return dispatch => {
+ return (dispatch, getState) => {
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
@@ -1023,6 +1211,12 @@ function cancelTypedMsg (msgData) {
}
dispatch(actions.completedTx(id))
+
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
+ !hasUnconfirmedTransactions(getState())) {
+ return global.platform.closeCurrentWindow()
+ }
+
return resolve(msgData)
})
})
@@ -1030,15 +1224,33 @@ function cancelTypedMsg (msgData) {
}
function cancelTx (txData) {
- return dispatch => {
+ return (dispatch, getState) => {
log.debug(`background.cancelTransaction`)
+ dispatch(actions.showLoadingIndication())
+
return new Promise((resolve, reject) => {
- background.cancelTransaction(txData.id, () => {
+ background.cancelTransaction(txData.id, err => {
+ if (err) {
+ return reject(err)
+ }
+
+ resolve()
+ })
+ })
+ .then(() => updateMetamaskStateFromBackground())
+ .then(newState => dispatch(actions.updateMetamaskState(newState)))
+ .then(() => {
dispatch(actions.clearSend())
dispatch(actions.completedTx(txData.id))
- resolve(txData)
+ dispatch(actions.hideLoadingIndication())
+
+ if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION &&
+ !hasUnconfirmedTransactions(getState())) {
+ return global.platform.closeCurrentWindow()
+ }
+
+ return txData
})
- })
}
}
@@ -1581,6 +1793,19 @@ function hideSidebar () {
}
}
+function showAlert (msg) {
+ return {
+ type: actions.ALERT_OPEN,
+ value: msg,
+ }
+}
+
+function hideAlert () {
+ return {
+ type: actions.ALERT_CLOSE,
+ }
+}
+
function showLoadingIndication (message) {
return {
diff --git a/ui/app/app.js b/ui/app/app.js
index d0e48a368..dbb6146d1 100644
--- a/ui/app/app.js
+++ b/ui/app/app.js
@@ -11,8 +11,8 @@ const log = require('loglevel')
// init
const InitializeScreen = require('../../mascara/src/app/first-time').default
// accounts
-const SendTransactionScreen = require('./components/send_/send.container')
-const ConfirmTxScreen = require('./conf-tx')
+const SendTransactionScreen = require('./components/send/send.container')
+const ConfirmTransaction = require('./components/pages/confirm-transaction')
// slideout menu
const WalletView = require('./components/wallet-view')
@@ -22,8 +22,7 @@ const Home = require('./components/pages/home')
const Authenticated = require('./components/pages/authenticated')
const Initialized = require('./components/pages/initialized')
const Settings = require('./components/pages/settings')
-const UnlockPage = require('./components/pages/unlock-page')
-const RestoreVaultPage = require('./components/pages/keychains/restore-vault')
+const RestoreVaultPage = require('./components/pages/keychains/restore-vault').default
const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed')
const AddTokenPage = require('./components/pages/add-token')
const ConfirmAddTokenPage = require('./components/pages/confirm-add-token')
@@ -37,9 +36,13 @@ const AccountMenu = require('./components/account-menu')
// Global Modals
const Modal = require('./components/modals/index').Modal
+// Global Alert
+const Alert = require('./components/alert')
const AppHeader = require('./components/app-header')
+import UnlockPage from './components/pages/unlock-page'
+
// Routes
const {
DEFAULT_ROUTE,
@@ -76,7 +79,10 @@ class App extends Component {
h(Authenticated, { path: REVEAL_SEED_ROUTE, exact, component: RevealSeedConfirmation }),
h(Authenticated, { path: SETTINGS_ROUTE, component: Settings }),
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: ConfirmTransaction,
+ }),
h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen }),
h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }),
h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }),
@@ -89,6 +95,7 @@ class App extends Component {
render () {
const {
isLoading,
+ alertMessage,
loadingMessage,
network,
isMouseUser,
@@ -122,6 +129,9 @@ class App extends Component {
// global modal
h(Modal, {}, []),
+ // global alert
+ h(Alert, {visible: this.props.alertOpen, msg: alertMessage}),
+
h(AppHeader),
// sidebar
@@ -145,14 +155,6 @@ class App extends Component {
)
}
- renderGlobalModal () {
- return h(Modal, {
- ref: 'modalRef',
- }, [
- // h(BuyOptions, {}, []),
- ])
- }
-
renderSidebar () {
return h('div', [
h('style', `
@@ -261,11 +263,13 @@ App.propTypes = {
setCurrentCurrencyToUSD: PropTypes.func,
isLoading: PropTypes.bool,
loadingMessage: PropTypes.string,
+ alertMessage: PropTypes.string,
network: PropTypes.string,
provider: PropTypes.object,
frequentRpcList: PropTypes.array,
currentView: PropTypes.object,
sidebarOpen: PropTypes.bool,
+ alertOpen: PropTypes.bool,
hideSidebar: PropTypes.func,
isMascara: PropTypes.bool,
isOnboarding: PropTypes.bool,
@@ -301,6 +305,8 @@ function mapStateToProps (state) {
const {
networkDropdownOpen,
sidebarOpen,
+ alertOpen,
+ alertMessage,
isLoading,
loadingMessage,
} = appState
@@ -326,6 +332,8 @@ function mapStateToProps (state) {
// state from plugin
networkDropdownOpen,
sidebarOpen,
+ alertOpen,
+ alertMessage,
isLoading,
loadingMessage,
noActiveNotices,
diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js
index f34631ca8..9c063d31e 100644
--- a/ui/app/components/account-menu/index.js
+++ b/ui/app/components/account-menu/index.js
@@ -9,11 +9,17 @@ const actions = require('../../actions')
const { Menu, Item, Divider, CloseArea } = require('../dropdowns/components/menu')
const Identicon = require('../identicon')
const { formatBalance } = require('../../util')
+const { ENVIRONMENT_TYPE_POPUP } = require('../../../../app/scripts/lib/enums')
+const { getEnvironmentType } = require('../../../../app/scripts/lib/util')
+const Tooltip = require('../tooltip')
+
+
const {
SETTINGS_ROUTE,
INFO_ROUTE,
NEW_ACCOUNT_ROUTE,
IMPORT_ACCOUNT_ROUTE,
+ CONNECT_HARDWARE_ROUTE,
DEFAULT_ROUTE,
} = require('../../routes')
@@ -63,6 +69,9 @@ function mapDispatchToProps (dispatch) {
dispatch(actions.hideSidebar())
dispatch(actions.toggleAccountMenu())
},
+ showRemoveAccountConfirmationModal: (identity) => {
+ return dispatch(actions.showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', identity }))
+ },
}
}
@@ -106,6 +115,18 @@ AccountMenu.prototype.render = function () {
icon: h('img.account-menu__item-icon', { src: 'images/import-account.svg' }),
text: this.context.t('importAccount'),
}),
+ h(Item, {
+ onClick: () => {
+ toggleAccountMenu()
+ if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
+ global.platform.openExtensionInBrowser(CONNECT_HARDWARE_ROUTE)
+ } else {
+ history.push(CONNECT_HARDWARE_ROUTE)
+ }
+ },
+ icon: h('img.account-menu__item-icon', { src: 'images/connect-icon.svg' }),
+ text: this.context.t('connectHardwareWallet'),
+ }),
h(Divider),
h(Item, {
onClick: () => {
@@ -136,7 +157,8 @@ AccountMenu.prototype.renderAccounts = function () {
} = this.props
const accountOrder = keyrings.reduce((list, keyring) => list.concat(keyring.accounts), [])
- return accountOrder.map((address) => {
+ return accountOrder.filter(address => !!identities[address]).map((address) => {
+
const identity = identities[address]
const isSelected = identity.address === selectedAddress
@@ -170,16 +192,53 @@ AccountMenu.prototype.renderAccounts = function () {
h('div.account-menu__balance', formattedBalance),
]),
- this.indicateIfLoose(keyring),
+ this.renderKeyringType(keyring),
+ this.renderRemoveAccount(keyring, identity),
],
)
})
}
-AccountMenu.prototype.indicateIfLoose = function (keyring) {
+AccountMenu.prototype.renderRemoveAccount = function (keyring, identity) {
+ // Any account that's not from the HD wallet Keyring can be removed
+ const type = keyring.type
+ const isRemovable = type !== 'HD Key Tree'
+ if (isRemovable) {
+ return h(Tooltip, {
+ title: this.context.t('removeAccount'),
+ position: 'bottom',
+ }, [
+ h('a.remove-account-icon', {
+ onClick: (e) => this.removeAccount(e, identity),
+ }, ''),
+ ])
+ }
+ return null
+}
+
+AccountMenu.prototype.removeAccount = function (e, identity) {
+ e.preventDefault()
+ e.stopPropagation()
+ const { showRemoveAccountConfirmationModal } = this.props
+ showRemoveAccountConfirmationModal(identity)
+}
+
+AccountMenu.prototype.renderKeyringType = function (keyring) {
try { // Sometimes keyrings aren't loaded yet:
const type = keyring.type
- const isLoose = type !== 'HD Key Tree'
- return isLoose ? h('.keyring-label.allcaps', this.context.t('imported')) : null
+ let label
+ switch (type) {
+ case 'Trezor Hardware':
+ label = this.context.t('hardware')
+ break
+ case 'Simple Key Pair':
+ label = this.context.t('imported')
+ break
+ default:
+ label = ''
+ }
+
+ return label !== '' ? h('.keyring-label.allcaps', label) : null
+
} catch (e) { return }
}
diff --git a/ui/app/components/alert/index.js b/ui/app/components/alert/index.js
new file mode 100644
index 000000000..5620d847a
--- /dev/null
+++ b/ui/app/components/alert/index.js
@@ -0,0 +1,62 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+
+class Alert extends Component {
+
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ visble: false,
+ msg: false,
+ className: '',
+ }
+ }
+
+ componentWillReceiveProps (nextProps) {
+ if (!this.props.visible && nextProps.visible) {
+ this.animateIn(nextProps)
+ } else if (this.props.visible && !nextProps.visible) {
+ this.animateOut(nextProps)
+ }
+ }
+
+ animateIn (props) {
+ this.setState({
+ msg: props.msg,
+ visible: true,
+ className: '.visible',
+ })
+ }
+
+ animateOut (props) {
+ this.setState({
+ msg: null,
+ className: '.hidden',
+ })
+
+ setTimeout(_ => {
+ this.setState({visible: false})
+ }, 500)
+
+ }
+
+ render () {
+ if (this.state.visible) {
+ return (
+ h(`div.global-alert${this.state.className}`, {},
+ h('a.msg', {}, this.state.msg)
+ )
+ )
+ }
+ return null
+ }
+}
+
+Alert.propTypes = {
+ visible: PropTypes.bool.isRequired,
+ msg: PropTypes.string,
+}
+module.exports = Alert
+
diff --git a/ui/app/components/app-header/app-header.component.js b/ui/app/components/app-header/app-header.component.js
index 62b04562a..07ca6cf84 100644
--- a/ui/app/components/app-header/app-header.component.js
+++ b/ui/app/components/app-header/app-header.component.js
@@ -91,7 +91,6 @@ class AppHeader extends Component {
network,
provider,
history,
- location,
isUnlocked,
} = this.props
@@ -126,7 +125,7 @@ class AppHeader extends Component {
network={network}
provider={provider}
onClick={event => this.handleNetworkIndicatorClick(event)}
- disabled={location.pathname === CONFIRM_TRANSACTION_ROUTE}
+ disabled={this.isConfirming()}
/>
</div>
{ this.renderAccountMenu() }
diff --git a/ui/app/components/button-group/button-group.component.js b/ui/app/components/button-group/button-group.component.js
new file mode 100644
index 000000000..f99f710ce
--- /dev/null
+++ b/ui/app/components/button-group/button-group.component.js
@@ -0,0 +1,61 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+
+export default class ButtonGroup extends PureComponent {
+ static propTypes = {
+ defaultActiveButtonIndex: PropTypes.number,
+ disabled: PropTypes.bool,
+ children: PropTypes.array,
+ className: PropTypes.string,
+ style: PropTypes.object,
+ }
+
+ static defaultProps = {
+ className: 'button-group',
+ }
+
+ state = {
+ activeButtonIndex: this.props.defaultActiveButtonIndex || 0,
+ }
+
+ handleButtonClick (activeButtonIndex) {
+ this.setState({ activeButtonIndex })
+ }
+
+ renderButtons () {
+ const { children, disabled } = this.props
+
+ return React.Children.map(children, (child, index) => {
+ return child && (
+ <button
+ className={classnames(
+ 'button-group__button',
+ { 'button-group__button--active': index === this.state.activeButtonIndex },
+ )}
+ onClick={() => {
+ this.handleButtonClick(index)
+ child.props.onClick && child.props.onClick()
+ }}
+ disabled={disabled || child.props.disabled}
+ key={index}
+ >
+ { child.props.children }
+ </button>
+ )
+ })
+ }
+
+ render () {
+ const { className, style } = this.props
+
+ return (
+ <div
+ className={className}
+ style={style}
+ >
+ { this.renderButtons() }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/button-group/button-group.stories.js b/ui/app/components/button-group/button-group.stories.js
new file mode 100644
index 000000000..14e1a7e49
--- /dev/null
+++ b/ui/app/components/button-group/button-group.stories.js
@@ -0,0 +1,49 @@
+import React from 'react'
+import { storiesOf } from '@storybook/react'
+import { action } from '@storybook/addon-actions'
+import ButtonGroup from './'
+import Button from '../button'
+import { text, boolean } from '@storybook/addon-knobs/react'
+
+storiesOf('ButtonGroup', module)
+ .add('with Buttons', () =>
+ <ButtonGroup
+ style={{ width: '300px' }}
+ disabled={boolean('Disabled', false)}
+ defaultActiveButtonIndex={1}
+ >
+ <Button
+ onClick={action('cheap')}
+ >
+ {text('Button1', 'Cheap')}
+ </Button>
+ <Button
+ onClick={action('average')}
+ >
+ {text('Button2', 'Average')}
+ </Button>
+ <Button
+ onClick={action('fast')}
+ >
+ {text('Button3', 'Fast')}
+ </Button>
+ </ButtonGroup>
+ )
+ .add('with a disabled Button', () =>
+ <ButtonGroup
+ style={{ width: '300px' }}
+ disabled={boolean('Disabled', false)}
+ >
+ <Button
+ onClick={action('enabled')}
+ >
+ {text('Button1', 'Enabled')}
+ </Button>
+ <Button
+ onClick={action('disabled')}
+ disabled
+ >
+ {text('Button2', 'Disabled')}
+ </Button>
+ </ButtonGroup>
+ )
diff --git a/ui/app/components/button-group/index.js b/ui/app/components/button-group/index.js
new file mode 100644
index 000000000..df470bd57
--- /dev/null
+++ b/ui/app/components/button-group/index.js
@@ -0,0 +1 @@
+export { default } from './button-group.component'
diff --git a/ui/app/components/button-group/index.scss b/ui/app/components/button-group/index.scss
new file mode 100644
index 000000000..29713c75b
--- /dev/null
+++ b/ui/app/components/button-group/index.scss
@@ -0,0 +1,38 @@
+.button-group {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &__button {
+ font-family: Roboto;
+ font-size: 1rem;
+ color: $tundora;
+ border-style: solid;
+ border-color: $alto;
+ border-width: 1px 1px 1px;
+ border-left: 0;
+ flex: 1;
+ padding: 12px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ &:first-child {
+ border-left: 1px solid $alto;
+ border-radius: 4px 0 0 4px;
+ }
+
+ &:last-child {
+ border-radius: 0 4px 4px 0;
+ }
+
+ &--active {
+ background-color: $dodger-blue;
+ color: $white;
+ }
+
+ &:disabled {
+ opacity: .5;
+ }
+ }
+} \ No newline at end of file
diff --git a/ui/app/components/button-group/tests/button-group-component.test.js b/ui/app/components/button-group/tests/button-group-component.test.js
new file mode 100644
index 000000000..f07bb97c8
--- /dev/null
+++ b/ui/app/components/button-group/tests/button-group-component.test.js
@@ -0,0 +1,97 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import ButtonGroup from '../button-group.component.js'
+
+const childButtonSpies = {
+ onClick: sinon.spy(),
+}
+
+sinon.spy(ButtonGroup.prototype, 'handleButtonClick')
+sinon.spy(ButtonGroup.prototype, 'renderButtons')
+
+const mockButtons = [
+ <button onClick={childButtonSpies.onClick} key={'a'}><div className="mockClass" /></button>,
+ <button onClick={childButtonSpies.onClick} key={'b'}></button>,
+ <button onClick={childButtonSpies.onClick} key={'c'}></button>,
+]
+
+describe('ButtonGroup Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<ButtonGroup
+ defaultActiveButtonIndex={1}
+ disabled={false}
+ className="someClassName"
+ style={ { color: 'red' } }
+ >{mockButtons}</ButtonGroup>)
+ })
+
+ afterEach(() => {
+ childButtonSpies.onClick.resetHistory()
+ ButtonGroup.prototype.handleButtonClick.resetHistory()
+ ButtonGroup.prototype.renderButtons.resetHistory()
+ })
+
+ describe('handleButtonClick', () => {
+ it('should set the activeButtonIndex', () => {
+ assert.equal(wrapper.state('activeButtonIndex'), 1)
+ wrapper.instance().handleButtonClick(2)
+ assert.equal(wrapper.state('activeButtonIndex'), 2)
+ })
+ })
+
+ describe('renderButtons', () => {
+ it('should render a button for each child', () => {
+ const childButtons = wrapper.find('.button-group__button')
+ assert.equal(childButtons.length, 3)
+ })
+
+ it('should render the correct button with an active state', () => {
+ const childButtons = wrapper.find('.button-group__button')
+ const activeChildButton = wrapper.find('.button-group__button--active')
+ assert.deepEqual(childButtons.get(1), activeChildButton.get(0))
+ })
+
+ it('should call handleButtonClick and the respective button\'s onClick method when a button is clicked', () => {
+ assert.equal(ButtonGroup.prototype.handleButtonClick.callCount, 0)
+ assert.equal(childButtonSpies.onClick.callCount, 0)
+ const childButtons = wrapper.find('.button-group__button')
+ childButtons.at(0).props().onClick()
+ childButtons.at(1).props().onClick()
+ childButtons.at(2).props().onClick()
+ assert.equal(ButtonGroup.prototype.handleButtonClick.callCount, 3)
+ assert.equal(childButtonSpies.onClick.callCount, 3)
+ })
+
+ it('should render all child buttons as disabled if props.disabled is true', () => {
+ const childButtons = wrapper.find('.button-group__button')
+ childButtons.forEach(button => {
+ assert.equal(button.props().disabled, undefined)
+ })
+ wrapper.setProps({ disabled: true })
+ const disabledChildButtons = wrapper.find('[disabled=true]')
+ assert.equal(disabledChildButtons.length, 3)
+ })
+
+ it('should render the children of the button', () => {
+ const mockClass = wrapper.find('.mockClass')
+ assert.equal(mockClass.length, 1)
+ })
+ })
+
+ describe('render', () => {
+ it('should render a div with the expected class and style', () => {
+ assert.equal(wrapper.find('div').at(0).props().className, 'someClassName')
+ assert.deepEqual(wrapper.find('div').at(0).props().style, { color: 'red' })
+ })
+
+ it('should call renderButtons when rendering', () => {
+ assert.equal(ButtonGroup.prototype.renderButtons.callCount, 1)
+ wrapper.instance().render()
+ assert.equal(ButtonGroup.prototype.renderButtons.callCount, 2)
+ })
+ })
+})
diff --git a/ui/app/components/button/button.component.js b/ui/app/components/button/button.component.js
index e8e798445..1e0ef1b64 100644
--- a/ui/app/components/button/button.component.js
+++ b/ui/app/components/button/button.component.js
@@ -5,15 +5,24 @@ import classnames from 'classnames'
const CLASSNAME_DEFAULT = 'btn-default'
const CLASSNAME_PRIMARY = 'btn-primary'
const CLASSNAME_SECONDARY = 'btn-secondary'
+const CLASSNAME_CONFIRM = 'btn-confirm'
const CLASSNAME_LARGE = 'btn--large'
const typeHash = {
default: CLASSNAME_DEFAULT,
primary: CLASSNAME_PRIMARY,
secondary: CLASSNAME_SECONDARY,
+ confirm: CLASSNAME_CONFIRM,
}
-class Button extends Component {
+export default class Button extends Component {
+ static propTypes = {
+ type: PropTypes.string,
+ large: PropTypes.bool,
+ className: PropTypes.string,
+ children: PropTypes.string,
+ }
+
render () {
const { type, large, className, ...buttonProps } = this.props
@@ -31,13 +40,3 @@ class Button extends Component {
)
}
}
-
-Button.propTypes = {
- type: PropTypes.string,
- large: PropTypes.bool,
- className: PropTypes.string,
- children: PropTypes.string,
-}
-
-export default Button
-
diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js b/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js
new file mode 100644
index 000000000..f0703dde2
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js
@@ -0,0 +1,52 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+
+const ConfirmDetailRow = props => {
+ const {
+ label,
+ fiatText,
+ ethText,
+ onHeaderClick,
+ fiatTextColor,
+ headerText,
+ headerTextClassName,
+ } = props
+
+ return (
+ <div className="confirm-detail-row">
+ <div className="confirm-detail-row__label">
+ { label }
+ </div>
+ <div className="confirm-detail-row__details">
+ <div
+ className={classnames('confirm-detail-row__header-text', headerTextClassName)}
+ onClick={() => onHeaderClick && onHeaderClick()}
+ >
+ { headerText }
+ </div>
+ <div
+ className="confirm-detail-row__fiat"
+ style={{ color: fiatTextColor }}
+ >
+ { fiatText }
+ </div>
+ <div className="confirm-detail-row__eth">
+ { ethText }
+ </div>
+ </div>
+ </div>
+ )
+}
+
+ConfirmDetailRow.propTypes = {
+ label: PropTypes.string,
+ fiatText: PropTypes.string,
+ ethText: PropTypes.string,
+ fiatTextColor: PropTypes.string,
+ onHeaderClick: PropTypes.func,
+ headerText: PropTypes.string,
+ headerTextClassName: PropTypes.string,
+}
+
+export default ConfirmDetailRow
diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/index.js b/ui/app/components/confirm-page-container/confirm-detail-row/index.js
new file mode 100644
index 000000000..056afff04
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-detail-row/index.js
@@ -0,0 +1 @@
+export { default } from './confirm-detail-row.component'
diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/index.scss b/ui/app/components/confirm-page-container/confirm-detail-row/index.scss
new file mode 100644
index 000000000..84d0d56ed
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-detail-row/index.scss
@@ -0,0 +1,43 @@
+.confirm-detail-row {
+ padding: 14px 0;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+
+ &__label {
+ font-size: .75rem;
+ font-weight: 500;
+ color: $scorpion;
+ text-transform: uppercase;
+ }
+
+ &__details {
+ flex: 1;
+ text-align: end;
+ }
+
+ &__fiat {
+ font-size: 1.5rem;
+ }
+
+ &__eth {
+ color: $oslo-gray;
+ }
+
+ &__header-text {
+ font-size: .75rem;
+ text-transform: uppercase;
+ margin-bottom: 6px;
+ color: $scorpion;
+
+ &--edit {
+ color: $curious-blue;
+ cursor: pointer;
+ }
+
+ &--total {
+ font-size: .625rem;
+ }
+ }
+}
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js
new file mode 100644
index 000000000..08923af88
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js
@@ -0,0 +1,105 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import { Tabs, Tab } from '../../tabs'
+import {
+ ConfirmPageContainerSummary,
+ ConfirmPageContainerError,
+ ConfirmPageContainerWarning,
+} from './'
+
+export default class ConfirmPageContainerContent extends Component {
+ static propTypes = {
+ action: PropTypes.string,
+ dataComponent: PropTypes.node,
+ detailsComponent: PropTypes.node,
+ errorKey: PropTypes.string,
+ errorMessage: PropTypes.string,
+ hideSubtitle: PropTypes.bool,
+ identiconAddress: PropTypes.string,
+ nonce: PropTypes.string,
+ subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ summaryComponent: PropTypes.node,
+ title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ titleComponent: PropTypes.func,
+ warning: PropTypes.string,
+ }
+
+ renderContent () {
+ const { detailsComponent, dataComponent } = this.props
+
+ if (detailsComponent && dataComponent) {
+ return this.renderTabs()
+ } else {
+ return detailsComponent || dataComponent
+ }
+ }
+
+ renderTabs () {
+ const { detailsComponent, dataComponent } = this.props
+
+ return (
+ <Tabs>
+ <Tab name="Details">
+ { detailsComponent }
+ </Tab>
+ <Tab name="Data">
+ { dataComponent }
+ </Tab>
+ </Tabs>
+ )
+ }
+
+ render () {
+ const {
+ action,
+ errorKey,
+ errorMessage,
+ title,
+ subtitle,
+ hideSubtitle,
+ identiconAddress,
+ nonce,
+ summaryComponent,
+ detailsComponent,
+ dataComponent,
+ warning,
+ } = this.props
+
+ return (
+ <div className="confirm-page-container-content">
+ {
+ warning && (
+ <ConfirmPageContainerWarning warning={warning} />
+ )
+ }
+ {
+ summaryComponent || (
+ <ConfirmPageContainerSummary
+ className={classnames({
+ 'confirm-page-container-summary--border': !detailsComponent || !dataComponent,
+ })}
+ action={action}
+ title={title}
+ subtitle={subtitle}
+ hideSubtitle={hideSubtitle}
+ identiconAddress={identiconAddress}
+ nonce={nonce}
+ />
+ )
+ }
+ { this.renderContent() }
+ {
+ (errorKey || errorMessage) && (
+ <div className="confirm-page-container-content__error-container">
+ <ConfirmPageContainerError
+ errorMessage={errorMessage}
+ errorKey={errorKey}
+ />
+ </div>
+ )
+ }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/confirm-page-container-error.component.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/confirm-page-container-error.component.js
new file mode 100644
index 000000000..70ebdeb20
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/confirm-page-container-error.component.js
@@ -0,0 +1,28 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+
+const ConfirmPageContainerError = (props, context) => {
+ const { errorMessage, errorKey } = props
+ const error = errorKey ? context.t(errorKey) : errorMessage
+
+ return (
+ <div className="confirm-page-container-error">
+ <img
+ src="/images/alert-red.svg"
+ className="confirm-page-container-error__icon"
+ />
+ { `ALERT: ${error}` }
+ </div>
+ )
+}
+
+ConfirmPageContainerError.propTypes = {
+ errorMessage: PropTypes.string,
+ errorKey: PropTypes.string,
+}
+
+ConfirmPageContainerError.contextTypes = {
+ t: PropTypes.func,
+}
+
+export default ConfirmPageContainerError
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.js
new file mode 100644
index 000000000..4ac95d0e3
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.js
@@ -0,0 +1 @@
+export { default } from './confirm-page-container-error.component'
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.scss b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.scss
new file mode 100644
index 000000000..e99b0f631
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-error/index.scss
@@ -0,0 +1,17 @@
+.confirm-page-container-error {
+ height: 32px;
+ border: 1px solid $monzo;
+ color: $monzo;
+ background: lighten($monzo, 56%);
+ border-radius: 4px;
+ font-size: .75rem;
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ padding-left: 16px;
+
+ &__icon {
+ margin-right: 8px;
+ flex: 0 0 auto;
+ }
+}
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js
new file mode 100644
index 000000000..3b1ee62c5
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js
@@ -0,0 +1,56 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import Identicon from '../../../identicon'
+
+const ConfirmPageContainerSummary = props => {
+ const { action, title, subtitle, hideSubtitle, className, identiconAddress, nonce } = props
+
+ return (
+ <div className={classnames('confirm-page-container-summary', className)}>
+ <div className="confirm-page-container-summary__action-row">
+ <div className="confirm-page-container-summary__action">
+ { action }
+ </div>
+ {
+ nonce && (
+ <div className="confirm-page-container-summary__nonce">
+ { `#${nonce}` }
+ </div>
+ )
+ }
+ </div>
+ <div className="confirm-page-container-summary__title">
+ {
+ identiconAddress && (
+ <Identicon
+ className="confirm-page-container-summary__identicon"
+ diameter={36}
+ address={identiconAddress}
+ />
+ )
+ }
+ <div className="confirm-page-container-summary__title-text">
+ { title }
+ </div>
+ </div>
+ {
+ hideSubtitle || <div className="confirm-page-container-summary__subtitle">
+ { subtitle }
+ </div>
+ }
+ </div>
+ )
+}
+
+ConfirmPageContainerSummary.propTypes = {
+ action: PropTypes.string,
+ title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ hideSubtitle: PropTypes.bool,
+ className: PropTypes.string,
+ identiconAddress: PropTypes.string,
+ nonce: PropTypes.string,
+}
+
+export default ConfirmPageContainerSummary
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.js
new file mode 100644
index 000000000..ed1b28cf2
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.js
@@ -0,0 +1 @@
+export { default } from './confirm-page-container-summary.component'
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss
new file mode 100644
index 000000000..7f0f5d37a
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss
@@ -0,0 +1,54 @@
+.confirm-page-container-summary {
+ padding: 16px 24px 0;
+ background-color: #f9fafa;
+ height: 133px;
+ box-sizing: border-box;
+
+ &__action-row {
+ display: flex;
+ justify-content: space-between;
+ }
+
+ &__action {
+ text-transform: uppercase;
+ color: $oslo-gray;
+ font-size: .75rem;
+ padding: 3px 8px;
+ border: 1px solid $oslo-gray;
+ border-radius: 4px;
+ display: inline-block;
+ }
+
+ &__nonce {
+ color: $oslo-gray;
+ }
+
+ &__title {
+ padding: 4px 0;
+ display: flex;
+ align-items: center;
+ }
+
+ &__identicon {
+ flex: 0 0 auto;
+ margin-right: 8px;
+ }
+
+ &__title-text {
+ font-size: 2.25rem;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ &__subtitle {
+ color: $oslo-gray;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ &--border {
+ border-bottom: 1px solid $geyser;
+ }
+}
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js
new file mode 100644
index 000000000..79901c8fc
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js
@@ -0,0 +1,22 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+
+const ConfirmPageContainerWarning = props => {
+ return (
+ <div className="confirm-page-container-warning">
+ <img
+ className="confirm-page-container-warning__icon"
+ src="/images/alert.svg"
+ />
+ <div className="confirm-page-container-warning__warning">
+ { props.warning }
+ </div>
+ </div>
+ )
+}
+
+ConfirmPageContainerWarning.propTypes = {
+ warning: PropTypes.string,
+}
+
+export default ConfirmPageContainerWarning
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.js
new file mode 100644
index 000000000..6e48bd144
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.js
@@ -0,0 +1 @@
+export { default } from './confirm-page-container-warning.component'
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.scss b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.scss
new file mode 100644
index 000000000..50545a1a2
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.scss
@@ -0,0 +1,18 @@
+.confirm-page-container-warning {
+ background-color: #fffcdb;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-bottom: 1px solid $geyser;
+ padding: 12px 24px;
+
+ &__icon {
+ flex: 0 0 auto;
+ margin-right: 16px;
+ }
+
+ &__warning {
+ font-size: .75rem;
+ color: #5f5922;
+ }
+}
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/index.js b/ui/app/components/confirm-page-container/confirm-page-container-content/index.js
new file mode 100644
index 000000000..1469dd438
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-page-container-content/index.js
@@ -0,0 +1,4 @@
+export { default } from './confirm-page-container-content.component'
+export { default as ConfirmPageContainerSummary } from './confirm-page-container-summary'
+export { default as ConfirmPageContainerError } from './confirm-page-container-error'
+export { default as ConfirmPageContainerWarning } from './confirm-page-container-warning'
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/index.scss b/ui/app/components/confirm-page-container/confirm-page-container-content/index.scss
new file mode 100644
index 000000000..39797a43f
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-page-container-content/index.scss
@@ -0,0 +1,66 @@
+@import './confirm-page-container-error/index';
+
+@import './confirm-page-container-warning/index';
+
+@import './confirm-page-container-summary/index';
+
+.confirm-page-container-content {
+ overflow-y: auto;
+ flex: 1;
+
+ &__error-container {
+ padding: 0 16px 16px 16px;
+ }
+
+ &__details {
+ box-sizing: border-box;
+ padding: 0 24px;
+ }
+
+ &__data {
+ padding: 16px;
+ color: $oslo-gray;
+ }
+
+ &__data-box {
+ background-color: #f9fafa;
+ padding: 12px;
+ font-size: .75rem;
+ margin-bottom: 16px;
+ word-wrap: break-word;
+ max-height: 200px;
+ overflow-y: auto;
+
+ &-label {
+ text-transform: uppercase;
+ padding: 8px 0 12px;
+ font-size: 12px;
+ }
+ }
+
+ &__data-field {
+ display: flex;
+ flex-direction: row;
+
+ &-label {
+ font-weight: 500;
+ padding-right: 16px;
+ }
+
+ &:not(:last-child) {
+ margin-bottom: 5px;
+ }
+ }
+
+ &__gas-fee {
+ border-bottom: 1px solid $geyser;
+ }
+
+ &__function-type {
+ font-size: .875rem;
+ font-weight: 500;
+ text-transform: capitalize;
+ color: $black;
+ padding-left: 5px;
+ }
+}
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js b/ui/app/components/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js
new file mode 100644
index 000000000..e6fe8f82c
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js
@@ -0,0 +1,63 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import {
+ ENVIRONMENT_TYPE_POPUP,
+ ENVIRONMENT_TYPE_NOTIFICATION,
+} from '../../../../../app/scripts/lib/enums'
+import NetworkDisplay from '../../network-display'
+
+export default class ConfirmPageContainer extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ showEdit: PropTypes.bool,
+ onEdit: PropTypes.func,
+ children: PropTypes.node,
+ }
+
+ renderTop () {
+ const { onEdit, showEdit } = this.props
+ const windowType = window.METAMASK_UI_TYPE
+ const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION &&
+ windowType !== ENVIRONMENT_TYPE_POPUP
+
+ if (!showEdit && isFullScreen) {
+ return null
+ }
+
+ return (
+ <div className="confirm-page-container-header__row">
+ <div
+ className="confirm-page-container-header__back-button-container"
+ style={{
+ visibility: showEdit ? 'initial' : 'hidden',
+ }}
+ >
+ <img
+ src="/images/caret-left.svg"
+ />
+ <span
+ className="confirm-page-container-header__back-button"
+ onClick={() => onEdit()}
+ >
+ { this.context.t('edit') }
+ </span>
+ </div>
+ { !isFullScreen && <NetworkDisplay /> }
+ </div>
+ )
+ }
+
+ render () {
+ const { children } = this.props
+
+ return (
+ <div className="confirm-page-container-header">
+ { this.renderTop() }
+ { children }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-header/index.js b/ui/app/components/confirm-page-container/confirm-page-container-header/index.js
new file mode 100644
index 000000000..71feb6931
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-page-container-header/index.js
@@ -0,0 +1 @@
+export { default } from './confirm-page-container-header.component'
diff --git a/ui/app/components/confirm-page-container/confirm-page-container-header/index.scss b/ui/app/components/confirm-page-container/confirm-page-container-header/index.scss
new file mode 100644
index 000000000..43e1e4427
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-page-container-header/index.scss
@@ -0,0 +1,27 @@
+.confirm-page-container-header {
+ display: flex;
+ flex-direction: column;
+ flex: 0 0 auto;
+
+ &__row {
+ display: flex;
+ justify-content: space-between;
+ border-bottom: 1px solid $geyser;
+ padding: 13px 13px 13px 24px;
+ flex: 0 0 auto;
+ }
+
+ &__back-button-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ &__back-button {
+ color: #2f9ae0;
+ font-size: 1rem;
+ cursor: pointer;
+ font-weight: 400;
+ padding-left: 5px;
+ }
+}
diff --git a/ui/app/components/confirm-page-container/confirm-page-container.component.js b/ui/app/components/confirm-page-container/confirm-page-container.component.js
new file mode 100644
index 000000000..93e4ae7bf
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-page-container.component.js
@@ -0,0 +1,118 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import SenderToRecipient from '../sender-to-recipient'
+import { PageContainerFooter } from '../page-container'
+import { ConfirmPageContainerHeader, ConfirmPageContainerContent } from './'
+
+export default class ConfirmPageContainer extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ // Header
+ action: PropTypes.string,
+ hideSubtitle: PropTypes.bool,
+ onEdit: PropTypes.func,
+ showEdit: PropTypes.bool,
+ subtitle: PropTypes.string,
+ title: PropTypes.string,
+ titleComponent: PropTypes.func,
+ // Sender to Recipient
+ fromAddress: PropTypes.string,
+ fromName: PropTypes.string,
+ toAddress: PropTypes.string,
+ toName: PropTypes.string,
+ // Content
+ contentComponent: PropTypes.node,
+ errorKey: PropTypes.string,
+ errorMessage: PropTypes.string,
+ fiatTransactionAmount: PropTypes.string,
+ fiatTransactionFee: PropTypes.string,
+ fiatTransactionTotal: PropTypes.string,
+ ethTransactionAmount: PropTypes.string,
+ ethTransactionFee: PropTypes.string,
+ ethTransactionTotal: PropTypes.string,
+ onEditGas: PropTypes.func,
+ dataComponent: PropTypes.node,
+ detailsComponent: PropTypes.node,
+ identiconAddress: PropTypes.string,
+ nonce: PropTypes.string,
+ summaryComponent: PropTypes.node,
+ warning: PropTypes.string,
+ // Footer
+ onCancel: PropTypes.func,
+ onSubmit: PropTypes.func,
+ valid: PropTypes.bool,
+ }
+
+ render () {
+ const {
+ showEdit,
+ onEdit,
+ fromName,
+ fromAddress,
+ toName,
+ toAddress,
+ valid,
+ errorKey,
+ errorMessage,
+ contentComponent,
+ action,
+ title,
+ titleComponent,
+ subtitle,
+ hideSubtitle,
+ summaryComponent,
+ detailsComponent,
+ dataComponent,
+ onCancel,
+ onSubmit,
+ identiconAddress,
+ nonce,
+ warning,
+ } = this.props
+
+ return (
+ <div className="page-container">
+ <ConfirmPageContainerHeader
+ showEdit={showEdit}
+ onEdit={() => onEdit()}
+ >
+ <SenderToRecipient
+ senderName={fromName}
+ senderAddress={fromAddress}
+ recipientName={toName}
+ recipientAddress={toAddress}
+ />
+ </ConfirmPageContainerHeader>
+ {
+ contentComponent || (
+ <ConfirmPageContainerContent
+ action={action}
+ title={title}
+ titleComponent={titleComponent}
+ subtitle={subtitle}
+ hideSubtitle={hideSubtitle}
+ summaryComponent={summaryComponent}
+ detailsComponent={detailsComponent}
+ dataComponent={dataComponent}
+ errorMessage={errorMessage}
+ errorKey={errorKey}
+ identiconAddress={identiconAddress}
+ nonce={nonce}
+ warning={warning}
+ />
+ )
+ }
+ <PageContainerFooter
+ onCancel={() => onCancel()}
+ onSubmit={() => onSubmit()}
+ submitText={this.context.t('confirm')}
+ submitButtonType="confirm"
+ disabled={!valid}
+ />
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/confirm-page-container/index.js b/ui/app/components/confirm-page-container/index.js
new file mode 100644
index 000000000..ee88aa5d3
--- /dev/null
+++ b/ui/app/components/confirm-page-container/index.js
@@ -0,0 +1,8 @@
+export { default } from './confirm-page-container.component'
+export { default as ConfirmPageContainerHeader } from './confirm-page-container-header'
+export { default as ConfirmDetailRow } from './confirm-detail-row'
+export {
+ default as ConfirmPageContainerContent,
+ ConfirmPageContainerSummary,
+ ConfirmPageContainerError,
+} from './confirm-page-container-content'
diff --git a/ui/app/components/confirm-page-container/index.scss b/ui/app/components/confirm-page-container/index.scss
new file mode 100644
index 000000000..af7a5b555
--- /dev/null
+++ b/ui/app/components/confirm-page-container/index.scss
@@ -0,0 +1,5 @@
+@import './confirm-page-container-content/index';
+
+@import './confirm-page-container-header/index';
+
+@import './confirm-detail-row/index';
diff --git a/ui/app/components/customize-gas-modal/index.js b/ui/app/components/customize-gas-modal/index.js
index cd8f76ed5..c255fd64d 100644
--- a/ui/app/components/customize-gas-modal/index.js
+++ b/ui/app/components/customize-gas-modal/index.js
@@ -16,11 +16,11 @@ const {
MIN_GAS_PRICE_DEC,
MIN_GAS_LIMIT_DEC,
MIN_GAS_PRICE_GWEI,
-} = require('../send_/send.constants')
+} = require('../send/send.constants')
const {
isBalanceSufficient,
-} = require('../send_/send.utils')
+} = require('../send/send.utils')
const {
conversionUtil,
@@ -31,8 +31,6 @@ const {
} = require('../../conversion-util')
const {
- getGasPrice,
- getGasLimit,
getGasIsLoading,
getForceGasMin,
conversionRateSelector,
@@ -44,6 +42,11 @@ const {
getSendMaxModeState,
} = require('../../selectors')
+const {
+ getGasPrice,
+ getGasLimit,
+} = require('../send/send.selectors')
+
function mapStateToProps (state) {
const selectedToken = getSelectedToken(state)
const currentAccount = getSendFrom(state) || getCurrentAccountWithSendEtherInfo(state)
diff --git a/ui/app/components/dropdowns/account-dropdown-mini.js b/ui/app/components/dropdowns/account-dropdown-mini.js
index a7a908d3b..261eb0aa2 100644
--- a/ui/app/components/dropdowns/account-dropdown-mini.js
+++ b/ui/app/components/dropdowns/account-dropdown-mini.js
@@ -1,7 +1,7 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
-const AccountListItem = require('../send_/account-list-item/account-list-item.component').default
+const AccountListItem = require('../send/account-list-item/account-list-item.component').default
module.exports = AccountDropdownMini
diff --git a/ui/app/components/dropdowns/components/network-dropdown-icon.js b/ui/app/components/dropdowns/components/network-dropdown-icon.js
index 7e94e0af5..a45da4c10 100644
--- a/ui/app/components/dropdowns/components/network-dropdown-icon.js
+++ b/ui/app/components/dropdowns/components/network-dropdown-icon.js
@@ -15,6 +15,7 @@ NetworkDropdownIcon.prototype.render = function () {
backgroundColor,
isSelected,
innerBorder = 'none',
+ diameter = '12',
} = this.props
return h(`.menu-icon-circle${isSelected ? '--active' : ''}`, {},
@@ -22,6 +23,8 @@ NetworkDropdownIcon.prototype.render = function () {
style: {
background: backgroundColor,
border: innerBorder,
+ height: `${diameter}px`,
+ width: `${diameter}px`,
},
})
)
diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js
index 292dcdde6..b9f99b3d1 100644
--- a/ui/app/components/ens-input.js
+++ b/ui/app/components/ens-input.js
@@ -10,7 +10,7 @@ const networkMap = require('ethjs-ens/lib/network-map.json')
const ensRE = /.+\..+$/
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
const connect = require('react-redux').connect
-const ToAutoComplete = require('./send/to-autocomplete')
+const ToAutoComplete = require('./send/to-autocomplete').default
const log = require('loglevel')
const { isValidENSAddress } = require('../util')
diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss
index 351640f6e..b3e14ce23 100644
--- a/ui/app/components/index.scss
+++ b/ui/app/components/index.scss
@@ -1,9 +1,21 @@
+@import './button-group/index';
+
@import './export-text-container/index';
@import './selected-account/index';
@import './info-box/index';
+@import './network-display/index';
+
+@import './confirm-page-container/index';
+
+@import './page-container/index';
+
@import './pages/index';
@import './modals/index';
+
+@import './sender-to-recipient/index';
+
+@import './tabs/index';
diff --git a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js
new file mode 100644
index 000000000..5a9f0f289
--- /dev/null
+++ b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js
@@ -0,0 +1,93 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import Button from '../../button'
+import { addressSummary } from '../../../util'
+import Identicon from '../../identicon'
+import genAccountLink from '../../../../lib/account-link'
+
+class ConfirmRemoveAccount extends Component {
+ static propTypes = {
+ hideModal: PropTypes.func.isRequired,
+ removeAccount: PropTypes.func.isRequired,
+ identity: PropTypes.object.isRequired,
+ network: PropTypes.string.isRequired,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ handleRemove () {
+ this.props.removeAccount(this.props.identity.address)
+ .then(() => this.props.hideModal())
+ }
+
+ renderSelectedAccount () {
+ const { identity } = this.props
+ return (
+ <div className="modal-container__account">
+ <div className="modal-container__account__identicon">
+ <Identicon
+ address={identity.address}
+ diameter={32}
+ />
+ </div>
+ <div className="modal-container__account__name">
+ <span className="modal-container__account__label">Name</span>
+ <span className="account_value">{identity.name}</span>
+ </div>
+ <div className="modal-container__account__address">
+ <span className="modal-container__account__label">Public Address</span>
+ <span className="account_value">{ addressSummary(identity.address, 4, 4) }</span>
+ </div>
+ <div className="modal-container__account__link">
+ <a
+ className=""
+ href={genAccountLink(identity.address, this.props.network)}
+ target={'_blank'}
+ title={this.context.t('etherscanView')}
+ >
+ <img src="images/popout.svg" />
+ </a>
+ </div>
+ </div>
+ )
+ }
+
+ render () {
+ const { t } = this.context
+
+ return (
+ <div className="modal-container">
+ <div className="modal-container__content">
+ <div className="modal-container__title">
+ { `${t('removeAccount')}` }?
+ </div>
+ { this.renderSelectedAccount() }
+ <div className="modal-container__description">
+ { t('removeAccountDescription') }
+ <a className="modal-container__link" rel="noopener noreferrer" target="_blank" href="https://consensys.zendesk.com/hc/en-us/articles/360004180111-What-are-imported-accounts-New-UI-">{ t('learnMore') }</a>
+ </div>
+ </div>
+ <div className="modal-container__footer">
+ <Button
+ type="default"
+ className="modal-container__footer-button"
+ onClick={() => this.props.hideModal()}
+ >
+ { t('nevermind') }
+ </Button>
+ <Button
+ type="secondary"
+ className="modal-container__footer-button"
+ onClick={() => this.handleRemove()}
+ >
+ { t('remove') }
+ </Button>
+ </div>
+ </div>
+ )
+ }
+}
+
+export default ConfirmRemoveAccount
diff --git a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js
new file mode 100644
index 000000000..4b194c995
--- /dev/null
+++ b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js
@@ -0,0 +1,20 @@
+import { connect } from 'react-redux'
+import ConfirmRemoveAccount from './confirm-remove-account.component'
+
+const { hideModal, removeAccount } = require('../../../actions')
+
+const mapStateToProps = state => {
+ return {
+ identity: state.appState.modal.modalState.props.identity,
+ network: state.metamask.network,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ hideModal: () => dispatch(hideModal()),
+ removeAccount: (address) => dispatch(removeAccount(address)),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(ConfirmRemoveAccount)
diff --git a/ui/app/components/modals/confirm-remove-account/index.js b/ui/app/components/modals/confirm-remove-account/index.js
new file mode 100644
index 000000000..9763fbe05
--- /dev/null
+++ b/ui/app/components/modals/confirm-remove-account/index.js
@@ -0,0 +1,2 @@
+import ConfirmRemoveAccount from './confirm-remove-account.container'
+module.exports = ConfirmRemoveAccount
diff --git a/ui/app/components/modals/customize-gas/customize-gas.component.js b/ui/app/components/modals/customize-gas/customize-gas.component.js
new file mode 100644
index 000000000..0337c5413
--- /dev/null
+++ b/ui/app/components/modals/customize-gas/customize-gas.component.js
@@ -0,0 +1,140 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import GasModalCard from '../../customize-gas-modal/gas-modal-card'
+import { MIN_GAS_PRICE_GWEI } from '../../send/send.constants'
+
+import {
+ getDecimalGasLimit,
+ getDecimalGasPrice,
+ getPrefixedHexGasLimit,
+ getPrefixedHexGasPrice,
+} from './customize-gas.util'
+
+export default class CustomizeGas extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ txData: PropTypes.object.isRequired,
+ hideModal: PropTypes.func,
+ validate: PropTypes.func,
+ onSubmit: PropTypes.func,
+ }
+
+ state = {
+ gasPrice: 0,
+ gasLimit: 0,
+ originalGasPrice: 0,
+ originalGasLimit: 0,
+ }
+
+ componentDidMount () {
+ const { txData = {} } = this.props
+ const { txParams: { gas: hexGasLimit, gasPrice: hexGasPrice } = {} } = txData
+
+ const gasLimit = getDecimalGasLimit(hexGasLimit)
+ const gasPrice = getDecimalGasPrice(hexGasPrice)
+
+ this.setState({
+ gasPrice,
+ gasLimit,
+ originalGasPrice: gasPrice,
+ originalGasLimit: gasLimit,
+ })
+ }
+
+ handleRevert () {
+ const { originalGasPrice, originalGasLimit } = this.state
+
+ this.setState({
+ gasPrice: originalGasPrice,
+ gasLimit: originalGasLimit,
+ })
+ }
+
+ handleSave () {
+ const { onSubmit, hideModal } = this.props
+ const { gasLimit, gasPrice } = this.state
+ const prefixedHexGasPrice = getPrefixedHexGasPrice(gasPrice)
+ const prefixedHexGasLimit = getPrefixedHexGasLimit(gasLimit)
+
+ Promise.resolve(onSubmit({ gasPrice: prefixedHexGasPrice, gasLimit: prefixedHexGasLimit }))
+ .then(() => hideModal())
+ }
+
+ validate () {
+ const { gasLimit, gasPrice } = this.state
+ return this.props.validate({
+ gasPrice: getPrefixedHexGasPrice(gasPrice),
+ gasLimit: getPrefixedHexGasLimit(gasLimit),
+ })
+ }
+
+ render () {
+ const { t } = this.context
+ const { hideModal } = this.props
+ const { gasPrice, gasLimit } = this.state
+ const { valid, errorKey } = this.validate()
+
+ return (
+ <div className="customize-gas">
+ <div className="customize-gas__content">
+ <div className="customize-gas__header">
+ <div className="customize-gas__title">
+ { this.context.t('customGas') }
+ </div>
+ <div
+ className="customize-gas__close"
+ onClick={() => hideModal()}
+ />
+ </div>
+ <div className="customize-gas__body">
+ <GasModalCard
+ value={gasPrice}
+ min={MIN_GAS_PRICE_GWEI}
+ step={1}
+ onChange={value => this.setState({ gasPrice: value })}
+ title={t('gasPrice')}
+ copy={t('gasPriceCalculation')}
+ />
+ <GasModalCard
+ value={gasLimit}
+ min={1}
+ step={1}
+ onChange={value => this.setState({ gasLimit: value })}
+ title={t('gasLimit')}
+ copy={t('gasLimitCalculation')}
+ />
+ </div>
+ <div className="customize-gas__footer">
+ { !valid && <div className="customize-gas__error-message">{ t(errorKey) }</div> }
+ <div
+ className="customize-gas__revert"
+ onClick={() => this.handleRevert()}
+ >
+ { t('revert') }
+ </div>
+ <div className="customize-gas__buttons">
+ <button
+ className="btn-default customize-gas__cancel"
+ onClick={() => hideModal()}
+ style={{ marginRight: '10px' }}
+ >
+ { t('cancel') }
+ </button>
+ <button
+ className="btn-primary customize-gas__save"
+ onClick={() => this.handleSave()}
+ style={{ marginRight: '10px' }}
+ disabled={!valid}
+ >
+ { t('save') }
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/modals/customize-gas/customize-gas.container.js b/ui/app/components/modals/customize-gas/customize-gas.container.js
new file mode 100644
index 000000000..46a799795
--- /dev/null
+++ b/ui/app/components/modals/customize-gas/customize-gas.container.js
@@ -0,0 +1,22 @@
+import { connect } from 'react-redux'
+import CustomizeGas from './customize-gas.component'
+import { hideModal } from '../../../actions'
+
+const mapStateToProps = state => {
+ const { appState: { modal: { modalState: { props } } } } = state
+ const { txData, onSubmit, validate } = props
+
+ return {
+ txData,
+ onSubmit,
+ validate,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ hideModal: () => dispatch(hideModal()),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(CustomizeGas)
diff --git a/ui/app/components/modals/customize-gas/customize-gas.util.js b/ui/app/components/modals/customize-gas/customize-gas.util.js
new file mode 100644
index 000000000..6ba4a7705
--- /dev/null
+++ b/ui/app/components/modals/customize-gas/customize-gas.util.js
@@ -0,0 +1,34 @@
+import ethUtil from 'ethereumjs-util'
+import { conversionUtil } from '../../../conversion-util'
+
+export function getDecimalGasLimit (hexGasLimit) {
+ return conversionUtil(hexGasLimit, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ })
+}
+
+export function getDecimalGasPrice (hexGasPrice) {
+ return conversionUtil(hexGasPrice, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ fromDenomination: 'WEI',
+ toDenomination: 'GWEI',
+ })
+}
+
+export function getPrefixedHexGasLimit (gasLimit) {
+ return ethUtil.addHexPrefix(conversionUtil(gasLimit, {
+ fromNumericBase: 'dec',
+ toNumericBase: 'hex',
+ }))
+}
+
+export function getPrefixedHexGasPrice (gasPrice) {
+ return ethUtil.addHexPrefix(conversionUtil(gasPrice, {
+ fromNumericBase: 'dec',
+ toNumericBase: 'hex',
+ fromDenomination: 'GWEI',
+ toDenomination: 'WEI',
+ }))
+}
diff --git a/ui/app/components/modals/customize-gas/index.js b/ui/app/components/modals/customize-gas/index.js
new file mode 100644
index 000000000..3a0ab7edc
--- /dev/null
+++ b/ui/app/components/modals/customize-gas/index.js
@@ -0,0 +1 @@
+export { default } from './customize-gas.container'
diff --git a/ui/app/components/modals/customize-gas/index.scss b/ui/app/components/modals/customize-gas/index.scss
new file mode 100644
index 000000000..e10452691
--- /dev/null
+++ b/ui/app/components/modals/customize-gas/index.scss
@@ -0,0 +1,110 @@
+.customize-gas {
+ border: 1px solid #D8D8D8;
+ border-radius: 4px;
+ background-color: #FFFFFF;
+ box-shadow: 0 2px 4px 0 rgba(0,0,0,0.14);
+ font-family: Roboto;
+ display: flex;
+ flex-flow: column;
+
+ @media screen and (max-width: $break-small) {
+ width: 100vw;
+ height: 100vh;
+ }
+
+ &__header {
+ height: 52px;
+ border-bottom: 1px solid $alto;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 22px;
+
+ @media screen and (max-width: $break-small) {
+ flex: 0 0 auto;
+ }
+ }
+
+ &__title {
+ margin-left: 19.25px;
+ }
+
+ &__close::after {
+ content: '\00D7';
+ font-size: 1.8em;
+ color: $dusty-gray;
+ font-family: sans-serif;
+ cursor: pointer;
+ margin-right: 19.25px;
+ }
+
+ &__content {
+ display: flex;
+ flex-flow: column nowrap;
+ height: 100%;
+ }
+
+ &__body {
+ display: flex;
+ margin-bottom: 24px;
+
+ @media screen and (max-width: $break-small) {
+ flex-flow: column;
+ flex: 1 1 auto;
+ }
+ }
+
+ &__footer {
+ height: 75px;
+ border-top: 1px solid $alto;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 22px;
+ position: relative;
+
+ @media screen and (max-width: $break-small) {
+ flex: 0 0 auto;
+ }
+ }
+
+ &__buttons {
+ display: flex;
+ justify-content: space-between;
+ margin-right: 21.25px;
+ }
+
+ &__revert, &__cancel, &__save, &__save__error {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 0 3px;
+ cursor: pointer;
+ }
+
+ &__revert {
+ color: $silver-chalice;
+ font-size: 16px;
+ margin-left: 21.25px;
+ }
+
+ &__cancel, &__save, &__save__error {
+ width: 85.74px;
+ min-width: initial;
+ }
+
+ &__save__error {
+ opacity: 0.5;
+ cursor: auto;
+ }
+
+ &__error-message {
+ display: block;
+ position: absolute;
+ top: 4px;
+ right: 4px;
+ font-size: 12px;
+ line-height: 12px;
+ color: $red;
+ }
+}
diff --git a/ui/app/components/modals/index.scss b/ui/app/components/modals/index.scss
index ad6fe16d3..e198cca44 100644
--- a/ui/app/components/modals/index.scss
+++ b/ui/app/components/modals/index.scss
@@ -1,3 +1,5 @@
+@import './customize-gas/index';
+
.modal-container {
width: 100%;
height: 100%;
@@ -18,6 +20,58 @@
font-size: .875rem;
}
+ &__account {
+ border: 1px solid #b7b7b7;
+ border-radius: 4px;
+ padding: 10px;
+ display: flex;
+ margin-top: 10px;
+ margin-bottom: 20px;
+ width: 100%;
+
+ &__identicon {
+ margin-right: 10px;
+ }
+
+ &__name,
+ &__address {
+ margin-right: 10px;
+ font-size: 14px;
+ }
+
+ &__name {
+ width: 100px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ &__label {
+ font-size: 11px;
+ display: block;
+ color: #9b9b9b;
+ }
+
+ &__link {
+ margin-top: 14px;
+
+ img {
+ width: 15px;
+ height: 15px;
+ }
+ }
+
+ @media screen and (max-width: 575px) {
+ &__name {
+ width: 90px;
+ }
+ }
+ }
+
+ &__link {
+ color: #2f9ae0;
+ }
+
&__content {
overflow-y: auto;
flex: 1;
diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js
index 85e85597a..f59825ed1 100644
--- a/ui/app/components/modals/modal.js
+++ b/ui/app/components/modals/modal.js
@@ -20,10 +20,13 @@ const HideTokenConfirmationModal = require('./hide-token-confirmation-modal')
const CustomizeGasModal = require('../customize-gas-modal')
const NotifcationModal = require('./notification-modal')
const ConfirmResetAccount = require('./confirm-reset-account')
+const ConfirmRemoveAccount = require('./confirm-remove-account')
const TransactionConfirmed = require('./transaction-confirmed')
const WelcomeBeta = require('./welcome-beta')
const Notification = require('./notification')
+import ConfirmCustomizeGasModal from './customize-gas'
+
const modalContainerBaseStyle = {
transform: 'translate3d(-50%, 0, 0px)',
border: '1px solid #CCCFD1',
@@ -241,6 +244,19 @@ const MODALS = {
},
},
+ CONFIRM_REMOVE_ACCOUNT: {
+ contents: h(ConfirmRemoveAccount),
+ mobileModalStyle: {
+ ...modalContainerMobileStyle,
+ },
+ laptopModalStyle: {
+ ...modalContainerLaptopStyle,
+ },
+ contentStyle: {
+ borderRadius: '8px',
+ },
+ },
+
NEW_ACCOUNT: {
contents: [
h(NewAccountModal, {}, []),
@@ -267,7 +283,31 @@ const MODALS = {
CUSTOMIZE_GAS: {
contents: [
- h(CustomizeGasModal, {}, []),
+ h(CustomizeGasModal),
+ ],
+ mobileModalStyle: {
+ width: '100vw',
+ height: '100vh',
+ top: '0',
+ transform: 'none',
+ left: '0',
+ right: '0',
+ margin: '0 auto',
+ },
+ laptopModalStyle: {
+ width: '720px',
+ height: '377px',
+ top: '80px',
+ transform: 'none',
+ left: '0',
+ right: '0',
+ margin: '0 auto',
+ },
+ },
+
+ CONFIRM_CUSTOMIZE_GAS: {
+ contents: [
+ h(ConfirmCustomizeGasModal),
],
mobileModalStyle: {
width: '100vw',
diff --git a/ui/app/components/network-display.js b/ui/app/components/network-display.js
deleted file mode 100644
index 59719d9a4..000000000
--- a/ui/app/components/network-display.js
+++ /dev/null
@@ -1,56 +0,0 @@
-const { Component } = require('react')
-const h = require('react-hyperscript')
-const PropTypes = require('prop-types')
-const connect = require('react-redux').connect
-const NetworkDropdownIcon = require('./dropdowns/components/network-dropdown-icon')
-
-const networkToColorHash = {
- 1: '#038789',
- 3: '#e91550',
- 42: '#690496',
- 4: '#ebb33f',
-}
-
-class NetworkDisplay extends Component {
- renderNetworkIcon () {
- const { network } = this.props
- const networkColor = networkToColorHash[network]
-
- return networkColor
- ? h(NetworkDropdownIcon, { backgroundColor: networkColor })
- : h('i.fa.fa-question-circle.fa-med', {
- style: {
- margin: '0 4px',
- color: 'rgb(125, 128, 130)',
- },
- })
- }
-
- render () {
- const { provider: { type } } = this.props
- return h('.network-display__container', [
- this.renderNetworkIcon(),
- h('.network-name', this.context.t(type)),
- ])
- }
-}
-
-NetworkDisplay.propTypes = {
- network: PropTypes.string,
- provider: PropTypes.object,
- t: PropTypes.func,
-}
-
-const mapStateToProps = ({ metamask: { network, provider } }) => {
- return {
- network,
- provider,
- }
-}
-
-NetworkDisplay.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps)(NetworkDisplay)
-
diff --git a/ui/app/components/network-display/index.js b/ui/app/components/network-display/index.js
new file mode 100644
index 000000000..f6878ae5b
--- /dev/null
+++ b/ui/app/components/network-display/index.js
@@ -0,0 +1,2 @@
+import NetworkDisplay from './network-display.container'
+module.exports = NetworkDisplay
diff --git a/ui/app/components/network-display/index.scss b/ui/app/components/network-display/index.scss
new file mode 100644
index 000000000..2085cff67
--- /dev/null
+++ b/ui/app/components/network-display/index.scss
@@ -0,0 +1,54 @@
+.network-display {
+ &__container {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ background-color: lighten(rgb(125, 128, 130), 45%);
+ padding: 0 10px;
+ border-radius: 4px;
+ height: 25px;
+
+ &--mainnet {
+ background-color: lighten($blue-lagoon, 68%);
+ }
+
+ &--ropsten {
+ background-color: lighten($crimson, 45%);
+ }
+
+ &--kovan {
+ background-color: lighten($purple, 65%);
+ }
+
+ &--rinkeby {
+ background-color: lighten($tulip-tree, 35%);
+ }
+ }
+
+ &__name {
+ font-size: .75rem;
+ padding-left: 5px;
+ }
+
+ &__icon {
+ height: 10px;
+ width: 10px;
+ border-radius: 10px;
+
+ &--mainnet {
+ background-color: $blue-lagoon;
+ }
+
+ &--ropsten {
+ background-color: $crimson;
+ }
+
+ &--kovan {
+ background-color: $purple;
+ }
+
+ &--rinkeby {
+ background-color: $tulip-tree;
+ }
+ }
+}
diff --git a/ui/app/components/network-display/network-display.component.js b/ui/app/components/network-display/network-display.component.js
new file mode 100644
index 000000000..38626af20
--- /dev/null
+++ b/ui/app/components/network-display/network-display.component.js
@@ -0,0 +1,69 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import {
+ MAINNET_CODE,
+ ROPSTEN_CODE,
+ RINKEYBY_CODE,
+ KOVAN_CODE,
+} from '../../../../app/scripts/controllers/network/enums'
+
+const networkToClassHash = {
+ [MAINNET_CODE]: 'mainnet',
+ [ROPSTEN_CODE]: 'ropsten',
+ [RINKEYBY_CODE]: 'rinkeby',
+ [KOVAN_CODE]: 'kovan',
+}
+
+export default class NetworkDisplay extends Component {
+ static propTypes = {
+ network: PropTypes.string,
+ provider: PropTypes.object,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ renderNetworkIcon () {
+ const { network } = this.props
+ const networkClass = networkToClassHash[network]
+
+ return networkClass
+ ? <div className={`network-display__icon network-display__icon--${networkClass}`} />
+ : <div
+ className="i fa fa-question-circle fa-med"
+ style={{
+ margin: '0 4px',
+ color: 'rgb(125, 128, 130)',
+ }}
+ />
+ }
+
+ render () {
+ const { network, provider: { type } } = this.props
+ const networkClass = networkToClassHash[network]
+
+ return (
+ <div className={classnames(
+ 'network-display__container',
+ networkClass && ('network-display__container--' + networkClass)
+ )}>
+ {
+ networkClass
+ ? <div className={`network-display__icon network-display__icon--${networkClass}`} />
+ : <div
+ className="i fa fa-question-circle fa-med"
+ style={{
+ margin: '0 4px',
+ color: 'rgb(125, 128, 130)',
+ }}
+ />
+ }
+ <div className="network-display__name">
+ { this.context.t(type) }
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/network-display/network-display.container.js b/ui/app/components/network-display/network-display.container.js
new file mode 100644
index 000000000..99a14fff4
--- /dev/null
+++ b/ui/app/components/network-display/network-display.container.js
@@ -0,0 +1,11 @@
+import { connect } from 'react-redux'
+import NetworkDisplay from './network-display.component'
+
+const mapStateToProps = ({ metamask: { network, provider } }) => {
+ return {
+ network,
+ provider,
+ }
+}
+
+export default connect(mapStateToProps)(NetworkDisplay)
diff --git a/ui/app/components/page-container/index.js b/ui/app/components/page-container/index.js
index 415870b37..913b8c9c6 100644
--- a/ui/app/components/page-container/index.js
+++ b/ui/app/components/page-container/index.js
@@ -1 +1,4 @@
+import PageContainerHeader from './page-container-header'
+import PageContainerFooter from './page-container-footer'
export { default } from './page-container.component'
+export { PageContainerHeader, PageContainerFooter }
diff --git a/ui/app/components/page-container/index.scss b/ui/app/components/page-container/index.scss
new file mode 100644
index 000000000..06c3ef709
--- /dev/null
+++ b/ui/app/components/page-container/index.scss
@@ -0,0 +1,186 @@
+.page-container {
+ width: 408px;
+ background-color: $white;
+ box-shadow: 0 0 7px 0 rgba(0, 0, 0, .08);
+ z-index: 25;
+ display: flex;
+ flex-flow: column;
+ border-radius: 8px;
+
+ &__header {
+ display: flex;
+ flex-flow: column;
+ border-bottom: 1px solid $geyser;
+ padding: 16px;
+ flex: 0 0 auto;
+ position: relative;
+
+ &--no-padding-bottom {
+ padding-bottom: 0;
+ }
+ }
+
+ &__header-close {
+ color: $tundora;
+ position: absolute;
+ top: 16px;
+ right: 16px;
+ cursor: pointer;
+ overflow: hidden;
+
+ &::after {
+ content: '\00D7';
+ font-size: 40px;
+ line-height: 20px;
+ }
+ }
+
+ &__header-row {
+ padding-bottom: 10px;
+ display: flex;
+ justify-content: space-between;
+ }
+
+ &__footer {
+ display: flex;
+ flex-flow: row;
+ justify-content: center;
+ border-top: 1px solid $geyser;
+ padding: 16px;
+ flex: 0 0 auto;
+
+ .btn-default,
+ .btn-confirm {
+ font-size: 1rem;
+ }
+ }
+
+ &__footer-button {
+ height: 55px;
+ font-size: 1rem;
+ text-transform: uppercase;
+ margin-right: 16px;
+
+ &:last-of-type {
+ margin-right: 0;
+ }
+ }
+
+ &__back-button {
+ color: #2f9ae0;
+ font-size: 1rem;
+ cursor: pointer;
+ font-weight: 400;
+ }
+
+ &__title {
+ color: $black;
+ font-size: 2rem;
+ font-weight: 500;
+ line-height: 2rem;
+ }
+
+ &__subtitle {
+ padding-top: .5rem;
+ line-height: initial;
+ font-size: .9rem;
+ color: $gray;
+ }
+
+ &__tabs {
+ display: flex;
+ margin-top: 16px;
+ }
+
+ &__tab {
+ min-width: 5rem;
+ padding: 8px;
+ color: $dusty-gray;
+ font-family: Roboto;
+ font-size: 1rem;
+ text-align: center;
+ cursor: pointer;
+ border-bottom: none;
+ margin-right: 16px;
+
+ &:last-of-type {
+ margin-right: 0;
+ }
+
+ &--selected {
+ color: $curious-blue;
+ border-bottom: 3px solid $curious-blue;
+ }
+ }
+
+ &--full-width {
+ width: 100% !important;
+ }
+
+ &--full-height {
+ height: 100% !important;
+ max-height: initial !important;
+ min-height: initial !important;
+ }
+
+ &__content {
+ overflow-y: auto;
+ flex: 1;
+ }
+
+ &__warning-container {
+ background: $linen;
+ padding: 20px;
+ display: flex;
+ align-items: start;
+ }
+
+ &__warning-message {
+ padding-left: 15px;
+ }
+
+ &__warning-title {
+ font-weight: 500;
+ }
+
+ &__warning-icon {
+ padding-top: 5px;
+ }
+}
+
+@media screen and (max-width: 250px) {
+ .page-container {
+ &__footer {
+ flex-flow: column-reverse;
+ }
+
+ &__footer-button {
+ width: 100%;
+ margin-bottom: 1rem;
+ margin-right: 0;
+
+ &:first-of-type {
+ margin-bottom: 0;
+ }
+ }
+ }
+}
+
+@media screen and (max-width: 575px) {
+ .page-container {
+ height: 100%;
+ width: 100%;
+ overflow-y: auto;
+ background-color: $white;
+ border-radius: 0;
+ flex: 1;
+ }
+}
+
+@media screen and (min-width: 576px) {
+ .page-container {
+ max-height: 82vh;
+ min-height: 570px;
+ flex: 0 0 auto;
+ }
+}
diff --git a/ui/app/components/page-container/page-container-footer/page-container-footer.component.js b/ui/app/components/page-container/page-container-footer/page-container-footer.component.js
index 0458ae78a..3d15df294 100644
--- a/ui/app/components/page-container/page-container-footer/page-container-footer.component.js
+++ b/ui/app/components/page-container/page-container-footer/page-container-footer.component.js
@@ -10,6 +10,7 @@ export default class PageContainerFooter extends Component {
onSubmit: PropTypes.func,
submitText: PropTypes.string,
disabled: PropTypes.bool,
+ submitButtonType: PropTypes.string,
}
static contextTypes = {
@@ -23,6 +24,7 @@ export default class PageContainerFooter extends Component {
onSubmit,
submitText,
disabled,
+ submitButtonType,
} = this.props
return (
@@ -30,16 +32,16 @@ export default class PageContainerFooter extends Component {
<Button
type="default"
- large={true}
+ large
className="page-container__footer-button"
- onClick={() => onCancel()}
+ onClick={e => onCancel(e)}
>
{ cancelText || this.context.t('cancel') }
</Button>
<Button
- type="primary"
- large={true}
+ type={submitButtonType || 'primary'}
+ large
className="page-container__footer-button"
disabled={disabled}
onClick={e => onSubmit(e)}
diff --git a/ui/app/components/page-container/page-container-header.component.js b/ui/app/components/page-container/page-container-header.component.js
deleted file mode 100644
index 5c9d63221..000000000
--- a/ui/app/components/page-container/page-container-header.component.js
+++ /dev/null
@@ -1,35 +0,0 @@
-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>
- )
- }
-
-}
diff --git a/ui/app/components/page-container/page-container-header/page-container-header.component.js b/ui/app/components/page-container/page-container-header/page-container-header.component.js
index 28882edce..5a5de1e5a 100644
--- a/ui/app/components/page-container/page-container-header/page-container-header.component.js
+++ b/ui/app/components/page-container/page-container-header/page-container-header.component.js
@@ -4,13 +4,14 @@ import PropTypes from 'prop-types'
export default class PageContainerHeader extends Component {
static propTypes = {
- title: PropTypes.string.isRequired,
+ title: PropTypes.string,
subtitle: PropTypes.string,
onClose: PropTypes.func,
showBackButton: PropTypes.bool,
onBackButtonClick: PropTypes.func,
backButtonStyles: PropTypes.object,
backButtonString: PropTypes.string,
+ children: PropTypes.node,
};
renderHeaderRow () {
@@ -30,25 +31,33 @@ export default class PageContainerHeader extends Component {
}
render () {
- const { title, subtitle, onClose } = this.props
+ const { title, subtitle, onClose, children } = this.props
return (
<div className="page-container__header">
{ this.renderHeaderRow() }
- <div className="page-container__title">
- {title}
- </div>
+ { children }
- <div className="page-container__subtitle">
- {subtitle}
- </div>
+ {
+ title && <div className="page-container__title">
+ { title }
+ </div>
+ }
- <div
- className="page-container__header-close"
- onClick={() => onClose()}
- />
+ {
+ subtitle && <div className="page-container__subtitle">
+ { subtitle }
+ </div>
+ }
+
+ {
+ onClose && <div
+ className="page-container__header-close"
+ onClick={() => onClose()}
+ />
+ }
</div>
)
diff --git a/ui/app/components/pages/confirm-approve/confirm-approve.component.js b/ui/app/components/pages/confirm-approve/confirm-approve.component.js
new file mode 100644
index 000000000..b71eaa1d4
--- /dev/null
+++ b/ui/app/components/pages/confirm-approve/confirm-approve.component.js
@@ -0,0 +1,21 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ConfirmTokenTransactionBase from '../confirm-token-transaction-base'
+
+export default class ConfirmApprove extends Component {
+ static propTypes = {
+ tokenAmount: PropTypes.number,
+ tokenSymbol: PropTypes.string,
+ }
+
+ render () {
+ const { tokenAmount, tokenSymbol } = this.props
+
+ return (
+ <ConfirmTokenTransactionBase
+ tokenAmount={tokenAmount}
+ warning={`By approving this action, you grant permission for this contract to spend up to ${tokenAmount} of your ${tokenSymbol}.`}
+ />
+ )
+ }
+}
diff --git a/ui/app/components/pages/confirm-approve/confirm-approve.container.js b/ui/app/components/pages/confirm-approve/confirm-approve.container.js
new file mode 100644
index 000000000..4ef9f4ced
--- /dev/null
+++ b/ui/app/components/pages/confirm-approve/confirm-approve.container.js
@@ -0,0 +1,15 @@
+import { connect } from 'react-redux'
+import ConfirmApprove from './confirm-approve.component'
+import { approveTokenAmountAndToAddressSelector } from '../../../selectors/confirm-transaction'
+
+const mapStateToProps = state => {
+ const { confirmTransaction: { tokenProps: { tokenSymbol } = {} } } = state
+ const { tokenAmount } = approveTokenAmountAndToAddressSelector(state)
+
+ return {
+ tokenAmount,
+ tokenSymbol,
+ }
+}
+
+export default connect(mapStateToProps)(ConfirmApprove)
diff --git a/ui/app/components/pages/confirm-approve/index.js b/ui/app/components/pages/confirm-approve/index.js
new file mode 100644
index 000000000..791297be7
--- /dev/null
+++ b/ui/app/components/pages/confirm-approve/index.js
@@ -0,0 +1 @@
+export { default } from './confirm-approve.container'
diff --git a/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.component.js b/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.component.js
new file mode 100644
index 000000000..9bc0daab9
--- /dev/null
+++ b/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.component.js
@@ -0,0 +1,64 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ethUtil from 'ethereumjs-util'
+import ConfirmTransactionBase from '../confirm-transaction-base'
+
+export default class ConfirmDeployContract extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ txData: PropTypes.object,
+ }
+
+ renderData () {
+ const { t } = this.context
+ const {
+ txData: {
+ origin,
+ txParams: {
+ data,
+ } = {},
+ } = {},
+ } = this.props
+
+ return (
+ <div className="confirm-page-container-content__data">
+ <div className="confirm-page-container-content__data-box">
+ <div className="confirm-page-container-content__data-field">
+ <div className="confirm-page-container-content__data-field-label">
+ { `${t('origin')}:` }
+ </div>
+ <div>
+ { origin }
+ </div>
+ </div>
+ <div className="confirm-page-container-content__data-field">
+ <div className="confirm-page-container-content__data-field-label">
+ { `${t('bytes')}:` }
+ </div>
+ <div>
+ { ethUtil.toBuffer(data).length }
+ </div>
+ </div>
+ </div>
+ <div className="confirm-page-container-content__data-box-label">
+ { `${t('hexData')}:` }
+ </div>
+ <div className="confirm-page-container-content__data-box">
+ { data }
+ </div>
+ </div>
+ )
+ }
+
+ render () {
+ return (
+ <ConfirmTransactionBase
+ action={this.context.t('contractDeployment')}
+ dataComponent={this.renderData()}
+ />
+ )
+ }
+}
diff --git a/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.container.js b/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.container.js
new file mode 100644
index 000000000..336ee83ea
--- /dev/null
+++ b/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.container.js
@@ -0,0 +1,12 @@
+import { connect } from 'react-redux'
+import ConfirmDeployContract from './confirm-deploy-contract.component'
+
+const mapStateToProps = state => {
+ const { confirmTransaction: { txData } = {} } = state
+
+ return {
+ txData,
+ }
+}
+
+export default connect(mapStateToProps)(ConfirmDeployContract)
diff --git a/ui/app/components/pages/confirm-deploy-contract/index.js b/ui/app/components/pages/confirm-deploy-contract/index.js
new file mode 100644
index 000000000..c4fb01b52
--- /dev/null
+++ b/ui/app/components/pages/confirm-deploy-contract/index.js
@@ -0,0 +1 @@
+export { default } from './confirm-deploy-contract.container'
diff --git a/ui/app/components/pages/confirm-send-ether/confirm-send-ether.component.js b/ui/app/components/pages/confirm-send-ether/confirm-send-ether.component.js
new file mode 100644
index 000000000..442a478b8
--- /dev/null
+++ b/ui/app/components/pages/confirm-send-ether/confirm-send-ether.component.js
@@ -0,0 +1,39 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ConfirmTransactionBase from '../confirm-transaction-base'
+import { SEND_ROUTE } from '../../../routes'
+
+export default class ConfirmSendEther extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ editTransaction: PropTypes.func,
+ history: PropTypes.object,
+ txParams: PropTypes.object,
+ }
+
+ handleEdit ({ txData }) {
+ const { editTransaction, history } = this.props
+ editTransaction(txData)
+ history.push(SEND_ROUTE)
+ }
+
+ shouldHideData () {
+ const { txParams = {} } = this.props
+ return !txParams.data
+ }
+
+ render () {
+ const hideData = this.shouldHideData()
+
+ return (
+ <ConfirmTransactionBase
+ action={this.context.t('confirm')}
+ hideData={hideData}
+ onEdit={confirmTransactionData => this.handleEdit(confirmTransactionData)}
+ />
+ )
+ }
+}
diff --git a/ui/app/components/pages/confirm-send-ether/confirm-send-ether.container.js b/ui/app/components/pages/confirm-send-ether/confirm-send-ether.container.js
new file mode 100644
index 000000000..e48ef54a8
--- /dev/null
+++ b/ui/app/components/pages/confirm-send-ether/confirm-send-ether.container.js
@@ -0,0 +1,45 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import { withRouter } from 'react-router-dom'
+import { updateSend } from '../../../actions'
+import { clearConfirmTransaction } from '../../../ducks/confirm-transaction.duck'
+import ConfirmSendEther from './confirm-send-ether.component'
+
+const mapStateToProps = state => {
+ const { confirmTransaction: { txData: { txParams } = {} } } = state
+
+ return {
+ txParams,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ editTransaction: txData => {
+ const { id, txParams } = txData
+ const {
+ gas: gasLimit,
+ gasPrice,
+ to,
+ value: amount,
+ } = txParams
+
+ dispatch(updateSend({
+ gasLimit,
+ gasPrice,
+ gasTotal: null,
+ to,
+ amount,
+ errors: { to: null, amount: null },
+ editingTransactionId: id && id.toString(),
+ }))
+
+ dispatch(clearConfirmTransaction())
+ },
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(ConfirmSendEther)
diff --git a/ui/app/components/pages/confirm-send-ether/index.js b/ui/app/components/pages/confirm-send-ether/index.js
new file mode 100644
index 000000000..2d5767c39
--- /dev/null
+++ b/ui/app/components/pages/confirm-send-ether/index.js
@@ -0,0 +1 @@
+export { default } from './confirm-send-ether.container'
diff --git a/ui/app/components/pages/confirm-send-token/confirm-send-token.component.js b/ui/app/components/pages/confirm-send-token/confirm-send-token.component.js
new file mode 100644
index 000000000..cb39e3d7b
--- /dev/null
+++ b/ui/app/components/pages/confirm-send-token/confirm-send-token.component.js
@@ -0,0 +1,29 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ConfirmTokenTransactionBase from '../confirm-token-transaction-base'
+import { SEND_ROUTE } from '../../../routes'
+
+export default class ConfirmSendToken extends Component {
+ static propTypes = {
+ history: PropTypes.object,
+ editTransaction: PropTypes.func,
+ tokenAmount: PropTypes.number,
+ }
+
+ handleEdit (confirmTransactionData) {
+ const { editTransaction, history } = this.props
+ editTransaction(confirmTransactionData)
+ history.push(SEND_ROUTE)
+ }
+
+ render () {
+ const { tokenAmount } = this.props
+
+ return (
+ <ConfirmTokenTransactionBase
+ onEdit={confirmTransactionData => this.handleEdit(confirmTransactionData)}
+ tokenAmount={tokenAmount}
+ />
+ )
+ }
+}
diff --git a/ui/app/components/pages/confirm-send-token/confirm-send-token.container.js b/ui/app/components/pages/confirm-send-token/confirm-send-token.container.js
new file mode 100644
index 000000000..d60911e59
--- /dev/null
+++ b/ui/app/components/pages/confirm-send-token/confirm-send-token.container.js
@@ -0,0 +1,52 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import { withRouter } from 'react-router-dom'
+import ConfirmSendToken from './confirm-send-token.component'
+import { clearConfirmTransaction } from '../../../ducks/confirm-transaction.duck'
+import { setSelectedToken, updateSend, showSendTokenPage } from '../../../actions'
+import { conversionUtil } from '../../../conversion-util'
+import { sendTokenTokenAmountAndToAddressSelector } from '../../../selectors/confirm-transaction'
+
+const mapStateToProps = state => {
+ const { tokenAmount } = sendTokenTokenAmountAndToAddressSelector(state)
+
+ return {
+ tokenAmount,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ editTransaction: ({ txData, tokenData, tokenProps }) => {
+ const { txParams: { to: tokenAddress, gas: gasLimit, gasPrice } = {}, id } = txData
+ const { params = [] } = tokenData
+ const { value: to } = params[0] || {}
+ const { value: tokenAmountInDec } = params[1] || {}
+ const tokenAmountInHex = conversionUtil(tokenAmountInDec, {
+ fromNumericBase: 'dec',
+ toNumericBase: 'hex',
+ })
+ dispatch(setSelectedToken(tokenAddress))
+ dispatch(updateSend({
+ gasLimit,
+ gasPrice,
+ gasTotal: null,
+ to,
+ amount: tokenAmountInHex,
+ errors: { to: null, amount: null },
+ editingTransactionId: id && id.toString(),
+ token: {
+ ...tokenProps,
+ address: tokenAddress,
+ },
+ }))
+ dispatch(clearConfirmTransaction())
+ dispatch(showSendTokenPage())
+ },
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(ConfirmSendToken)
diff --git a/ui/app/components/pages/confirm-send-token/index.js b/ui/app/components/pages/confirm-send-token/index.js
new file mode 100644
index 000000000..409b6ef3d
--- /dev/null
+++ b/ui/app/components/pages/confirm-send-token/index.js
@@ -0,0 +1 @@
+export { default } from './confirm-send-token.container'
diff --git a/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js b/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js
new file mode 100644
index 000000000..365ae216e
--- /dev/null
+++ b/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js
@@ -0,0 +1,85 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ConfirmTransactionBase from '../confirm-transaction-base'
+import {
+ formatCurrency,
+ convertTokenToFiat,
+ addFiat,
+} from '../../../helpers/confirm-transaction/util'
+
+export default class ConfirmTokenTransactionBase extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ tokenAddress: PropTypes.string,
+ toAddress: PropTypes.string,
+ tokenAmount: PropTypes.number,
+ tokenSymbol: PropTypes.string,
+ fiatTransactionTotal: PropTypes.string,
+ ethTransactionTotal: PropTypes.string,
+ contractExchangeRate: PropTypes.number,
+ conversionRate: PropTypes.number,
+ currentCurrency: PropTypes.string,
+ }
+
+ getFiatTransactionAmount () {
+ const { tokenAmount, currentCurrency, conversionRate, contractExchangeRate } = this.props
+
+ return convertTokenToFiat({
+ value: tokenAmount,
+ toCurrency: currentCurrency,
+ conversionRate,
+ contractExchangeRate,
+ })
+ }
+
+ getSubtitle () {
+ const { currentCurrency, contractExchangeRate } = this.props
+
+ if (typeof contractExchangeRate === 'undefined') {
+ return this.context.t('noConversionRateAvailable')
+ } else {
+ const fiatTransactionAmount = this.getFiatTransactionAmount()
+ return formatCurrency(fiatTransactionAmount, currentCurrency)
+ }
+ }
+
+ getFiatTotalTextOverride () {
+ const { fiatTransactionTotal, currentCurrency, contractExchangeRate } = this.props
+
+ if (typeof contractExchangeRate === 'undefined') {
+ return formatCurrency(fiatTransactionTotal, currentCurrency)
+ } else {
+ const fiatTransactionAmount = this.getFiatTransactionAmount()
+ const fiatTotal = addFiat(fiatTransactionAmount, fiatTransactionTotal)
+ return formatCurrency(fiatTotal, currentCurrency)
+ }
+ }
+
+ render () {
+ const {
+ toAddress,
+ tokenAddress,
+ tokenSymbol,
+ tokenAmount,
+ ethTransactionTotal,
+ ...restProps
+ } = this.props
+
+ const tokensText = `${tokenAmount} ${tokenSymbol}`
+
+ return (
+ <ConfirmTransactionBase
+ toAddress={toAddress}
+ identiconAddress={tokenAddress}
+ title={tokensText}
+ subtitle={this.getSubtitle()}
+ ethTotalTextOverride={`${tokensText} + \u2666 ${ethTransactionTotal}`}
+ fiatTotalTextOverride={this.getFiatTotalTextOverride()}
+ {...restProps}
+ />
+ )
+ }
+}
diff --git a/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js b/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js
new file mode 100644
index 000000000..be38acdb0
--- /dev/null
+++ b/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js
@@ -0,0 +1,34 @@
+import { connect } from 'react-redux'
+import ConfirmTokenTransactionBase from './confirm-token-transaction-base.component'
+import {
+ tokenAmountAndToAddressSelector,
+ contractExchangeRateSelector,
+} from '../../../selectors/confirm-transaction'
+
+const mapStateToProps = (state, ownProps) => {
+ const { tokenAmount: ownTokenAmount } = ownProps
+ const { confirmTransaction, metamask: { currentCurrency, conversionRate } } = state
+ const {
+ txData: { txParams: { to: tokenAddress } = {} } = {},
+ tokenProps: { tokenSymbol } = {},
+ fiatTransactionTotal,
+ ethTransactionTotal,
+ } = confirmTransaction
+
+ const { tokenAmount, toAddress } = tokenAmountAndToAddressSelector(state)
+ const contractExchangeRate = contractExchangeRateSelector(state)
+
+ return {
+ toAddress,
+ tokenAddress,
+ tokenAmount: typeof ownTokenAmount !== 'undefined' ? ownTokenAmount : tokenAmount,
+ tokenSymbol,
+ currentCurrency,
+ conversionRate,
+ contractExchangeRate,
+ fiatTransactionTotal,
+ ethTransactionTotal,
+ }
+}
+
+export default connect(mapStateToProps)(ConfirmTokenTransactionBase)
diff --git a/ui/app/components/pages/confirm-token-transaction-base/index.js b/ui/app/components/pages/confirm-token-transaction-base/index.js
new file mode 100644
index 000000000..e15c5d56b
--- /dev/null
+++ b/ui/app/components/pages/confirm-token-transaction-base/index.js
@@ -0,0 +1,2 @@
+export { default } from './confirm-token-transaction-base.container'
+export { default as ConfirmTokenTransactionBase } from './confirm-token-transaction-base.component'
diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js
new file mode 100644
index 000000000..e1bf2210f
--- /dev/null
+++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js
@@ -0,0 +1,330 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ConfirmPageContainer, { ConfirmDetailRow } from '../../confirm-page-container'
+import { formatCurrency } from '../../../helpers/confirm-transaction/util'
+import { isBalanceSufficient } from '../../send/send.utils'
+import { DEFAULT_ROUTE } from '../../../routes'
+import {
+ INSUFFICIENT_FUNDS_ERROR_KEY,
+ TRANSACTION_ERROR_KEY,
+} from '../../../constants/error-keys'
+
+export default class ConfirmTransactionBase extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ // react-router props
+ match: PropTypes.object,
+ history: PropTypes.object,
+ // Redux props
+ balance: PropTypes.string,
+ cancelTransaction: PropTypes.func,
+ clearConfirmTransaction: PropTypes.func,
+ clearSend: PropTypes.func,
+ conversionRate: PropTypes.number,
+ currentCurrency: PropTypes.string,
+ editTransaction: PropTypes.func,
+ ethTransactionAmount: PropTypes.string,
+ ethTransactionFee: PropTypes.string,
+ ethTransactionTotal: PropTypes.string,
+ fiatTransactionAmount: PropTypes.string,
+ fiatTransactionFee: PropTypes.string,
+ fiatTransactionTotal: PropTypes.string,
+ fromAddress: PropTypes.string,
+ fromName: PropTypes.string,
+ hexGasTotal: PropTypes.string,
+ isTxReprice: PropTypes.bool,
+ methodData: PropTypes.object,
+ nonce: PropTypes.string,
+ sendTransaction: PropTypes.func,
+ showCustomizeGasModal: PropTypes.func,
+ showTransactionConfirmedModal: PropTypes.func,
+ toAddress: PropTypes.string,
+ tokenData: PropTypes.object,
+ tokenProps: PropTypes.object,
+ toName: PropTypes.string,
+ transactionStatus: PropTypes.string,
+ txData: PropTypes.object,
+ // Component props
+ action: PropTypes.string,
+ contentComponent: PropTypes.node,
+ dataComponent: PropTypes.node,
+ detailsComponent: PropTypes.node,
+ errorKey: PropTypes.string,
+ errorMessage: PropTypes.string,
+ ethTotalTextOverride: PropTypes.string,
+ fiatTotalTextOverride: PropTypes.string,
+ hideData: PropTypes.bool,
+ hideDetails: PropTypes.bool,
+ hideSubtitle: PropTypes.bool,
+ identiconAddress: PropTypes.string,
+ onCancel: PropTypes.func,
+ onEdit: PropTypes.func,
+ onEditGas: PropTypes.func,
+ onSubmit: PropTypes.func,
+ subtitle: PropTypes.string,
+ summaryComponent: PropTypes.node,
+ title: PropTypes.string,
+ valid: PropTypes.bool,
+ warning: PropTypes.string,
+ }
+
+ componentDidUpdate () {
+ const {
+ transactionStatus,
+ showTransactionConfirmedModal,
+ history,
+ clearConfirmTransaction,
+ } = this.props
+
+ if (transactionStatus === 'dropped') {
+ showTransactionConfirmedModal({
+ onHide: () => {
+ clearConfirmTransaction()
+ history.push(DEFAULT_ROUTE)
+ },
+ })
+
+ return
+ }
+ }
+
+ getErrorKey () {
+ const {
+ balance,
+ conversionRate,
+ hexGasTotal,
+ txData: {
+ simulationFails,
+ txParams: {
+ value: amount,
+ } = {},
+ } = {},
+ } = this.props
+
+ const insufficientBalance = balance && !isBalanceSufficient({
+ amount,
+ gasTotal: hexGasTotal || '0x0',
+ balance,
+ conversionRate,
+ })
+
+ if (insufficientBalance) {
+ return {
+ valid: false,
+ errorKey: INSUFFICIENT_FUNDS_ERROR_KEY,
+ }
+ }
+
+ if (simulationFails) {
+ return {
+ valid: false,
+ errorKey: TRANSACTION_ERROR_KEY,
+ }
+ }
+
+ return {
+ valid: true,
+ }
+ }
+
+ handleEditGas () {
+ const { onEditGas, showCustomizeGasModal } = this.props
+
+ if (onEditGas) {
+ onEditGas()
+ } else {
+ showCustomizeGasModal()
+ }
+ }
+
+ renderDetails () {
+ const {
+ detailsComponent,
+ fiatTransactionFee,
+ ethTransactionFee,
+ currentCurrency,
+ fiatTransactionTotal,
+ ethTransactionTotal,
+ fiatTotalTextOverride,
+ ethTotalTextOverride,
+ hideDetails,
+ } = this.props
+
+ if (hideDetails) {
+ return null
+ }
+
+ const formattedCurrency = formatCurrency(fiatTransactionTotal, currentCurrency)
+
+ return (
+ detailsComponent || (
+ <div className="confirm-page-container-content__details">
+ <div className="confirm-page-container-content__gas-fee">
+ <ConfirmDetailRow
+ label="Gas Fee"
+ fiatText={formatCurrency(fiatTransactionFee, currentCurrency)}
+ ethText={`\u2666 ${ethTransactionFee}`}
+ headerText="Edit"
+ headerTextClassName="confirm-detail-row__header-text--edit"
+ onHeaderClick={() => this.handleEditGas()}
+ />
+ </div>
+ <div>
+ <ConfirmDetailRow
+ label="Total"
+ fiatText={fiatTotalTextOverride || formattedCurrency}
+ ethText={ethTotalTextOverride || `\u2666 ${ethTransactionTotal}`}
+ headerText="Amount + Gas Fee"
+ headerTextClassName="confirm-detail-row__header-text--total"
+ fiatTextColor="#2f9ae0"
+ />
+ </div>
+ </div>
+ )
+ )
+ }
+
+ renderData () {
+ const { t } = this.context
+ const {
+ txData: {
+ txParams: {
+ data,
+ } = {},
+ } = {},
+ methodData: {
+ name,
+ params,
+ } = {},
+ hideData,
+ dataComponent,
+ } = this.props
+
+ if (hideData) {
+ return null
+ }
+
+ return dataComponent || (
+ <div className="confirm-page-container-content__data">
+ <div className="confirm-page-container-content__data-box-label">
+ {`${t('functionType')}:`}
+ <span className="confirm-page-container-content__function-type">
+ { name || t('notFound') }
+ </span>
+ </div>
+ {
+ params && (
+ <div className="confirm-page-container-content__data-box">
+ <div className="confirm-page-container-content__data-field-label">
+ { `${t('parameters')}:` }
+ </div>
+ <div>
+ <pre>{ JSON.stringify(params, null, 2) }</pre>
+ </div>
+ </div>
+ )
+ }
+ <div className="confirm-page-container-content__data-box-label">
+ {`${t('hexData')}:`}
+ </div>
+ <div className="confirm-page-container-content__data-box">
+ { data }
+ </div>
+ </div>
+ )
+ }
+
+ handleEdit () {
+ const { txData, tokenData, tokenProps, onEdit } = this.props
+ onEdit({ txData, tokenData, tokenProps })
+ }
+
+ handleCancel () {
+ const { onCancel, txData, cancelTransaction, history, clearConfirmTransaction } = this.props
+
+ if (onCancel) {
+ onCancel(txData)
+ } else {
+ cancelTransaction(txData)
+ .then(() => {
+ clearConfirmTransaction()
+ history.push(DEFAULT_ROUTE)
+ })
+ }
+ }
+
+ handleSubmit () {
+ const { sendTransaction, clearConfirmTransaction, txData, history, onSubmit } = this.props
+
+ if (onSubmit) {
+ onSubmit(txData)
+ } else {
+ sendTransaction(txData)
+ .then(() => {
+ clearConfirmTransaction()
+ history.push(DEFAULT_ROUTE)
+ })
+ }
+ }
+
+ render () {
+ const {
+ isTxReprice,
+ fromName,
+ fromAddress,
+ toName,
+ toAddress,
+ methodData,
+ ethTransactionAmount,
+ fiatTransactionAmount,
+ valid: propsValid,
+ errorMessage,
+ errorKey: propsErrorKey,
+ currentCurrency,
+ action,
+ title,
+ subtitle,
+ hideSubtitle,
+ identiconAddress,
+ summaryComponent,
+ contentComponent,
+ onEdit,
+ nonce,
+ warning,
+ } = this.props
+
+ const { name } = methodData
+ const fiatConvertedAmount = formatCurrency(fiatTransactionAmount, currentCurrency)
+ const { valid, errorKey } = this.getErrorKey()
+
+ return (
+ <ConfirmPageContainer
+ fromName={fromName}
+ fromAddress={fromAddress}
+ toName={toName}
+ toAddress={toAddress}
+ showEdit={onEdit && !isTxReprice}
+ action={action || name || this.context.t('unknownFunction')}
+ title={title || `${fiatConvertedAmount} ${currentCurrency.toUpperCase()}`}
+ subtitle={subtitle || `\u2666 ${ethTransactionAmount}`}
+ hideSubtitle={hideSubtitle}
+ summaryComponent={summaryComponent}
+ detailsComponent={this.renderDetails()}
+ dataComponent={this.renderData()}
+ contentComponent={contentComponent}
+ nonce={nonce}
+ identiconAddress={identiconAddress}
+ errorMessage={errorMessage}
+ errorKey={propsErrorKey || errorKey}
+ warning={warning}
+ valid={propsValid || valid}
+ onEdit={() => this.handleEdit()}
+ onCancel={() => this.handleCancel()}
+ onSubmit={() => this.handleSubmit()}
+ />
+ )
+ }
+}
diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js
new file mode 100644
index 000000000..0c0deff18
--- /dev/null
+++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js
@@ -0,0 +1,181 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import { withRouter } from 'react-router-dom'
+import R from 'ramda'
+import contractMap from 'eth-contract-metadata'
+import ConfirmTransactionBase from './confirm-transaction-base.component'
+import {
+ clearConfirmTransaction,
+ updateGasAndCalculate,
+} from '../../../ducks/confirm-transaction.duck'
+import { clearSend, cancelTx, updateAndApproveTx, showModal } from '../../../actions'
+import {
+ INSUFFICIENT_FUNDS_ERROR_KEY,
+ GAS_LIMIT_TOO_LOW_ERROR_KEY,
+} from '../../../constants/error-keys'
+import { getHexGasTotal } from '../../../helpers/confirm-transaction/util'
+import { isBalanceSufficient } from '../../send/send.utils'
+import { conversionGreaterThan } from '../../../conversion-util'
+import { MIN_GAS_LIMIT_DEC } from '../../send/send.constants'
+import { addressSlicer } from '../../../util'
+
+const casedContractMap = Object.keys(contractMap).reduce((acc, base) => {
+ return {
+ ...acc,
+ [base.toLowerCase()]: contractMap[base],
+ }
+}, {})
+
+const mapStateToProps = (state, props) => {
+ const { toAddress: propsToAddress } = props
+ const { confirmTransaction, metamask } = state
+ const {
+ ethTransactionAmount,
+ ethTransactionFee,
+ ethTransactionTotal,
+ fiatTransactionAmount,
+ fiatTransactionFee,
+ fiatTransactionTotal,
+ hexGasTotal,
+ tokenData,
+ methodData,
+ txData,
+ tokenProps,
+ nonce,
+ } = confirmTransaction
+ const { txParams = {}, lastGasPrice, id: transactionId } = txData
+ const { from: fromAddress, to: txParamsToAddress } = txParams
+ const {
+ conversionRate,
+ identities,
+ currentCurrency,
+ accounts,
+ selectedAddress,
+ selectedAddressTxList,
+ } = metamask
+
+ const { balance } = accounts[selectedAddress]
+ const { name: fromName } = identities[selectedAddress]
+ const toAddress = propsToAddress || txParamsToAddress
+ const toName = identities[toAddress]
+ ? identities[toAddress].name
+ : casedContractMap[toAddress] ? casedContractMap[toAddress].name : addressSlicer(toAddress)
+
+ const isTxReprice = Boolean(lastGasPrice)
+
+ const transaction = R.find(({ id }) => id === transactionId)(selectedAddressTxList)
+ const transactionStatus = transaction ? transaction.status : ''
+
+ return {
+ balance,
+ fromAddress,
+ fromName,
+ toAddress,
+ toName,
+ ethTransactionAmount,
+ ethTransactionFee,
+ ethTransactionTotal,
+ fiatTransactionAmount,
+ fiatTransactionFee,
+ fiatTransactionTotal,
+ hexGasTotal,
+ txData,
+ tokenData,
+ methodData,
+ tokenProps,
+ isTxReprice,
+ currentCurrency,
+ conversionRate,
+ transactionStatus,
+ nonce,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
+ clearSend: () => dispatch(clearSend()),
+ showTransactionConfirmedModal: ({ onHide }) => {
+ return dispatch(showModal({ name: 'TRANSACTION_CONFIRMED', onHide }))
+ },
+ showCustomizeGasModal: ({ txData, onSubmit, validate }) => {
+ return dispatch(showModal({ name: 'CONFIRM_CUSTOMIZE_GAS', txData, onSubmit, validate }))
+ },
+ updateGasAndCalculate: ({ gasLimit, gasPrice }) => {
+ return dispatch(updateGasAndCalculate({ gasLimit, gasPrice }))
+ },
+ cancelTransaction: ({ id }) => dispatch(cancelTx({ id })),
+ sendTransaction: txData => dispatch(updateAndApproveTx(txData)),
+ }
+}
+
+const getValidateEditGas = ({ balance, conversionRate, txData }) => {
+ const { txParams: { value: amount } = {} } = txData
+
+ return ({ gasLimit, gasPrice }) => {
+ const gasTotal = getHexGasTotal({ gasLimit, gasPrice })
+ const hasSufficientBalance = isBalanceSufficient({
+ amount,
+ gasTotal,
+ balance,
+ conversionRate,
+ })
+
+ if (!hasSufficientBalance) {
+ return {
+ valid: false,
+ errorKey: INSUFFICIENT_FUNDS_ERROR_KEY,
+ }
+ }
+
+ const gasLimitTooLow = gasLimit && conversionGreaterThan(
+ {
+ value: MIN_GAS_LIMIT_DEC,
+ fromNumericBase: 'dec',
+ conversionRate,
+ },
+ {
+ value: gasLimit,
+ fromNumericBase: 'hex',
+ },
+ )
+
+ if (gasLimitTooLow) {
+ return {
+ valid: false,
+ errorKey: GAS_LIMIT_TOO_LOW_ERROR_KEY,
+ }
+ }
+
+ return {
+ valid: true,
+ }
+ }
+}
+
+const mergeProps = (stateProps, dispatchProps, ownProps) => {
+ const { balance, conversionRate, txData } = stateProps
+ const {
+ showCustomizeGasModal: dispatchShowCustomizeGasModal,
+ updateGasAndCalculate: dispatchUpdateGasAndCalculate,
+ ...otherDispatchProps
+ } = dispatchProps
+
+ const validateEditGas = getValidateEditGas({ balance, conversionRate, txData })
+
+ return {
+ ...stateProps,
+ ...otherDispatchProps,
+ ...ownProps,
+ showCustomizeGasModal: () => dispatchShowCustomizeGasModal({
+ txData,
+ onSubmit: txData => dispatchUpdateGasAndCalculate(txData),
+ validate: validateEditGas,
+ }),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps, mergeProps)
+)(ConfirmTransactionBase)
diff --git a/ui/app/components/pages/confirm-transaction-base/index.js b/ui/app/components/pages/confirm-transaction-base/index.js
new file mode 100644
index 000000000..9996e9aeb
--- /dev/null
+++ b/ui/app/components/pages/confirm-transaction-base/index.js
@@ -0,0 +1 @@
+export { default } from './confirm-transaction-base.container'
diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js
new file mode 100644
index 000000000..0280f73c6
--- /dev/null
+++ b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js
@@ -0,0 +1,85 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { Redirect } from 'react-router-dom'
+import Loading from '../../loading-screen'
+import {
+ CONFIRM_TRANSACTION_ROUTE,
+ CONFIRM_DEPLOY_CONTRACT_PATH,
+ CONFIRM_SEND_ETHER_PATH,
+ CONFIRM_SEND_TOKEN_PATH,
+ CONFIRM_APPROVE_PATH,
+ CONFIRM_TRANSFER_FROM_PATH,
+ CONFIRM_TOKEN_METHOD_PATH,
+ SIGNATURE_REQUEST_PATH,
+} from '../../../routes'
+import { isConfirmDeployContract } from './confirm-transaction-switch.util'
+import {
+ TOKEN_METHOD_TRANSFER,
+ TOKEN_METHOD_APPROVE,
+ TOKEN_METHOD_TRANSFER_FROM,
+} from './confirm-transaction-switch.constants'
+
+export default class ConfirmTransactionSwitch extends Component {
+ static propTypes = {
+ txData: PropTypes.object,
+ methodData: PropTypes.object,
+ fetchingMethodData: PropTypes.bool,
+ }
+
+ redirectToTransaction () {
+ const {
+ txData,
+ methodData: { name },
+ fetchingMethodData,
+ } = this.props
+ const { id, txParams: { data } = {} } = txData
+
+ if (isConfirmDeployContract(txData)) {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_DEPLOY_CONTRACT_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+
+ if (fetchingMethodData) {
+ return <Loading />
+ }
+
+ if (data) {
+ const methodName = name && name.toLowerCase()
+
+ switch (methodName) {
+ case TOKEN_METHOD_TRANSFER: {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_TOKEN_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+ case TOKEN_METHOD_APPROVE: {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_APPROVE_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+ case TOKEN_METHOD_TRANSFER_FROM: {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_TRANSFER_FROM_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+ default: {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_TOKEN_METHOD_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+ }
+ }
+
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_ETHER_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+
+ render () {
+ const { txData } = this.props
+
+ if (txData.txParams) {
+ return this.redirectToTransaction()
+ } else if (txData.msgParams) {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${txData.id}${SIGNATURE_REQUEST_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+
+ return <Loading />
+ }
+}
diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js
new file mode 100644
index 000000000..9db4a2f96
--- /dev/null
+++ b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js
@@ -0,0 +1,3 @@
+export const TOKEN_METHOD_TRANSFER = 'transfer'
+export const TOKEN_METHOD_APPROVE = 'approve'
+export const TOKEN_METHOD_TRANSFER_FROM = 'transferfrom'
diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js
new file mode 100644
index 000000000..3d7fc78cc
--- /dev/null
+++ b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js
@@ -0,0 +1,20 @@
+import { connect } from 'react-redux'
+import ConfirmTransactionSwitch from './confirm-transaction-switch.component'
+
+const mapStateToProps = state => {
+ const {
+ confirmTransaction: {
+ txData,
+ methodData,
+ fetchingMethodData,
+ },
+ } = state
+
+ return {
+ txData,
+ methodData,
+ fetchingMethodData,
+ }
+}
+
+export default connect(mapStateToProps)(ConfirmTransactionSwitch)
diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.util.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.util.js
new file mode 100644
index 000000000..536aa5212
--- /dev/null
+++ b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.util.js
@@ -0,0 +1,4 @@
+export function isConfirmDeployContract (txData = {}) {
+ const { txParams = {} } = txData
+ return !txParams.to
+}
diff --git a/ui/app/components/pages/confirm-transaction-switch/index.js b/ui/app/components/pages/confirm-transaction-switch/index.js
new file mode 100644
index 000000000..c288acb1a
--- /dev/null
+++ b/ui/app/components/pages/confirm-transaction-switch/index.js
@@ -0,0 +1,2 @@
+import ConfirmTransactionSwitch from './confirm-transaction-switch.container'
+module.exports = ConfirmTransactionSwitch
diff --git a/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js b/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js
new file mode 100644
index 000000000..3ac656d73
--- /dev/null
+++ b/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js
@@ -0,0 +1,157 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { Switch, Route } from 'react-router-dom'
+import Loading from '../../loading-screen'
+import ConfirmTransactionSwitch from '../confirm-transaction-switch'
+import ConfirmTransactionBase from '../confirm-transaction-base'
+import ConfirmSendEther from '../confirm-send-ether'
+import ConfirmSendToken from '../confirm-send-token'
+import ConfirmDeployContract from '../confirm-deploy-contract'
+import ConfirmApprove from '../confirm-approve'
+import ConfirmTokenTransactionBase from '../confirm-token-transaction-base'
+import ConfTx from '../../../conf-tx'
+import {
+ DEFAULT_ROUTE,
+ CONFIRM_TRANSACTION_ROUTE,
+ CONFIRM_DEPLOY_CONTRACT_PATH,
+ CONFIRM_SEND_ETHER_PATH,
+ CONFIRM_SEND_TOKEN_PATH,
+ CONFIRM_APPROVE_PATH,
+ CONFIRM_TRANSFER_FROM_PATH,
+ CONFIRM_TOKEN_METHOD_PATH,
+ SIGNATURE_REQUEST_PATH,
+} from '../../../routes'
+
+export default class ConfirmTransaction extends Component {
+ static propTypes = {
+ history: PropTypes.object.isRequired,
+ totalUnapprovedCount: PropTypes.number.isRequired,
+ match: PropTypes.object,
+ send: PropTypes.object,
+ unconfirmedTransactions: PropTypes.array,
+ setTransactionToConfirm: PropTypes.func,
+ confirmTransaction: PropTypes.object,
+ clearConfirmTransaction: PropTypes.func,
+ }
+
+ getParamsTransactionId () {
+ const { match: { params: { id } = {} } } = this.props
+ return id || null
+ }
+
+ componentDidMount () {
+ const {
+ totalUnapprovedCount = 0,
+ send = {},
+ history,
+ confirmTransaction: { txData: { id: transactionId } = {} },
+ } = this.props
+
+ if (!totalUnapprovedCount && !send.to) {
+ history.replace(DEFAULT_ROUTE)
+ return
+ }
+
+ if (!transactionId) {
+ this.setTransactionToConfirm()
+ }
+ }
+
+ componentDidUpdate () {
+ const {
+ setTransactionToConfirm,
+ confirmTransaction: { txData: { id: transactionId } = {} },
+ clearConfirmTransaction,
+ } = this.props
+ const paramsTransactionId = this.getParamsTransactionId()
+
+ if (paramsTransactionId && transactionId && paramsTransactionId !== transactionId + '') {
+ clearConfirmTransaction()
+ setTransactionToConfirm(paramsTransactionId)
+ return
+ }
+
+ if (!transactionId) {
+ this.setTransactionToConfirm()
+ }
+ }
+
+ setTransactionToConfirm () {
+ const {
+ history,
+ unconfirmedTransactions,
+ setTransactionToConfirm,
+ } = this.props
+ const paramsTransactionId = this.getParamsTransactionId()
+
+ if (paramsTransactionId) {
+ // Check to make sure params ID is valid
+ const tx = unconfirmedTransactions.find(({ id }) => id + '' === paramsTransactionId)
+
+ if (!tx) {
+ history.replace(DEFAULT_ROUTE)
+ } else {
+ setTransactionToConfirm(paramsTransactionId)
+ }
+ } else if (unconfirmedTransactions.length) {
+ const totalUnconfirmed = unconfirmedTransactions.length
+ const transaction = unconfirmedTransactions[totalUnconfirmed - 1]
+ const { id: transactionId, loadingDefaults } = transaction
+
+ if (!loadingDefaults) {
+ setTransactionToConfirm(transactionId)
+ }
+ }
+ }
+
+ render () {
+ const { confirmTransaction: { txData: { id } } = {} } = this.props
+ const paramsTransactionId = this.getParamsTransactionId()
+
+ // Show routes when state.confirmTransaction has been set and when either the ID in the params
+ // isn't specified or is specified and matches the ID in state.confirmTransaction in order to
+ // support URLs of /confirm-transaction or /confirm-transaction/<transactionId>
+ return id && (!paramsTransactionId || paramsTransactionId === id + '')
+ ? (
+ <Switch>
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_DEPLOY_CONTRACT_PATH}`}
+ component={ConfirmDeployContract}
+ />
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_TOKEN_METHOD_PATH}`}
+ component={ConfirmTransactionBase}
+ />
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_SEND_ETHER_PATH}`}
+ component={ConfirmSendEther}
+ />
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_SEND_TOKEN_PATH}`}
+ component={ConfirmSendToken}
+ />
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_APPROVE_PATH}`}
+ component={ConfirmApprove}
+ />
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_TRANSFER_FROM_PATH}`}
+ component={ConfirmTokenTransactionBase}
+ />
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${SIGNATURE_REQUEST_PATH}`}
+ component={ConfTx}
+ />
+ <Route path="*" component={ConfirmTransactionSwitch} />
+ </Switch>
+ )
+ : <Loading />
+ }
+}
diff --git a/ui/app/components/pages/confirm-transaction/confirm-transaction.container.js b/ui/app/components/pages/confirm-transaction/confirm-transaction.container.js
new file mode 100644
index 000000000..1bc2f1efb
--- /dev/null
+++ b/ui/app/components/pages/confirm-transaction/confirm-transaction.container.js
@@ -0,0 +1,33 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import { withRouter } from 'react-router-dom'
+import {
+ setTransactionToConfirm,
+ clearConfirmTransaction,
+} from '../../../ducks/confirm-transaction.duck'
+import ConfirmTransaction from './confirm-transaction.component'
+import { getTotalUnapprovedCount } from '../../../selectors'
+import { unconfirmedTransactionsListSelector } from '../../../selectors/confirm-transaction'
+
+const mapStateToProps = state => {
+ const { metamask: { send }, confirmTransaction } = state
+
+ return {
+ totalUnapprovedCount: getTotalUnapprovedCount(state),
+ send,
+ confirmTransaction,
+ unconfirmedTransactions: unconfirmedTransactionsListSelector(state),
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ setTransactionToConfirm: transactionId => dispatch(setTransactionToConfirm(transactionId)),
+ clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps),
+)(ConfirmTransaction)
diff --git a/ui/app/components/pages/confirm-transaction/index.js b/ui/app/components/pages/confirm-transaction/index.js
new file mode 100644
index 000000000..4bf42d85c
--- /dev/null
+++ b/ui/app/components/pages/confirm-transaction/index.js
@@ -0,0 +1,2 @@
+import ConfirmTransaction from './confirm-transaction.container'
+module.exports = ConfirmTransaction
diff --git a/ui/app/components/pages/create-account/connect-hardware/account-list.js b/ui/app/components/pages/create-account/connect-hardware/account-list.js
new file mode 100644
index 000000000..c722d1f55
--- /dev/null
+++ b/ui/app/components/pages/create-account/connect-hardware/account-list.js
@@ -0,0 +1,143 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const genAccountLink = require('../../../../../lib/account-link.js')
+
+class AccountList extends Component {
+ constructor (props, context) {
+ super(props)
+ }
+
+ renderHeader () {
+ return (
+ h('div.hw-connect', [
+ h('h3.hw-connect__title', {}, this.context.t('selectAnAccount')),
+ h('p.hw-connect__msg', {}, this.context.t('selectAnAccountHelp')),
+ ])
+ )
+ }
+
+ renderAccounts () {
+ return h('div.hw-account-list', [
+ this.props.accounts.map((a, i) => {
+
+ return h('div.hw-account-list__item', { key: a.address }, [
+ h('div.hw-account-list__item__radio', [
+ h('input', {
+ type: 'radio',
+ name: 'selectedAccount',
+ id: `address-${i}`,
+ value: a.index,
+ onChange: (e) => this.props.onAccountChange(e.target.value),
+ checked: this.props.selectedAccount === a.index.toString(),
+ }),
+ h(
+ 'label.hw-account-list__item__label',
+ {
+ htmlFor: `address-${i}`,
+ },
+ [
+ h('span.hw-account-list__item__index', a.index + 1),
+ `${a.address.slice(0, 4)}...${a.address.slice(-4)}`,
+ h('span.hw-account-list__item__balance', `${a.balance}`),
+ ]),
+ ]),
+ h(
+ 'a.hw-account-list__item__link',
+ {
+ href: genAccountLink(a.address, this.props.network),
+ target: '_blank',
+ title: this.context.t('etherscanView'),
+ },
+ h('img', { src: 'images/popout.svg' })
+ ),
+ ])
+ }),
+ ])
+ }
+
+ renderPagination () {
+ return h('div.hw-list-pagination', [
+ h(
+ 'button.hw-list-pagination__button',
+ {
+ onClick: () => this.props.getPage(-1),
+ },
+ `< ${this.context.t('prev')}`
+ ),
+
+ h(
+ 'button.hw-list-pagination__button',
+ {
+ onClick: () => this.props.getPage(1),
+ },
+ `${this.context.t('next')} >`
+ ),
+ ])
+ }
+
+ renderButtons () {
+ const disabled = this.props.selectedAccount === null
+ const buttonProps = {}
+ if (disabled) {
+ buttonProps.disabled = true
+ }
+
+ return h('div.new-account-connect-form__buttons', {}, [
+ h(
+ 'button.btn-default.btn--large.new-account-connect-form__button',
+ {
+ onClick: this.props.onCancel.bind(this),
+ },
+ [this.context.t('cancel')]
+ ),
+
+ h(
+ `button.btn-primary.btn--large.new-account-connect-form__button.unlock ${disabled ? '.btn-primary--disabled' : ''}`,
+ {
+ onClick: this.props.onUnlockAccount.bind(this),
+ ...buttonProps,
+ },
+ [this.context.t('unlock')]
+ ),
+ ])
+ }
+
+ renderForgetDevice () {
+ return h('div.hw-forget-device-container', {}, [
+ h('a', {
+ onClick: this.props.onForgetDevice.bind(this),
+ }, this.context.t('forgetDevice')),
+ ])
+ }
+
+ render () {
+ return h('div.new-account-connect-form.account-list', {}, [
+ this.renderHeader(),
+ this.renderAccounts(),
+ this.renderPagination(),
+ this.renderButtons(),
+ this.renderForgetDevice(),
+ ])
+ }
+
+}
+
+
+AccountList.propTypes = {
+ accounts: PropTypes.array.isRequired,
+ onAccountChange: PropTypes.func.isRequired,
+ onForgetDevice: PropTypes.func.isRequired,
+ getPage: PropTypes.func.isRequired,
+ network: PropTypes.string,
+ selectedAccount: PropTypes.string,
+ history: PropTypes.object,
+ onUnlockAccount: PropTypes.func,
+ onCancel: PropTypes.func,
+}
+
+AccountList.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = AccountList
diff --git a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js
new file mode 100644
index 000000000..cb2b86595
--- /dev/null
+++ b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js
@@ -0,0 +1,149 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+
+class ConnectScreen extends Component {
+ constructor (props, context) {
+ super(props)
+ }
+
+ renderUnsupportedBrowser () {
+ return (
+ h('div.new-account-connect-form.unsupported-browser', {}, [
+ h('div.hw-connect', [
+ h('h3.hw-connect__title', {}, this.context.t('browserNotSupported')),
+ h('p.hw-connect__msg', {}, this.context.t('chromeRequiredForTrezor')),
+ ]),
+ h(
+ 'button.btn-primary.btn--large',
+ {
+ onClick: () => global.platform.openWindow({
+ url: 'https://google.com/chrome',
+ }),
+ },
+ this.context.t('downloadGoogleChrome')
+ ),
+ ])
+ )
+ }
+
+ renderHeader () {
+ return (
+ h('div.hw-connect__header', {}, [
+ h('h3.hw-connect__header__title', {}, this.context.t(`hardwareSupport`)),
+ h('p.hw-connect__header__msg', {}, this.context.t(`hardwareSupportMsg`)),
+ ])
+ )
+ }
+
+ renderTrezorAffiliateLink () {
+ return h('div.hw-connect__get-trezor', {}, [
+ h('p.hw-connect__get-trezor__msg', {}, this.context.t(`dontHaveATrezorWallet`)),
+ h('a.hw-connect__get-trezor__link', {
+ href: 'https://shop.trezor.io/?a=metamask',
+ target: '_blank',
+ }, this.context.t('orderOneHere')),
+ ])
+ }
+
+ renderConnectToTrezorButton () {
+ return h(
+ 'button.btn-primary.btn--large',
+ { onClick: this.props.connectToTrezor.bind(this) },
+ this.props.btnText
+ )
+ }
+
+ scrollToTutorial = (e) => {
+ if (this.referenceNode) this.referenceNode.scrollIntoView({behavior: 'smooth'})
+ }
+
+ renderLearnMore () {
+ return (
+ h('p.hw-connect__learn-more', {
+ onClick: this.scrollToTutorial,
+ }, [
+ this.context.t('learnMore'),
+ h('img.hw-connect__learn-more__arrow', { src: 'images/caret-right.svg'}),
+ ])
+ )
+ }
+
+ renderTutorialSteps () {
+ const steps = [
+ {
+ asset: 'hardware-wallet-step-1',
+ dimensions: {width: '225px', height: '75px'},
+ },
+ {
+ asset: 'hardware-wallet-step-2',
+ dimensions: {width: '300px', height: '100px'},
+ },
+ {
+ asset: 'hardware-wallet-step-3',
+ dimensions: {width: '120px', height: '90px'},
+ },
+ ]
+
+ return h('.hw-tutorial', {
+ ref: node => { this.referenceNode = node },
+ },
+ steps.map((step, i) => (
+ h('div.hw-connect', {}, [
+ h('h3.hw-connect__title', {}, this.context.t(`step${i + 1}HardwareWallet`)),
+ h('p.hw-connect__msg', {}, this.context.t(`step${i + 1}HardwareWalletMsg`)),
+ h('img.hw-connect__step-asset', { src: `images/${step.asset}.svg`, ...step.dimensions }),
+ ])
+ ))
+ )
+ }
+
+ renderFooter () {
+ return (
+ h('div.hw-connect__footer', {}, [
+ h('h3.hw-connect__footer__title', {}, this.context.t(`readyToConnect`)),
+ this.renderConnectToTrezorButton(),
+ h('p.hw-connect__footer__msg', {}, [
+ this.context.t(`havingTroubleConnecting`),
+ h('a.hw-connect__footer__link', {
+ href: 'https://support.metamask.io/',
+ target: '_blank',
+ }, this.context.t('getHelp')),
+ ]),
+ ])
+ )
+ }
+
+ renderConnectScreen () {
+ return (
+ h('div.new-account-connect-form', {}, [
+ this.renderHeader(),
+ this.renderTrezorAffiliateLink(),
+ this.renderConnectToTrezorButton(),
+ this.renderLearnMore(),
+ this.renderTutorialSteps(),
+ this.renderFooter(),
+ ])
+ )
+ }
+
+ render () {
+ if (this.props.browserSupported) {
+ return this.renderConnectScreen()
+ }
+ return this.renderUnsupportedBrowser()
+ }
+}
+
+ConnectScreen.propTypes = {
+ connectToTrezor: PropTypes.func.isRequired,
+ btnText: PropTypes.string.isRequired,
+ browserSupported: PropTypes.bool.isRequired,
+}
+
+ConnectScreen.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = ConnectScreen
+
diff --git a/ui/app/components/pages/create-account/connect-hardware/index.js b/ui/app/components/pages/create-account/connect-hardware/index.js
new file mode 100644
index 000000000..3f66e7098
--- /dev/null
+++ b/ui/app/components/pages/create-account/connect-hardware/index.js
@@ -0,0 +1,241 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('../../../../actions')
+const ConnectScreen = require('./connect-screen')
+const AccountList = require('./account-list')
+const { DEFAULT_ROUTE } = require('../../../../routes')
+const { formatBalance } = require('../../../../util')
+
+class ConnectHardwareForm extends Component {
+ constructor (props, context) {
+ super(props)
+ this.state = {
+ error: null,
+ btnText: context.t('connectToTrezor'),
+ selectedAccount: null,
+ accounts: [],
+ browserSupported: true,
+ unlocked: false,
+ }
+ }
+
+ componentWillReceiveProps (nextProps) {
+ const { accounts } = nextProps
+ const newAccounts = this.state.accounts.map(a => {
+ const normalizedAddress = a.address.toLowerCase()
+ const balanceValue = accounts[normalizedAddress] && accounts[normalizedAddress].balance || null
+ a.balance = balanceValue ? formatBalance(balanceValue, 6) : '...'
+ return a
+ })
+ this.setState({accounts: newAccounts})
+ }
+
+
+ componentDidMount () {
+ this.checkIfUnlocked()
+ }
+
+ async checkIfUnlocked () {
+ const unlocked = await this.props.checkHardwareStatus('trezor')
+ if (unlocked) {
+ this.setState({unlocked: true})
+ this.getPage(0)
+ }
+ }
+
+ connectToTrezor = () => {
+ if (this.state.accounts.length) {
+ return null
+ }
+ this.setState({ btnText: this.context.t('connecting')})
+ this.getPage(0)
+ }
+
+ onAccountChange = (account) => {
+ this.setState({selectedAccount: account.toString(), error: null})
+ }
+
+ showTemporaryAlert () {
+ this.props.showAlert(this.context.t('hardwareWalletConnected'))
+ // Autohide the alert after 5 seconds
+ setTimeout(_ => {
+ this.props.hideAlert()
+ }, 5000)
+ }
+
+ getPage = (page) => {
+ this.props
+ .connectHardware('trezor', page)
+ .then(accounts => {
+ if (accounts.length) {
+
+ // If we just loaded the accounts for the first time
+ // (device previously locked) show the global alert
+ if (this.state.accounts.length === 0 && !this.state.unlocked) {
+ this.showTemporaryAlert()
+ }
+
+ const newState = { unlocked: true }
+ // Default to the first account
+ if (this.state.selectedAccount === null) {
+ accounts.forEach((a, i) => {
+ if (a.address.toLowerCase() === this.props.address) {
+ newState.selectedAccount = a.index.toString()
+ }
+ })
+ // If the page doesn't contain the selected account, let's deselect it
+ } else if (!accounts.filter(a => a.index.toString() === this.state.selectedAccount).length) {
+ newState.selectedAccount = null
+ }
+
+
+ // Map accounts with balances
+ newState.accounts = accounts.map(account => {
+ const normalizedAddress = account.address.toLowerCase()
+ const balanceValue = this.props.accounts[normalizedAddress] && this.props.accounts[normalizedAddress].balance || null
+ account.balance = balanceValue ? formatBalance(balanceValue, 6) : '...'
+ return account
+ })
+
+ this.setState(newState)
+ }
+ })
+ .catch(e => {
+ if (e === 'Window blocked') {
+ this.setState({ browserSupported: false })
+ }
+ this.setState({ btnText: this.context.t('connectToTrezor') })
+ })
+ }
+
+ onForgetDevice = () => {
+ this.props.forgetDevice('trezor')
+ .then(_ => {
+ this.setState({
+ error: null,
+ btnText: this.context.t('connectToTrezor'),
+ selectedAccount: null,
+ accounts: [],
+ unlocked: false,
+ })
+ }).catch(e => {
+ this.setState({ error: e.toString() })
+ })
+ }
+
+ onUnlockAccount = () => {
+
+ if (this.state.selectedAccount === null) {
+ this.setState({ error: this.context.t('accountSelectionRequired') })
+ }
+
+ this.props.unlockTrezorAccount(this.state.selectedAccount)
+ .then(_ => {
+ this.props.history.push(DEFAULT_ROUTE)
+ }).catch(e => {
+ this.setState({ error: e.toString() })
+ })
+ }
+
+ onCancel = () => {
+ this.props.history.push(DEFAULT_ROUTE)
+ }
+
+ renderError () {
+ return this.state.error
+ ? h('span.error', { style: { marginBottom: 40 } }, this.state.error)
+ : null
+ }
+
+ renderContent () {
+ if (!this.state.accounts.length) {
+ return h(ConnectScreen, {
+ connectToTrezor: this.connectToTrezor,
+ btnText: this.state.btnText,
+ browserSupported: this.state.browserSupported,
+ })
+ }
+
+ return h(AccountList, {
+ accounts: this.state.accounts,
+ selectedAccount: this.state.selectedAccount,
+ onAccountChange: this.onAccountChange,
+ network: this.props.network,
+ getPage: this.getPage,
+ history: this.props.history,
+ onUnlockAccount: this.onUnlockAccount,
+ onForgetDevice: this.onForgetDevice,
+ onCancel: this.onCancel,
+ })
+ }
+
+ render () {
+ return h('div', [
+ this.renderError(),
+ this.renderContent(),
+ ])
+ }
+}
+
+ConnectHardwareForm.propTypes = {
+ hideModal: PropTypes.func,
+ showImportPage: PropTypes.func,
+ showConnectPage: PropTypes.func,
+ connectHardware: PropTypes.func,
+ checkHardwareStatus: PropTypes.func,
+ forgetDevice: PropTypes.func,
+ showAlert: PropTypes.func,
+ hideAlert: PropTypes.func,
+ unlockTrezorAccount: PropTypes.func,
+ numberOfExistingAccounts: PropTypes.number,
+ history: PropTypes.object,
+ t: PropTypes.func,
+ network: PropTypes.string,
+ accounts: PropTypes.object,
+ address: PropTypes.string,
+}
+
+const mapStateToProps = state => {
+ const {
+ metamask: { network, selectedAddress, identities = {}, accounts = [] },
+ } = state
+ const numberOfExistingAccounts = Object.keys(identities).length
+
+ return {
+ network,
+ accounts,
+ address: selectedAddress,
+ numberOfExistingAccounts,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ connectHardware: (deviceName, page) => {
+ return dispatch(actions.connectHardware(deviceName, page))
+ },
+ checkHardwareStatus: (deviceName) => {
+ return dispatch(actions.checkHardwareStatus(deviceName))
+ },
+ forgetDevice: (deviceName) => {
+ return dispatch(actions.forgetDevice(deviceName))
+ },
+ unlockTrezorAccount: index => {
+ return dispatch(actions.unlockTrezorAccount(index))
+ },
+ showImportPage: () => dispatch(actions.showImportPage()),
+ showConnectPage: () => dispatch(actions.showConnectPage()),
+ showAlert: (msg) => dispatch(actions.showAlert(msg)),
+ hideAlert: () => dispatch(actions.hideAlert()),
+ }
+}
+
+ConnectHardwareForm.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(
+ ConnectHardwareForm
+)
diff --git a/ui/app/components/pages/create-account/index.js b/ui/app/components/pages/create-account/index.js
index 5681e43a9..d3de1ea01 100644
--- a/ui/app/components/pages/create-account/index.js
+++ b/ui/app/components/pages/create-account/index.js
@@ -8,7 +8,12 @@ const { getCurrentViewContext } = require('../../../selectors')
const classnames = require('classnames')
const NewAccountCreateForm = require('./new-account')
const NewAccountImportForm = require('./import-account')
-const { NEW_ACCOUNT_ROUTE, IMPORT_ACCOUNT_ROUTE } = require('../../../routes')
+const ConnectHardwareForm = require('./connect-hardware')
+const {
+ NEW_ACCOUNT_ROUTE,
+ IMPORT_ACCOUNT_ROUTE,
+ CONNECT_HARDWARE_ROUTE,
+} = require('../../../routes')
class CreateAccountPage extends Component {
renderTabs () {
@@ -36,6 +41,19 @@ class CreateAccountPage extends Component {
}, [
this.context.t('import'),
]),
+ h(
+ 'div.new-account__tabs__tab',
+ {
+ className: classnames('new-account__tabs__tab', {
+ 'new-account__tabs__selected': matchPath(location.pathname, {
+ path: CONNECT_HARDWARE_ROUTE,
+ exact: true,
+ }),
+ }),
+ onClick: () => history.push(CONNECT_HARDWARE_ROUTE),
+ },
+ this.context.t('connect')
+ ),
])
}
@@ -57,6 +75,11 @@ class CreateAccountPage extends Component {
path: IMPORT_ACCOUNT_ROUTE,
component: NewAccountImportForm,
}),
+ h(Route, {
+ exact: true,
+ path: CONNECT_HARDWARE_ROUTE,
+ component: ConnectHardwareForm,
+ }),
]),
]),
])
diff --git a/ui/app/components/pages/create-account/new-account.js b/ui/app/components/pages/create-account/new-account.js
index 9c94990e0..402b8f03b 100644
--- a/ui/app/components/pages/create-account/new-account.js
+++ b/ui/app/components/pages/create-account/new-account.js
@@ -62,6 +62,7 @@ class NewAccountCreateForm extends Component {
NewAccountCreateForm.propTypes = {
hideModal: PropTypes.func,
showImportPage: PropTypes.func,
+ showConnectPage: PropTypes.func,
createAccount: PropTypes.func,
numberOfExistingAccounts: PropTypes.number,
history: PropTypes.object,
@@ -92,6 +93,7 @@ const mapDispatchToProps = dispatch => {
})
},
showImportPage: () => dispatch(actions.showImportPage()),
+ showConnectPage: () => dispatch(actions.showConnectPage()),
}
}
diff --git a/ui/app/components/pages/home.js b/ui/app/components/pages/home.js
index c53413d3b..5e3fdc9af 100644
--- a/ui/app/components/pages/home.js
+++ b/ui/app/components/pages/home.js
@@ -1,6 +1,6 @@
const { Component } = require('react')
+const { connect } = require('react-redux')
const PropTypes = require('prop-types')
-const connect = require('../../metamask-connect')
const { Redirect, withRouter } = require('react-router-dom')
const { compose } = require('recompose')
const h = require('react-hyperscript')
@@ -27,19 +27,17 @@ const {
NOTICE_ROUTE,
} = require('../../routes')
+const { unconfirmedTransactionsCountSelector } = require('../../selectors/confirm-transaction')
+
class Home extends Component {
componentDidMount () {
const {
history,
- unapprovedTxs = {},
- unapprovedMsgCount = 0,
- unapprovedPersonalMsgCount = 0,
- unapprovedTypedMessagesCount = 0,
+ unconfirmedTransactionsCount = 0,
} = this.props
// unapprovedTxs and unapproved messages
- if (Object.keys(unapprovedTxs).length ||
- unapprovedTypedMessagesCount + unapprovedMsgCount + unapprovedPersonalMsgCount > 0) {
+ if (unconfirmedTransactionsCount > 0) {
history.push(CONFIRM_TRANSACTION_ROUTE)
}
}
@@ -83,51 +81,6 @@ class Home extends Component {
})
}
- // if (!props.noActiveNotices) {
- // log.debug('rendering notice screen for unread notices.')
- // return h(NoticeScreen, {
- // notice: props.nextUnreadNotice,
- // key: 'NoticeScreen',
- // onConfirm: () => props.dispatch(actions.markNoticeRead(props.nextUnreadNotice)),
- // })
- // } else if (props.lostAccounts && props.lostAccounts.length > 0) {
- // log.debug('rendering notice screen for lost accounts view.')
- // return h(NoticeScreen, {
- // notice: generateLostAccountsNotice(props.lostAccounts),
- // key: 'LostAccountsNotice',
- // onConfirm: () => props.dispatch(actions.markAccountsFound()),
- // })
- // }
-
- // if (props.seedWords) {
- // log.debug('rendering seed words')
- // return h(HDCreateVaultComplete, {key: 'HDCreateVaultComplete'})
- // }
-
- // show initialize screen
- // if (!isInitialized || forgottenPassword) {
- // // show current view
- // log.debug('rendering an initialize screen')
- // // switch (props.currentView.name) {
-
- // // case 'restoreVault':
- // // log.debug('rendering restore vault screen')
- // // return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'})
-
- // // default:
- // // log.debug('rendering menu screen')
- // // return h(InitializeScreen, {key: 'menuScreenInit'})
- // // }
- // }
-
- // // show unlock screen
- // if (!props.isUnlocked) {
- // return h(MainContainer, {
- // currentViewName: props.currentView.name,
- // isUnlocked: props.isUnlocked,
- // })
- // }
-
// show current view
switch (currentView.name) {
@@ -135,59 +88,10 @@ class Home extends Component {
log.debug('rendering main container')
return h(MainContainer, {key: 'account-detail'})
- // case 'sendTransaction':
- // log.debug('rendering send tx screen')
-
- // // Going to leave this here until we are ready to delete SendTransactionScreen v1
- // // const SendComponentToRender = checkFeatureToggle('send-v2')
- // // ? SendTransactionScreen2
- // // : SendTransactionScreen
-
- // return h(SendTransactionScreen2, {key: 'send-transaction'})
-
- // case 'sendToken':
- // log.debug('rendering send token screen')
-
- // // Going to leave this here until we are ready to delete SendTransactionScreen v1
- // // const SendTokenComponentToRender = checkFeatureToggle('send-v2')
- // // ? SendTransactionScreen2
- // // : SendTokenScreen
-
- // return h(SendTransactionScreen2, {key: 'sendToken'})
-
case 'newKeychain':
log.debug('rendering new keychain screen')
return h(NewKeyChainScreen, {key: 'new-keychain'})
- // case 'confTx':
- // log.debug('rendering confirm tx screen')
- // return h(Redirect, {
- // to: {
- // pathname: CONFIRM_TRANSACTION_ROUTE,
- // },
- // })
- // return h(ConfirmTxScreen, {key: 'confirm-tx'})
-
- // case 'add-token':
- // log.debug('rendering add-token screen from unlock screen.')
- // return h(AddTokenScreen, {key: 'add-token'})
-
- // case 'config':
- // log.debug('rendering config screen')
- // return h(Settings, {key: 'config'})
-
- // case 'import-menu':
- // log.debug('rendering import screen')
- // return h(Import, {key: 'import-menu'})
-
- // case 'reveal-seed-conf':
- // log.debug('rendering reveal seed confirmation screen')
- // return h(RevealSeedConfirmation, {key: 'reveal-seed-conf'})
-
- // case 'info':
- // log.debug('rendering info screen')
- // return h(Settings, {key: 'info', tab: 'info'})
-
case 'buyEth':
log.debug('rendering buy ether screen')
return h(BuyView, {key: 'buyEthView'})
@@ -261,6 +165,7 @@ Home.propTypes = {
isPopup: PropTypes.bool,
isMouseUser: PropTypes.bool,
t: PropTypes.func,
+ unconfirmedTransactionsCount: PropTypes.number,
}
function mapStateToProps (state) {
@@ -324,6 +229,7 @@ function mapStateToProps (state) {
// state needed to get account dropdown temporarily rendering from app bar
selected,
+ unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state),
}
}
diff --git a/ui/app/components/pages/keychains/restore-vault.js b/ui/app/components/pages/keychains/restore-vault.js
index 33575bfbb..d90a33e49 100644
--- a/ui/app/components/pages/keychains/restore-vault.js
+++ b/ui/app/components/pages/keychains/restore-vault.js
@@ -1,178 +1,189 @@
-const { withRouter } = require('react-router-dom')
-const PropTypes = require('prop-types')
-const { compose } = require('recompose')
-const PersistentForm = require('../../../../lib/persistent-form')
-const connect = require('../../../metamask-connect')
-const h = require('react-hyperscript')
-const { createNewVaultAndRestore, unMarkPasswordForgotten } = require('../../../actions')
-const { DEFAULT_ROUTE } = require('../../../routes')
-const log = require('loglevel')
-
-class RestoreVaultPage extends PersistentForm {
- constructor (props) {
- super(props)
-
- this.state = {
- error: null,
- }
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import {connect} from 'react-redux'
+import {
+ createNewVaultAndRestore,
+ unMarkPasswordForgotten,
+} from '../../../actions'
+import { DEFAULT_ROUTE } from '../../../routes'
+import TextField from '../../text-field'
+
+class RestoreVaultPage extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
}
- createOnEnter (event) {
- if (event.key === 'Enter') {
- this.createNewVaultAndRestore()
- }
+ static propTypes = {
+ warning: PropTypes.string,
+ createNewVaultAndRestore: PropTypes.func.isRequired,
+ leaveImportSeedScreenState: PropTypes.func,
+ history: PropTypes.object,
+ isLoading: PropTypes.bool,
+ };
+
+ state = {
+ seedPhrase: '',
+ password: '',
+ confirmPassword: '',
+ seedPhraseError: null,
+ passwordError: null,
+ confirmPasswordError: null,
}
- cancel () {
- this.props.unMarkPasswordForgotten()
- .then(this.props.history.push(DEFAULT_ROUTE))
+ parseSeedPhrase = (seedPhrase) => {
+ return seedPhrase
+ .match(/\w+/g)
+ .join(' ')
}
- createNewVaultAndRestore () {
- this.setState({ error: null })
+ handleSeedPhraseChange (seedPhrase) {
+ let seedPhraseError = null
+
+ if (seedPhrase && this.parseSeedPhrase(seedPhrase).split(' ').length !== 12) {
+ seedPhraseError = this.context.t('seedPhraseReq')
+ }
+
+ this.setState({ seedPhrase, seedPhraseError })
+ }
- // check password
- var passwordBox = document.getElementById('password-box')
- var password = passwordBox.value
- var passwordConfirmBox = document.getElementById('password-box-confirm')
- var passwordConfirm = passwordConfirmBox.value
+ handlePasswordChange (password) {
+ const { confirmPassword } = this.state
+ let confirmPasswordError = null
+ let passwordError = null
- if (password.length < 8) {
- this.setState({ error: 'Password not long enough' })
- return
+ if (password && password.length < 8) {
+ passwordError = this.context.t('passwordNotLongEnough')
}
- if (password !== passwordConfirm) {
- this.setState({ error: 'Passwords don\'t match' })
- return
+ if (confirmPassword && password !== confirmPassword) {
+ confirmPasswordError = this.context.t('passwordsDontMatch')
}
- // check seed
- var seedBox = document.querySelector('textarea.twelve-word-phrase')
- var seed = seedBox.value.trim()
- if (seed.split(' ').length !== 12) {
- this.setState({ error: 'Seed phrases are 12 words long' })
- return
+ this.setState({ password, passwordError, confirmPasswordError })
+ }
+
+ handleConfirmPasswordChange (confirmPassword) {
+ const { password } = this.state
+ let confirmPasswordError = null
+
+ if (password !== confirmPassword) {
+ confirmPasswordError = this.context.t('passwordsDontMatch')
}
- // submit
- this.props.createNewVaultAndRestore(password, seed)
- .then(() => this.props.history.push(DEFAULT_ROUTE))
- .catch(({ message }) => {
- this.setState({ error: message })
- log.error(message)
- })
+ this.setState({ confirmPassword, confirmPasswordError })
+ }
+
+ onClick = () => {
+ const { password, seedPhrase } = this.state
+ const {
+ createNewVaultAndRestore,
+ leaveImportSeedScreenState,
+ history,
+ } = this.props
+
+ leaveImportSeedScreenState()
+ createNewVaultAndRestore(password, this.parseSeedPhrase(seedPhrase))
+ .then(() => history.push(DEFAULT_ROUTE))
+ }
+
+ hasError () {
+ const { passwordError, confirmPasswordError, seedPhraseError } = this.state
+ return passwordError || confirmPasswordError || seedPhraseError
}
render () {
- const { error } = this.state
- this.persistentFormParentId = 'restore-vault-form'
+ const {
+ seedPhrase,
+ password,
+ confirmPassword,
+ seedPhraseError,
+ passwordError,
+ confirmPasswordError,
+ } = this.state
+ const { t } = this.context
+ const { isLoading } = this.props
+ const disabled = !seedPhrase || !password || !confirmPassword || isLoading || this.hasError()
return (
- h('.initialize-screen.flex-column.flex-center.flex-grow', [
-
- h('h3.flex-center.text-transform-uppercase', {
- style: {
- background: '#EBEBEB',
- color: '#AEAEAE',
- marginBottom: 24,
- width: '100%',
- fontSize: '20px',
- padding: 6,
- },
- }, [
- this.props.t('restoreVault'),
- ]),
-
- // wallet seed entry
- h('h3', 'Wallet Seed'),
- h('textarea.twelve-word-phrase.letter-spacey', {
- dataset: {
- persistentFormId: 'wallet-seed',
- },
- placeholder: this.props.t('secretPhrase'),
- }),
-
- // password
- h('input.large-input.letter-spacey', {
- type: 'password',
- id: 'password-box',
- placeholder: this.props.t('newPassword8Chars'),
- dataset: {
- persistentFormId: 'password',
- },
- style: {
- width: 260,
- marginTop: 12,
- },
- }),
-
- // confirm password
- h('input.large-input.letter-spacey', {
- type: 'password',
- id: 'password-box-confirm',
- placeholder: this.props.t('confirmPassword'),
- onKeyPress: this.createOnEnter.bind(this),
- dataset: {
- persistentFormId: 'password-confirmation',
- },
- style: {
- width: 260,
- marginTop: 16,
- },
- }),
-
- error && (
- h('span.error.in-progress-notification', error)
- ),
-
- // submit
- h('.flex-row.flex-space-between', {
- style: {
- marginTop: 30,
- width: '50%',
- },
- }, [
-
- // cancel
- h('button.primary', {
- onClick: () => this.cancel(),
- }, this.props.t('cancel')),
-
- // submit
- h('button.primary', {
- onClick: this.createNewVaultAndRestore.bind(this),
- }, this.props.t('ok')),
-
- ]),
- ])
+ <div className="first-view-main-wrapper">
+ <div className="first-view-main">
+ <div className="import-account">
+ <a
+ className="import-account__back-button"
+ onClick={e => {
+ e.preventDefault()
+ this.props.history.goBack()
+ }}
+ href="#"
+ >
+ {`< Back`}
+ </a>
+ <div className="import-account__title">
+ { this.context.t('restoreAccountWithSeed') }
+ </div>
+ <div className="import-account__selector-label">
+ { this.context.t('secretPhrase') }
+ </div>
+ <div className="import-account__input-wrapper">
+ <label className="import-account__input-label">Wallet Seed</label>
+ <textarea
+ className="import-account__secret-phrase"
+ onChange={e => this.handleSeedPhraseChange(e.target.value)}
+ value={this.state.seedPhrase}
+ placeholder={this.context.t('separateEachWord')}
+ />
+ </div>
+ <span className="error">
+ { seedPhraseError }
+ </span>
+ <TextField
+ id="password"
+ label={t('newPassword')}
+ type="password"
+ className="first-time-flow__input"
+ value={this.state.password}
+ onChange={event => this.handlePasswordChange(event.target.value)}
+ error={passwordError}
+ autoComplete="new-password"
+ margin="normal"
+ largeLabel
+ />
+ <TextField
+ id="confirm-password"
+ label={t('confirmPassword')}
+ type="password"
+ className="first-time-flow__input"
+ value={this.state.confirmPassword}
+ onChange={event => this.handleConfirmPasswordChange(event.target.value)}
+ error={confirmPasswordError}
+ autoComplete="confirm-password"
+ margin="normal"
+ largeLabel
+ />
+ <button
+ className="first-time-flow__button"
+ onClick={() => !disabled && this.onClick()}
+ disabled={disabled}
+ >
+ {this.context.t('restore')}
+ </button>
+ </div>
+ </div>
+ </div>
)
}
}
-RestoreVaultPage.propTypes = {
- history: PropTypes.object,
-}
-
-const mapStateToProps = state => {
- const { appState: { warning, forgottenPassword } } = state
-
- return {
- warning,
- forgottenPassword,
- }
+RestoreVaultPage.contextTypes = {
+ t: PropTypes.func,
}
-const mapDispatchToProps = dispatch => {
- return {
- createNewVaultAndRestore: (password, seed) => {
- return dispatch(createNewVaultAndRestore(password, seed))
+export default connect(
+ ({ appState: { warning, isLoading } }) => ({ warning, isLoading }),
+ dispatch => ({
+ leaveImportSeedScreenState: () => {
+ dispatch(unMarkPasswordForgotten())
},
- unMarkPasswordForgotten: () => dispatch(unMarkPasswordForgotten()),
- }
-}
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
+ createNewVaultAndRestore: (pw, seed) => dispatch(createNewVaultAndRestore(pw, seed)),
+ })
)(RestoreVaultPage)
diff --git a/ui/app/components/pages/unlock-page/unlock-page.component.js b/ui/app/components/pages/unlock-page/unlock-page.component.js
index a1d3f9181..94915df76 100644
--- a/ui/app/components/pages/unlock-page/unlock-page.component.js
+++ b/ui/app/components/pages/unlock-page/unlock-page.component.js
@@ -2,19 +2,27 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Button from '@material-ui/core/Button'
import TextField from '../../text-field'
-
-const { ENVIRONMENT_TYPE_POPUP } = require('../../../../../app/scripts/lib/enums')
-const { getEnvironmentType } = require('../../../../../app/scripts/lib/util')
-const getCaretCoordinates = require('textarea-caret')
-const EventEmitter = require('events').EventEmitter
-const Mascot = require('../../mascot')
-const { DEFAULT_ROUTE, RESTORE_VAULT_ROUTE } = require('../../../routes')
-
-class UnlockPage extends Component {
+import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums'
+import { getEnvironmentType } from '../../../../../app/scripts/lib/util'
+import getCaretCoordinates from 'textarea-caret'
+import { EventEmitter } from 'events'
+import Mascot from '../../mascot'
+import { DEFAULT_ROUTE, RESTORE_VAULT_ROUTE } from '../../../routes'
+
+export default class UnlockPage extends Component {
static contextTypes = {
t: PropTypes.func,
}
+ static propTypes = {
+ forgotPassword: PropTypes.func,
+ tryUnlockMetamask: PropTypes.func,
+ markPasswordForgotten: PropTypes.func,
+ history: PropTypes.object,
+ isUnlocked: PropTypes.bool,
+ useOldInterface: PropTypes.func,
+ }
+
constructor (props) {
super(props)
@@ -23,6 +31,7 @@ class UnlockPage extends Component {
error: null,
}
+ this.submitting = false
this.animationEventEmitter = new EventEmitter()
}
@@ -41,20 +50,21 @@ class UnlockPage extends Component {
const { password } = this.state
const { tryUnlockMetamask, history } = this.props
- if (password === '') {
+ if (password === '' || this.submitting) {
return
}
this.setState({ error: null })
+ this.submitting = true
try {
await tryUnlockMetamask(password)
+ this.submitting = false
+ history.push(DEFAULT_ROUTE)
} catch ({ message }) {
this.setState({ error: message })
- return
+ this.submitting = false
}
-
- history.push(DEFAULT_ROUTE)
}
handleInputChange ({ target }) {
@@ -98,7 +108,9 @@ class UnlockPage extends Component {
}
render () {
- const { error } = this.state
+ const { password, error } = this.state
+ const { t } = this.context
+ const { markPasswordForgotten, history } = this.props
return (
<div className="unlock-page__container">
@@ -111,18 +123,18 @@ class UnlockPage extends Component {
/>
</div>
<h1 className="unlock-page__title">
- { this.context.t('welcomeBack') }
+ { t('welcomeBack') }
</h1>
- <div>{ this.context.t('unlockMessage') }</div>
+ <div>{ t('unlockMessage') }</div>
<form
className="unlock-page__form"
onSubmit={event => this.handleSubmit(event)}
>
<TextField
id="password"
- label={this.context.t('password')}
+ label={t('password')}
type="password"
- value={this.state.password}
+ value={password}
onChange={event => this.handleInputChange(event)}
error={error}
autoFocus
@@ -136,28 +148,28 @@ class UnlockPage extends Component {
<div
className="unlock-page__link"
onClick={() => {
- this.props.markPasswordForgotten()
- this.props.history.push(RESTORE_VAULT_ROUTE)
+ markPasswordForgotten()
+ history.push(RESTORE_VAULT_ROUTE)
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
global.platform.openExtensionInBrowser()
}
}}
>
- { this.context.t('restoreFromSeed') }
+ { t('restoreFromSeed') }
</div>
<div
className="unlock-page__link unlock-page__link--import"
onClick={() => {
- this.props.markPasswordForgotten()
- this.props.history.push(RESTORE_VAULT_ROUTE)
+ markPasswordForgotten()
+ history.push(RESTORE_VAULT_ROUTE)
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
global.platform.openExtensionInBrowser()
}
}}
>
- { this.context.t('importUsingSeed') }
+ { t('importUsingSeed') }
</div>
</div>
</div>
@@ -165,15 +177,3 @@ class UnlockPage extends Component {
)
}
}
-
-UnlockPage.propTypes = {
- forgotPassword: PropTypes.func,
- tryUnlockMetamask: PropTypes.func,
- markPasswordForgotten: PropTypes.func,
- history: PropTypes.object,
- isUnlocked: PropTypes.bool,
- t: PropTypes.func,
- useOldInterface: PropTypes.func,
-}
-
-export default UnlockPage
diff --git a/ui/app/components/pending-tx/confirm-deploy-contract.js b/ui/app/components/pending-tx/confirm-deploy-contract.js
deleted file mode 100644
index af3a14f57..000000000
--- a/ui/app/components/pending-tx/confirm-deploy-contract.js
+++ /dev/null
@@ -1,358 +0,0 @@
-const { Component } = require('react')
-const connect = require('react-redux').connect
-const h = require('react-hyperscript')
-const PropTypes = require('prop-types')
-const actions = require('../../actions')
-const clone = require('clone')
-const ethUtil = require('ethereumjs-util')
-const BN = ethUtil.BN
-const hexToBn = require('../../../../app/scripts/lib/hex-to-bn')
-const { conversionUtil } = require('../../conversion-util')
-const SenderToRecipient = require('../sender-to-recipient')
-const NetworkDisplay = require('../network-display')
-
-const { MIN_GAS_PRICE_HEX } = require('../send_/send.constants')
-
-class ConfirmDeployContract extends Component {
- constructor (props) {
- super(props)
-
- this.state = {
- valid: false,
- submitting: false,
- }
- }
-
- onSubmit (event) {
- event.preventDefault()
- const txMeta = this.gatherTxMeta()
- const valid = this.checkValidity()
- this.setState({ valid, submitting: true })
-
- if (valid && this.verifyGasParams()) {
- this.props.sendTransaction(txMeta, event)
- } else {
- this.props.displayWarning(this.context.t('invalidGasParams'))
- this.setState({ submitting: false })
- }
- }
-
- cancel (event, txMeta) {
- event.preventDefault()
- this.props.cancelTransaction(txMeta)
- }
-
- checkValidity () {
- const form = this.getFormEl()
- const valid = form.checkValidity()
- return valid
- }
-
- getFormEl () {
- const form = document.querySelector('form#pending-tx-form')
- // Stub out form for unit tests:
- if (!form) {
- return { checkValidity () { return true } }
- }
- return form
- }
-
- // After a customizable state value has been updated,
- gatherTxMeta () {
- const props = this.props
- const state = this.state
- const txData = clone(state.txData) || clone(props.txData)
-
- // log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`)
- return txData
- }
-
- verifyGasParams () {
- // We call this in case the gas has not been modified at all
- if (!this.state) { return true }
- return (
- this._notZeroOrEmptyString(this.state.gas) &&
- this._notZeroOrEmptyString(this.state.gasPrice)
- )
- }
-
- _notZeroOrEmptyString (obj) {
- return obj !== '' && obj !== '0x0'
- }
-
- bnMultiplyByFraction (targetBN, numerator, denominator) {
- const numBN = new BN(numerator)
- const denomBN = new BN(denominator)
- return targetBN.mul(numBN).div(denomBN)
- }
-
- getData () {
- const { identities } = this.props
- const txMeta = this.gatherTxMeta()
- const txParams = txMeta.txParams || {}
-
- return {
- from: {
- address: txParams.from,
- name: identities[txParams.from].name,
- },
- memo: txParams.memo || '',
- }
- }
-
- getAmount () {
- const { conversionRate, currentCurrency } = this.props
- const txMeta = this.gatherTxMeta()
- const txParams = txMeta.txParams || {}
-
- const FIAT = conversionUtil(txParams.value, {
- fromNumericBase: 'hex',
- toNumericBase: 'dec',
- fromCurrency: 'ETH',
- toCurrency: currentCurrency,
- numberOfDecimals: 2,
- fromDenomination: 'WEI',
- conversionRate,
- })
- const ETH = conversionUtil(txParams.value, {
- fromNumericBase: 'hex',
- toNumericBase: 'dec',
- fromCurrency: 'ETH',
- toCurrency: 'ETH',
- fromDenomination: 'WEI',
- conversionRate,
- numberOfDecimals: 6,
- })
-
- return {
- fiat: Number(FIAT),
- token: Number(ETH),
- }
-
- }
-
- getGasFee () {
- const { conversionRate, currentCurrency } = this.props
- const txMeta = this.gatherTxMeta()
- const txParams = txMeta.txParams || {}
-
- // Gas
- const gas = txParams.gas
- const gasBn = hexToBn(gas)
-
- // Gas Price
- const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_HEX
- const gasPriceBn = hexToBn(gasPrice)
-
- const txFeeBn = gasBn.mul(gasPriceBn)
-
- const FIAT = conversionUtil(txFeeBn, {
- fromNumericBase: 'BN',
- toNumericBase: 'dec',
- fromDenomination: 'WEI',
- fromCurrency: 'ETH',
- toCurrency: currentCurrency,
- numberOfDecimals: 2,
- conversionRate,
- })
- const ETH = conversionUtil(txFeeBn, {
- fromNumericBase: 'BN',
- toNumericBase: 'dec',
- fromDenomination: 'WEI',
- fromCurrency: 'ETH',
- toCurrency: 'ETH',
- numberOfDecimals: 6,
- conversionRate,
- })
-
- return {
- fiat: Number(FIAT),
- eth: Number(ETH),
- }
- }
-
- renderGasFee () {
- const { currentCurrency } = this.props
- const { fiat: fiatGas, eth: ethGas } = this.getGasFee()
-
- return (
- h('section.flex-row.flex-center.confirm-screen-row', [
- h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('gasFee') ]),
- h('div.confirm-screen-section-column', [
- h('div.confirm-screen-row-info', `${fiatGas} ${currentCurrency.toUpperCase()}`),
-
- h(
- 'div.confirm-screen-row-detail',
- `${ethGas} ETH`
- ),
- ]),
- ])
- )
- }
-
- renderHeroAmount () {
- const { currentCurrency } = this.props
- const { fiat: fiatAmount } = this.getAmount()
- const txMeta = this.gatherTxMeta()
- const txParams = txMeta.txParams || {}
- const { memo = '' } = txParams
-
- return (
- h('div.confirm-send-token__hero-amount-wrapper', [
- h('h3.flex-center.confirm-screen-send-amount', `${fiatAmount}`),
- h('h3.flex-center.confirm-screen-send-amount-currency', currentCurrency.toUpperCase()),
- h('div.flex-center.confirm-memo-wrapper', [
- h('h3.confirm-screen-send-memo', memo),
- ]),
- ])
- )
- }
-
- renderTotalPlusGas () {
- const { currentCurrency } = this.props
- const { fiat: fiatAmount, token: tokenAmount } = this.getAmount()
- const { fiat: fiatGas, eth: ethGas } = this.getGasFee()
-
- return (
- h('section.flex-row.flex-center.confirm-screen-row.confirm-screen-total-box ', [
- h('div.confirm-screen-section-column', [
- h('span.confirm-screen-label', [ this.context.t('total') + ' ' ]),
- h('div.confirm-screen-total-box__subtitle', [ this.context.t('amountPlusGas') ]),
- ]),
-
- h('div.confirm-screen-section-column', [
- h('div.confirm-screen-row-info', `${fiatAmount + fiatGas} ${currentCurrency.toUpperCase()}`),
- h('div.confirm-screen-row-detail', `${tokenAmount + ethGas} ETH`),
- ]),
- ])
- )
- }
-
- render () {
- const { backToAccountDetail, selectedAddress } = this.props
- const txMeta = this.gatherTxMeta()
-
- const {
- from: {
- address: fromAddress,
- name: fromName,
- },
- } = this.getData()
-
- this.inputs = []
-
- return (
- h('.page-container', [
- h('.page-container__header', [
- h('.page-container__header-row', [
- h('span.page-container__back-button', {
- onClick: () => backToAccountDetail(selectedAddress),
- }, this.context.t('back')),
- window.METAMASK_UI_TYPE === 'notification' && h(NetworkDisplay),
- ]),
- h('.page-container__title', this.context.t('confirmContract')),
- h('.page-container__subtitle', this.context.t('pleaseReviewTransaction')),
- ]),
- // Main Send token Card
- h('.page-container__content', [
-
- h(SenderToRecipient, {
- senderName: fromName,
- senderAddress: fromAddress,
- }),
-
- // h('h3.flex-center.confirm-screen-sending-to-message', {
- // style: {
- // textAlign: 'center',
- // fontSize: '16px',
- // },
- // }, [
- // `You're deploying a new contract.`,
- // ]),
-
- this.renderHeroAmount(),
-
- h('div.confirm-screen-rows', [
- h('section.flex-row.flex-center.confirm-screen-row', [
- h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('from') ]),
- h('div.confirm-screen-section-column', [
- h('div.confirm-screen-row-info', fromName),
- h('div.confirm-screen-row-detail', `...${fromAddress.slice(fromAddress.length - 4)}`),
- ]),
- ]),
-
- h('section.flex-row.flex-center.confirm-screen-row', [
- h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('to') ]),
- h('div.confirm-screen-section-column', [
- h('div.confirm-screen-row-info', this.context.t('newContract')),
- ]),
- ]),
-
- this.renderGasFee(),
-
- this.renderTotalPlusGas(),
-
- ]),
- ]),
-
- h('form#pending-tx-form', {
- onSubmit: event => this.onSubmit(event),
- }, [
- h('.page-container__footer', [
- // Cancel Button
- h('button.btn-cancel.page-container__footer-button.allcaps', {
- onClick: event => this.cancel(event, txMeta),
- }, this.context.t('cancel')),
-
- // Accept Button
- h('button.btn-confirm.page-container__footer-button.allcaps', {
- onClick: event => this.onSubmit(event),
- }, this.context.t('confirm')),
- ]),
- ]),
- ])
- )
- }
-}
-
-ConfirmDeployContract.propTypes = {
- sendTransaction: PropTypes.func,
- cancelTransaction: PropTypes.func,
- backToAccountDetail: PropTypes.func,
- displayWarning: PropTypes.func,
- identities: PropTypes.object,
- conversionRate: PropTypes.number,
- currentCurrency: PropTypes.string,
- selectedAddress: PropTypes.string,
- t: PropTypes.func,
-}
-
-const mapStateToProps = state => {
- const {
- conversionRate,
- identities,
- currentCurrency,
- } = state.metamask
- const accounts = state.metamask.accounts
- const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
- return {
- currentCurrency,
- conversionRate,
- identities,
- selectedAddress,
- }
-}
-
-const mapDispatchToProps = dispatch => {
- return {
- backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)),
- cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })),
- displayWarning: warning => actions.displayWarning(warning),
- }
-}
-
-ConfirmDeployContract.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(ConfirmDeployContract)
diff --git a/ui/app/components/pending-tx/confirm-send-ether.js b/ui/app/components/pending-tx/confirm-send-ether.js
deleted file mode 100644
index 22b2670d8..000000000
--- a/ui/app/components/pending-tx/confirm-send-ether.js
+++ /dev/null
@@ -1,692 +0,0 @@
-const Component = require('react').Component
-const { withRouter } = require('react-router-dom')
-const { compose } = require('recompose')
-const PropTypes = require('prop-types')
-const connect = require('react-redux').connect
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const actions = require('../../actions')
-const clone = require('clone')
-const ethUtil = require('ethereumjs-util')
-const BN = ethUtil.BN
-const hexToBn = require('../../../../app/scripts/lib/hex-to-bn')
-const classnames = require('classnames')
-const {
- conversionUtil,
- addCurrencies,
- multiplyCurrencies,
-} = require('../../conversion-util')
-const {
- calcGasTotal,
- isBalanceSufficient,
-} = require('../send_/send.utils')
-const GasFeeDisplay = require('../send_/send-content/send-gas-row/gas-fee-display/gas-fee-display.component').default
-const SenderToRecipient = require('../sender-to-recipient')
-const NetworkDisplay = require('../network-display')
-const currencyFormatter = require('currency-formatter')
-const currencies = require('currency-formatter/currencies')
-
-const { MIN_GAS_PRICE_HEX } = require('../send_/send.constants')
-const { SEND_ROUTE, DEFAULT_ROUTE } = require('../../routes')
-const {
- ENVIRONMENT_TYPE_POPUP,
- ENVIRONMENT_TYPE_NOTIFICATION,
-} = require('../../../../app/scripts/lib/enums')
-
-import {
- updateSendErrors,
-} from '../../ducks/send.duck'
-
-ConfirmSendEther.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(ConfirmSendEther)
-
-
-function mapStateToProps (state) {
- const {
- conversionRate,
- identities,
- currentCurrency,
- send,
- } = state.metamask
- const accounts = state.metamask.accounts
- const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
- const { balance } = accounts[selectedAddress]
- return {
- conversionRate,
- identities,
- selectedAddress,
- currentCurrency,
- send,
- balance,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- clearSend: () => dispatch(actions.clearSend()),
- editTransaction: txMeta => {
- const { id, txParams } = txMeta
- const {
- gas: gasLimit,
- gasPrice,
- to,
- value: amount,
- } = txParams
-
- dispatch(actions.updateSend({
- gasLimit,
- gasPrice,
- gasTotal: null,
- to,
- amount,
- errors: { to: null, amount: null },
- editingTransactionId: id,
- }))
- },
- cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })),
- showCustomizeGasModal: (txMeta, sendGasLimit, sendGasPrice, sendGasTotal) => {
- const { id, txParams, lastGasPrice } = txMeta
- const { gas: txGasLimit, gasPrice: txGasPrice } = txParams
-
- let forceGasMin
- if (lastGasPrice) {
- forceGasMin = ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, {
- multiplicandBase: 16,
- multiplierBase: 10,
- toNumericBase: 'hex',
- fromDenomination: 'WEI',
- }))
- }
-
- dispatch(actions.updateSend({
- gasLimit: sendGasLimit || txGasLimit,
- gasPrice: sendGasPrice || txGasPrice,
- editingTransactionId: id,
- gasTotal: sendGasTotal,
- forceGasMin,
- }))
- dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' }))
- },
- updateSendErrors: error => dispatch(updateSendErrors(error)),
- }
-}
-
-inherits(ConfirmSendEther, Component)
-function ConfirmSendEther () {
- Component.call(this)
- this.state = {}
- this.onSubmit = this.onSubmit.bind(this)
-}
-
-ConfirmSendEther.prototype.updateComponentSendErrors = function (prevProps) {
- const {
- balance: oldBalance,
- conversionRate: oldConversionRate,
- } = prevProps
- const {
- updateSendErrors,
- balance,
- conversionRate,
- send: {
- errors: {
- simulationFails,
- },
- },
- } = this.props
- const txMeta = this.gatherTxMeta()
-
- const shouldUpdateBalanceSendErrors = balance && [
- balance !== oldBalance,
- conversionRate !== oldConversionRate,
- ].some(x => Boolean(x))
-
- if (shouldUpdateBalanceSendErrors) {
- const balanceIsSufficient = this.isBalanceSufficient(txMeta)
- updateSendErrors({
- insufficientFunds: balanceIsSufficient ? false : 'insufficientFunds',
- })
- }
-
- const shouldUpdateSimulationSendError = Boolean(txMeta.simulationFails) !== Boolean(simulationFails)
-
- if (shouldUpdateSimulationSendError) {
- updateSendErrors({
- simulationFails: !txMeta.simulationFails ? false : 'transactionError',
- })
- }
-}
-
-ConfirmSendEther.prototype.componentWillMount = function () {
- this.updateComponentSendErrors({})
-}
-
-ConfirmSendEther.prototype.componentDidUpdate = function (prevProps) {
- this.updateComponentSendErrors(prevProps)
-}
-
-ConfirmSendEther.prototype.getAmount = function () {
- const { conversionRate, currentCurrency } = this.props
- const txMeta = this.gatherTxMeta()
- const txParams = txMeta.txParams || {}
-
- const FIAT = conversionUtil(txParams.value, {
- fromNumericBase: 'hex',
- toNumericBase: 'dec',
- fromCurrency: 'ETH',
- toCurrency: currentCurrency,
- numberOfDecimals: 2,
- fromDenomination: 'WEI',
- conversionRate,
- })
- const ETH = conversionUtil(txParams.value, {
- fromNumericBase: 'hex',
- toNumericBase: 'dec',
- fromCurrency: 'ETH',
- toCurrency: 'ETH',
- fromDenomination: 'WEI',
- conversionRate,
- numberOfDecimals: 6,
- })
-
- return {
- FIAT,
- ETH,
- }
-
-}
-
-ConfirmSendEther.prototype.getGasFee = function () {
- const { conversionRate, currentCurrency } = this.props
- const txMeta = this.gatherTxMeta()
- const txParams = txMeta.txParams || {}
-
- // Gas
- const gas = txParams.gas
- const gasBn = hexToBn(gas)
-
- // From latest master
-// const gasLimit = new BN(parseInt(blockGasLimit))
-// const safeGasLimitBN = this.bnMultiplyByFraction(gasLimit, 19, 20)
-// const saferGasLimitBN = this.bnMultiplyByFraction(gasLimit, 18, 20)
-// const safeGasLimit = safeGasLimitBN.toString(10)
-
- // Gas Price
- const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_HEX
- const gasPriceBn = hexToBn(gasPrice)
-
- const txFeeBn = gasBn.mul(gasPriceBn)
-
- const FIAT = conversionUtil(txFeeBn, {
- fromNumericBase: 'BN',
- toNumericBase: 'dec',
- fromDenomination: 'WEI',
- fromCurrency: 'ETH',
- toCurrency: currentCurrency,
- numberOfDecimals: 2,
- conversionRate,
- })
- const ETH = conversionUtil(txFeeBn, {
- fromNumericBase: 'BN',
- toNumericBase: 'dec',
- fromDenomination: 'WEI',
- fromCurrency: 'ETH',
- toCurrency: 'ETH',
- numberOfDecimals: 6,
- conversionRate,
- })
-
- return {
- FIAT,
- ETH,
- gasFeeInHex: txFeeBn.toString(16),
- }
-}
-
-ConfirmSendEther.prototype.getData = function () {
- const { identities } = this.props
- const txMeta = this.gatherTxMeta()
- const txParams = txMeta.txParams || {}
- const account = identities ? identities[txParams.from] || {} : {}
- const { FIAT: gasFeeInFIAT, ETH: gasFeeInETH, gasFeeInHex } = this.getGasFee()
- const { FIAT: amountInFIAT, ETH: amountInETH } = this.getAmount()
-
- const totalInFIAT = addCurrencies(gasFeeInFIAT, amountInFIAT, {
- toNumericBase: 'dec',
- numberOfDecimals: 2,
- })
- const totalInETH = addCurrencies(gasFeeInETH, amountInETH, {
- toNumericBase: 'dec',
- numberOfDecimals: 6,
- })
-
- return {
- from: {
- address: txParams.from,
- name: account.name,
- },
- to: {
- address: txParams.to,
- name: identities[txParams.to] ? identities[txParams.to].name : this.context.t('newRecipient'),
- },
- memo: txParams.memo || '',
- gasFeeInFIAT,
- gasFeeInETH,
- amountInFIAT,
- amountInETH,
- totalInFIAT,
- totalInETH,
- gasFeeInHex,
- }
-}
-
-ConfirmSendEther.prototype.convertToRenderableCurrency = function (value, currencyCode) {
- const upperCaseCurrencyCode = currencyCode.toUpperCase()
-
- return currencies.find(currency => currency.code === upperCaseCurrencyCode)
- ? currencyFormatter.format(Number(value), {
- code: upperCaseCurrencyCode,
- })
- : value
-}
-
-ConfirmSendEther.prototype.editTransaction = function () {
- const { editTransaction, history } = this.props
- const txMeta = this.gatherTxMeta()
- editTransaction(txMeta)
- history.push(SEND_ROUTE)
-}
-
-ConfirmSendEther.prototype.renderHeaderRow = function (isTxReprice) {
- const windowType = window.METAMASK_UI_TYPE
- const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION &&
- windowType !== ENVIRONMENT_TYPE_POPUP
-
- if (isTxReprice && isFullScreen) {
- return null
- }
-
- return (
- h('.page-container__header-row', [
- h('span.page-container__back-button', {
- onClick: () => this.editTransaction(),
- style: {
- visibility: isTxReprice ? 'hidden' : 'initial',
- },
- }, 'Edit'),
- !isFullScreen && h(NetworkDisplay),
- ])
- )
-}
-
-ConfirmSendEther.prototype.renderHeader = function (isTxReprice) {
- const title = isTxReprice ? this.context.t('speedUpTitle') : this.context.t('confirm')
- const subtitle = isTxReprice
- ? this.context.t('speedUpSubtitle')
- : this.context.t('pleaseReviewTransaction')
-
- return (
- h('.page-container__header', [
- this.renderHeaderRow(isTxReprice),
- h('.page-container__title', title),
- h('.page-container__subtitle', subtitle),
- ])
- )
-}
-
-ConfirmSendEther.prototype.render = function () {
- const {
- currentCurrency,
- clearSend,
- conversionRate,
- currentCurrency: convertedCurrency,
- showCustomizeGasModal,
- send: {
- gasTotal,
- gasLimit: sendGasLimit,
- gasPrice: sendGasPrice,
- errors,
- },
- } = this.props
- const txMeta = this.gatherTxMeta()
- const isTxReprice = Boolean(txMeta.lastGasPrice)
- const txParams = txMeta.txParams || {}
-
- const {
- from: {
- address: fromAddress,
- name: fromName,
- },
- to: {
- address: toAddress,
- name: toName,
- },
- memo,
- gasFeeInHex,
- amountInFIAT,
- totalInFIAT,
- totalInETH,
- } = this.getData()
-
- const convertedAmountInFiat = this.convertToRenderableCurrency(amountInFIAT, currentCurrency)
- const convertedTotalInFiat = this.convertToRenderableCurrency(totalInFIAT, currentCurrency)
-
- // This is from the latest master
- // It handles some of the errors that we are not currently handling
- // Leaving as comments fo reference
-
- // const balanceBn = hexToBn(balance)
- // const insufficientBalance = balanceBn.lt(maxCost)
- // const buyDisabled = insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting
- // const showRejectAll = props.unconfTxListLength > 1
-// const dangerousGasLimit = gasBn.gte(saferGasLimitBN)
-// const gasLimitSpecified = txMeta.gasLimitSpecified
-
- this.inputs = []
-
- return (
- // Main Send token Card
- h('.page-container', [
- this.renderHeader(isTxReprice),
- h('.page-container__content', [
- h(SenderToRecipient, {
- senderName: fromName,
- senderAddress: fromAddress,
- recipientName: toName,
- recipientAddress: txParams.to,
- }),
-
- // h('h3.flex-center.confirm-screen-sending-to-message', {
- // style: {
- // textAlign: 'center',
- // fontSize: '16px',
- // },
- // }, [
- // `You're sending to Recipient ...${toAddress.slice(toAddress.length - 4)}`,
- // ]),
-
- h('h3.flex-center.confirm-screen-send-amount', [`${convertedAmountInFiat}`]),
- h('h3.flex-center.confirm-screen-send-amount-currency', [ currentCurrency.toUpperCase() ]),
- h('div.flex-center.confirm-memo-wrapper', [
- h('h3.confirm-screen-send-memo', [ memo ? `"${memo}"` : '' ]),
- ]),
-
- h('div.confirm-screen-rows', [
- h('section.flex-row.flex-center.confirm-screen-row', [
- h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('from') ]),
- h('div.confirm-screen-section-column', [
- h('div.confirm-screen-row-info', fromName),
- h('div.confirm-screen-row-detail', `...${fromAddress.slice(fromAddress.length - 4)}`),
- ]),
- ]),
-
- h('section.flex-row.flex-center.confirm-screen-row', [
- h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('to') ]),
- h('div.confirm-screen-section-column', [
- h('div.confirm-screen-row-info', toName),
- h('div.confirm-screen-row-detail', `...${toAddress.slice(toAddress.length - 4)}`),
- ]),
- ]),
-
- h('section.flex-row.flex-center.confirm-screen-row', [
- h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('gasFee') ]),
- h('div.confirm-screen-section-column', [
- h(GasFeeDisplay, {
- gasTotal: gasTotal || gasFeeInHex,
- conversionRate,
- convertedCurrency,
- onClick: () => showCustomizeGasModal(txMeta, sendGasLimit, sendGasPrice, gasTotal),
- }),
- ]),
- ]),
-
- h('section.flex-row.flex-center.confirm-screen-row.confirm-screen-total-box ', [
- h('div', {
- className: classnames({
- 'confirm-screen-section-column--with-error': errors['insufficientFunds'],
- 'confirm-screen-section-column': !errors['insufficientFunds'],
- }),
- }, [
- h('span.confirm-screen-label', [ this.context.t('total') + ' ' ]),
- h('div.confirm-screen-total-box__subtitle', [ this.context.t('amountPlusGas') ]),
- ]),
-
- h('div.confirm-screen-section-column', [
- h('div.confirm-screen-row-info', `${convertedTotalInFiat} ${currentCurrency.toUpperCase()}`),
- h('div.confirm-screen-row-detail', `${totalInETH} ETH`),
- ]),
-
- this.renderErrorMessage('insufficientFunds'),
- ]),
- ]),
-
-// These are latest errors handling from master
-// Leaving as comments as reference when we start implementing error handling
-// h('style', `
-// .conf-buttons button {
-// margin-left: 10px;
-// text-transform: uppercase;
-// }
-// `),
-
-// txMeta.simulationFails ?
-// h('.error', {
-// style: {
-// marginLeft: 50,
-// fontSize: '0.9em',
-// },
-// }, 'Transaction Error. Exception thrown in contract code.')
-// : null,
-
-// !isValidAddress ?
-// h('.error', {
-// style: {
-// marginLeft: 50,
-// fontSize: '0.9em',
-// },
-// }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.')
-// : null,
-
-// insufficientBalance ?
-// h('span.error', {
-// style: {
-// marginLeft: 50,
-// fontSize: '0.9em',
-// },
-// }, 'Insufficient balance for transaction')
-// : null,
-
-// // send + cancel
-// h('.flex-row.flex-space-around.conf-buttons', {
-// style: {
-// display: 'flex',
-// justifyContent: 'flex-end',
-// margin: '14px 25px',
-// },
-// }, [
-// h('button', {
-// onClick: (event) => {
-// this.resetGasFields()
-// event.preventDefault()
-// },
-// }, 'Reset'),
-
-// // Accept Button or Buy Button
-// insufficientBalance ? h('button.btn-green', { onClick: props.buyEth }, 'Buy Ether') :
-// h('input.confirm.btn-green', {
-// type: 'submit',
-// value: 'SUBMIT',
-// style: { marginLeft: '10px' },
-// disabled: buyDisabled,
-// }),
-
-// h('button.cancel.btn-red', {
-// onClick: props.cancelTransaction,
-// }, 'Reject'),
-// ]),
-// showRejectAll ? h('.flex-row.flex-space-around.conf-buttons', {
-// style: {
-// display: 'flex',
-// justifyContent: 'flex-end',
-// margin: '14px 25px',
-// },
-// }, [
-// h('button.cancel.btn-red', {
-// onClick: props.cancelAllTransactions,
-// }, 'Reject All'),
-// ]) : null,
-// ]),
-// ])
-// )
-// }
- ]),
-
- h('form#pending-tx-form', {
- className: 'confirm-screen-form',
- onSubmit: this.onSubmit,
- }, [
- this.renderErrorMessage('simulationFails'),
- h('.page-container__footer', [
- // Cancel Button
- h('button.btn-cancel.page-container__footer-button.allcaps', {
- onClick: (event) => {
- clearSend()
- this.cancel(event, txMeta)
- },
- }, this.context.t('cancel')),
-
- // Accept Button
- h('button.btn-confirm.page-container__footer-button.allcaps', {
- onClick: event => this.onSubmit(event),
- }, this.context.t('confirm')),
- ]),
- ]),
- ])
- )
-}
-
-ConfirmSendEther.prototype.renderErrorMessage = function (message) {
- const { send: { errors } } = this.props
-
- return errors[message]
- ? h('div.confirm-screen-error', [ errors[message] ])
- : null
-}
-
-ConfirmSendEther.prototype.onSubmit = function (event) {
- event.preventDefault()
- const { updateSendErrors } = this.props
- const txMeta = this.gatherTxMeta()
- const valid = this.checkValidity()
- const balanceIsSufficient = this.isBalanceSufficient(txMeta)
- this.setState({ valid, submitting: true })
-
- if (valid && this.verifyGasParams() && balanceIsSufficient) {
- this.props.sendTransaction(txMeta, event)
- } else if (!balanceIsSufficient) {
- updateSendErrors({ insufficientFunds: 'insufficientFunds' })
- } else {
- updateSendErrors({ invalidGasParams: 'invalidGasParams' })
- this.setState({ submitting: false })
- }
-}
-
-ConfirmSendEther.prototype.cancel = function (event, txMeta) {
- event.preventDefault()
- const { cancelTransaction } = this.props
-
- cancelTransaction(txMeta)
- .then(() => this.props.history.push(DEFAULT_ROUTE))
-}
-
-ConfirmSendEther.prototype.isBalanceSufficient = function (txMeta) {
- const {
- balance,
- conversionRate,
- } = this.props
- const {
- txParams: {
- gas,
- gasPrice,
- value: amount,
- },
- } = txMeta
- const gasTotal = calcGasTotal(gas, gasPrice)
-
- return isBalanceSufficient({
- amount,
- gasTotal,
- balance,
- conversionRate,
- })
-}
-
-ConfirmSendEther.prototype.checkValidity = function () {
- const form = this.getFormEl()
- const valid = form.checkValidity()
- return valid
-}
-
-ConfirmSendEther.prototype.getFormEl = function () {
- const form = document.querySelector('form#pending-tx-form')
- // Stub out form for unit tests:
- if (!form) {
- return { checkValidity () { return true } }
- }
- return form
-}
-
-// After a customizable state value has been updated,
-ConfirmSendEther.prototype.gatherTxMeta = function () {
- const props = this.props
- const state = this.state
- const txData = clone(state.txData) || clone(props.txData)
-
- const { gasPrice: sendGasPrice, gasLimit: sendGasLimit } = props.send
- const {
- lastGasPrice,
- txParams: {
- gasPrice: txGasPrice,
- gas: txGasLimit,
- },
- } = txData
-
- let forceGasMin
- if (lastGasPrice) {
- forceGasMin = ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, {
- multiplicandBase: 16,
- multiplierBase: 10,
- toNumericBase: 'hex',
- }))
- }
-
- txData.txParams.gasPrice = sendGasPrice || forceGasMin || txGasPrice
- txData.txParams.gas = sendGasLimit || txGasLimit
-
- // log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`)
- return txData
-}
-
-ConfirmSendEther.prototype.verifyGasParams = function () {
- // We call this in case the gas has not been modified at all
- if (!this.state) { return true }
- return (
- this._notZeroOrEmptyString(this.state.gas) &&
- this._notZeroOrEmptyString(this.state.gasPrice)
- )
-}
-
-ConfirmSendEther.prototype._notZeroOrEmptyString = function (obj) {
- return obj !== '' && obj !== '0x0'
-}
-
-ConfirmSendEther.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) {
- const numBN = new BN(numerator)
- const denomBN = new BN(denominator)
- return targetBN.mul(numBN).div(denomBN)
-}
diff --git a/ui/app/components/pending-tx/confirm-send-token.js b/ui/app/components/pending-tx/confirm-send-token.js
deleted file mode 100644
index 535347cee..000000000
--- a/ui/app/components/pending-tx/confirm-send-token.js
+++ /dev/null
@@ -1,696 +0,0 @@
-const Component = require('react').Component
-const { withRouter } = require('react-router-dom')
-const { compose } = require('recompose')
-const PropTypes = require('prop-types')
-const connect = require('react-redux').connect
-const h = require('react-hyperscript')
-const inherits = require('util').inherits
-const tokenAbi = require('human-standard-token-abi')
-const abiDecoder = require('abi-decoder')
-abiDecoder.addABI(tokenAbi)
-const actions = require('../../actions')
-const clone = require('clone')
-const Identicon = require('../identicon')
-const GasFeeDisplay = require('../send_/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js').default
-const NetworkDisplay = require('../network-display')
-const ethUtil = require('ethereumjs-util')
-const BN = ethUtil.BN
-const {
- conversionUtil,
- multiplyCurrencies,
- addCurrencies,
-} = require('../../conversion-util')
-const {
- calcGasTotal,
- isBalanceSufficient,
-} = require('../send_/send.utils')
-const {
- calcTokenAmount,
-} = require('../../token-util')
-const classnames = require('classnames')
-const currencyFormatter = require('currency-formatter')
-const currencies = require('currency-formatter/currencies')
-
-const { MIN_GAS_PRICE_HEX } = require('../send_/send.constants')
-
-const {
- getTokenExchangeRate,
- getSelectedAddress,
- getSelectedTokenContract,
-} = require('../../selectors')
-const { SEND_ROUTE, DEFAULT_ROUTE } = require('../../routes')
-
-import {
- updateSendErrors,
-} from '../../ducks/send.duck'
-
-const {
- ENVIRONMENT_TYPE_POPUP,
- ENVIRONMENT_TYPE_NOTIFICATION,
-} = require('../../../../app/scripts/lib/enums')
-
-ConfirmSendToken.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = compose(
- withRouter,
- connect(mapStateToProps, mapDispatchToProps)
-)(ConfirmSendToken)
-
-
-function mapStateToProps (state, ownProps) {
- const { token: { address }, txData } = ownProps
- const { txParams } = txData || {}
- const tokenData = txParams.data && abiDecoder.decodeMethod(txParams.data)
-
- const {
- conversionRate,
- identities,
- currentCurrency,
- } = state.metamask
- const accounts = state.metamask.accounts
- const selectedAddress = getSelectedAddress(state)
- const tokenExchangeRate = getTokenExchangeRate(state, address)
- const { balance } = accounts[selectedAddress]
- return {
- conversionRate,
- identities,
- selectedAddress,
- tokenExchangeRate,
- tokenData: tokenData || {},
- currentCurrency: currentCurrency.toUpperCase(),
- send: state.metamask.send,
- tokenContract: getSelectedTokenContract(state),
- balance,
- }
-}
-
-function mapDispatchToProps (dispatch, ownProps) {
- return {
- backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)),
- cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })),
- editTransaction: txMeta => {
- const { token: { address } } = ownProps
- const { txParams = {}, id } = txMeta
- const tokenData = txParams.data && abiDecoder.decodeMethod(txParams.data) || {}
- const { params = [] } = tokenData
- const { value: to } = params[0] || {}
- const { value: tokenAmountInDec } = params[1] || {}
- const tokenAmountInHex = conversionUtil(tokenAmountInDec, {
- fromNumericBase: 'dec',
- toNumericBase: 'hex',
- })
- const {
- gas: gasLimit,
- gasPrice,
- } = txParams
- dispatch(actions.setSelectedToken(address))
- dispatch(actions.updateSend({
- gasLimit,
- gasPrice,
- gasTotal: null,
- to,
- amount: tokenAmountInHex,
- errors: { to: null, amount: null },
- editingTransactionId: id && id.toString(),
- token: ownProps.token,
- }))
- dispatch(actions.showSendTokenPage())
- },
- showCustomizeGasModal: (txMeta, sendGasLimit, sendGasPrice, sendGasTotal) => {
- const { id, txParams, lastGasPrice } = txMeta
- const { gas: txGasLimit, gasPrice: txGasPrice } = txParams
- const tokenData = txParams.data && abiDecoder.decodeMethod(txParams.data)
- const { params = [] } = tokenData
- const { value: to } = params[0] || {}
- const { value: tokenAmountInDec } = params[1] || {}
- const tokenAmountInHex = conversionUtil(tokenAmountInDec, {
- fromNumericBase: 'dec',
- toNumericBase: 'hex',
- })
-
- let forceGasMin
- if (lastGasPrice) {
- forceGasMin = ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, {
- multiplicandBase: 16,
- multiplierBase: 10,
- toNumericBase: 'hex',
- fromDenomination: 'WEI',
- }))
- }
-
- dispatch(actions.updateSend({
- gasLimit: sendGasLimit || txGasLimit,
- gasPrice: sendGasPrice || txGasPrice,
- editingTransactionId: id,
- gasTotal: sendGasTotal,
- to,
- amount: tokenAmountInHex,
- forceGasMin,
- }))
- dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' }))
- },
- updateSendErrors: error => dispatch(updateSendErrors(error)),
- }
-}
-
-inherits(ConfirmSendToken, Component)
-function ConfirmSendToken () {
- Component.call(this)
- this.state = {}
- this.onSubmit = this.onSubmit.bind(this)
-}
-
-ConfirmSendToken.prototype.editTransaction = function (txMeta) {
- const { editTransaction, history } = this.props
- editTransaction(txMeta)
- history.push(SEND_ROUTE)
-}
-
-ConfirmSendToken.prototype.updateComponentSendErrors = function (prevProps) {
- const {
- balance: oldBalance,
- conversionRate: oldConversionRate,
- } = prevProps
- const {
- updateSendErrors,
- balance,
- conversionRate,
- send: {
- errors: {
- simulationFails,
- },
- },
- } = this.props
- const txMeta = this.gatherTxMeta()
-
- const shouldUpdateBalanceSendErrors = balance && [
- balance !== oldBalance,
- conversionRate !== oldConversionRate,
- ].some(x => Boolean(x))
-
- if (shouldUpdateBalanceSendErrors) {
- const balanceIsSufficient = this.isBalanceSufficient(txMeta)
- updateSendErrors({
- insufficientFunds: balanceIsSufficient ? false : this.context.t('insufficientFunds'),
- })
- }
-
- const shouldUpdateSimulationSendError = Boolean(txMeta.simulationFails) !== Boolean(simulationFails)
-
- if (shouldUpdateSimulationSendError) {
- updateSendErrors({
- simulationFails: !txMeta.simulationFails ? false : this.context.t('transactionError'),
- })
- }
-}
-
-ConfirmSendToken.prototype.componentWillMount = function () {
- const { tokenContract, selectedAddress } = this.props
- tokenContract && tokenContract
- .balanceOf(selectedAddress)
- .then(usersToken => {
- })
- this.updateComponentSendErrors({})
-}
-
-ConfirmSendToken.prototype.componentDidUpdate = function (prevProps) {
- this.updateComponentSendErrors(prevProps)
-}
-
-ConfirmSendToken.prototype.getAmount = function () {
- const {
- conversionRate,
- tokenExchangeRate,
- token,
- tokenData,
- send: { amount, editingTransactionId },
- } = this.props
- const { params = [] } = tokenData
- let { value } = params[1] || {}
- const { decimals } = token
-
- if (editingTransactionId) {
- value = conversionUtil(amount, {
- fromNumericBase: 'hex',
- toNumericBase: 'dec',
- })
- }
-
- const sendTokenAmount = calcTokenAmount(value, decimals)
-
- return {
- fiat: tokenExchangeRate
- ? +(sendTokenAmount * tokenExchangeRate * conversionRate).toFixed(2)
- : null,
- token: typeof value === 'undefined'
- ? this.context.t('unknown')
- : +sendTokenAmount.toFixed(decimals),
- }
-
-}
-
-ConfirmSendToken.prototype.getGasFee = function () {
- const { conversionRate, tokenExchangeRate, token, currentCurrency } = this.props
- const txMeta = this.gatherTxMeta()
- const txParams = txMeta.txParams || {}
- const { decimals } = token
-
- const gas = txParams.gas
- const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_HEX
- const gasTotal = multiplyCurrencies(gas, gasPrice, {
- multiplicandBase: 16,
- multiplierBase: 16,
- })
-
- const FIAT = conversionUtil(gasTotal, {
- fromNumericBase: 'BN',
- toNumericBase: 'dec',
- fromDenomination: 'WEI',
- fromCurrency: 'ETH',
- toCurrency: currentCurrency,
- numberOfDecimals: 2,
- conversionRate,
- })
- const ETH = conversionUtil(gasTotal, {
- fromNumericBase: 'BN',
- toNumericBase: 'dec',
- fromDenomination: 'WEI',
- fromCurrency: 'ETH',
- toCurrency: 'ETH',
- numberOfDecimals: 6,
- conversionRate,
- })
- const tokenGas = multiplyCurrencies(gas, gasPrice, {
- toNumericBase: 'dec',
- multiplicandBase: 16,
- multiplierBase: 16,
- toCurrency: 'BAT',
- conversionRate: tokenExchangeRate,
- invertConversionRate: true,
- fromDenomination: 'WEI',
- numberOfDecimals: decimals || 4,
- })
-
- return {
- fiat: +Number(FIAT).toFixed(2),
- eth: ETH,
- token: tokenExchangeRate
- ? tokenGas
- : null,
- gasFeeInHex: gasTotal.toString(16),
- }
-}
-
-ConfirmSendToken.prototype.getData = function () {
- const { identities, tokenData } = this.props
- const { params = [] } = tokenData
- const { value } = params[0] || {}
- const txMeta = this.gatherTxMeta()
- const txParams = txMeta.txParams || {}
-
- return {
- from: {
- address: txParams.from,
- name: identities[txParams.from].name,
- },
- to: {
- address: value,
- name: identities[value] ? identities[value].name : this.context.t('newRecipient'),
- },
- memo: txParams.memo || '',
- }
-}
-
-ConfirmSendToken.prototype.renderHeroAmount = function () {
- const { token: { symbol }, currentCurrency } = this.props
- const { fiat: fiatAmount, token: tokenAmount } = this.getAmount()
- const txMeta = this.gatherTxMeta()
- const txParams = txMeta.txParams || {}
- const { memo = '' } = txParams
-
- const convertedAmountInFiat = this.convertToRenderableCurrency(fiatAmount, currentCurrency)
-
- return fiatAmount
- ? (
- h('div.confirm-send-token__hero-amount-wrapper', [
- h('h3.flex-center.confirm-screen-send-amount', `${convertedAmountInFiat}`),
- h('h3.flex-center.confirm-screen-send-amount-currency', currentCurrency),
- h('div.flex-center.confirm-memo-wrapper', [
- h('h3.confirm-screen-send-memo', [ memo ? `"${memo}"` : '' ]),
- ]),
- ])
- )
- : (
- h('div.confirm-send-token__hero-amount-wrapper', [
- h('h3.flex-center.confirm-screen-send-amount', tokenAmount),
- h('h3.flex-center.confirm-screen-send-amount-currency', symbol),
- h('div.flex-center.confirm-memo-wrapper', [
- h('h3.confirm-screen-send-memo', [ memo ? `"${memo}"` : '' ]),
- ]),
- ])
- )
-}
-
-ConfirmSendToken.prototype.renderGasFee = function () {
- const {
- currentCurrency: convertedCurrency,
- conversionRate,
- send: { gasTotal, gasLimit: sendGasLimit, gasPrice: sendGasPrice },
- showCustomizeGasModal,
- } = this.props
- const txMeta = this.gatherTxMeta()
- const { gasFeeInHex } = this.getGasFee()
-
- return (
- h('section.flex-row.flex-center.confirm-screen-row', [
- h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('gasFee') ]),
- h('div.confirm-screen-section-column', [
- h(GasFeeDisplay, {
- gasTotal: gasTotal || gasFeeInHex,
- conversionRate,
- convertedCurrency,
- onClick: () => showCustomizeGasModal(txMeta, sendGasLimit, sendGasPrice, gasTotal),
- }),
- ]),
- ])
- )
-}
-
-ConfirmSendToken.prototype.renderTotalPlusGas = function () {
- const { token: { symbol }, currentCurrency, send: { errors } } = this.props
- const { fiat: fiatAmount, token: tokenAmount } = this.getAmount()
- const { fiat: fiatGas, token: tokenGas } = this.getGasFee()
-
- const totalInFIAT = fiatAmount && fiatGas && addCurrencies(fiatAmount, fiatGas)
- const convertedTotalInFiat = this.convertToRenderableCurrency(totalInFIAT, currentCurrency)
-
- return fiatAmount && fiatGas
- ? (
- h('section.flex-row.flex-center.confirm-screen-row.confirm-screen-total-box ', [
- h('div.confirm-screen-section-column', [
- h('span.confirm-screen-label', [ this.context.t('total') + ' ' ]),
- h('div.confirm-screen-total-box__subtitle', [ this.context.t('amountPlusGas') ]),
- ]),
-
- h('div.confirm-screen-section-column', [
- h('div.confirm-screen-row-info', `${convertedTotalInFiat} ${currentCurrency}`),
- h('div.confirm-screen-row-detail', `${addCurrencies(tokenAmount, tokenGas || '0')} ${symbol}`),
- ]),
- ])
- )
- : (
- h('section.flex-row.flex-center.confirm-screen-row.confirm-screen-total-box ', [
- h('div', {
- className: classnames({
- 'confirm-screen-section-column--with-error': errors['insufficientFunds'],
- 'confirm-screen-section-column': !errors['insufficientFunds'],
- }),
- }, [
- h('span.confirm-screen-label', [ this.context.t('total') + ' ' ]),
- h('div.confirm-screen-total-box__subtitle', [ this.context.t('amountPlusGas') ]),
- ]),
-
- h('div.confirm-screen-section-column', [
- h('div.confirm-screen-row-info', `${tokenAmount} ${symbol}`),
- h('div.confirm-screen-row-detail', `+ ${fiatGas} ${currentCurrency} ${this.context.t('gas')}`),
- ]),
-
- this.renderErrorMessage('insufficientFunds'),
- ])
- )
-}
-
-ConfirmSendToken.prototype.renderErrorMessage = function (message) {
- const { send: { errors } } = this.props
-
- return errors[message]
- ? h('div.confirm-screen-error', [ errors[message] ])
- : null
-}
-
-ConfirmSendToken.prototype.convertToRenderableCurrency = function (value, currencyCode) {
- const upperCaseCurrencyCode = currencyCode.toUpperCase()
-
- return currencies.find(currency => currency.code === upperCaseCurrencyCode)
- ? currencyFormatter.format(Number(value), {
- code: upperCaseCurrencyCode,
- })
- : value
-}
-
-ConfirmSendToken.prototype.renderHeaderRow = function (isTxReprice) {
- const windowType = window.METAMASK_UI_TYPE
- const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION &&
- windowType !== ENVIRONMENT_TYPE_POPUP
-
- if (isTxReprice && isFullScreen) {
- return null
- }
-
- return (
- h('.page-container__header-row', [
- h('span.page-container__back-button', {
- onClick: () => this.editTransaction(),
- style: {
- visibility: isTxReprice ? 'hidden' : 'initial',
- },
- }, 'Edit'),
- !isFullScreen && h(NetworkDisplay),
- ])
- )
-}
-
-ConfirmSendToken.prototype.renderHeader = function (isTxReprice) {
- const title = isTxReprice ? this.context.t('speedUpTitle') : this.context.t('confirm')
- const subtitle = isTxReprice
- ? this.context.t('speedUpSubtitle')
- : this.context.t('pleaseReviewTransaction')
-
- return (
- h('.page-container__header', [
- this.renderHeaderRow(isTxReprice),
- h('.page-container__title', title),
- h('.page-container__subtitle', subtitle),
- ])
- )
-}
-
-ConfirmSendToken.prototype.render = function () {
- const txMeta = this.gatherTxMeta()
- const {
- from: {
- address: fromAddress,
- name: fromName,
- },
- to: {
- address: toAddress,
- name: toName,
- },
- } = this.getData()
-
- const isTxReprice = Boolean(txMeta.lastGasPrice)
-
- return (
- h('div.confirm-screen-container.confirm-send-token', [
- // Main Send token Card
- h('div.page-container', [
- this.renderHeader(isTxReprice),
- h('.page-container__content', [
- h('div.flex-row.flex-center.confirm-screen-identicons', [
- h('div.confirm-screen-account-wrapper', [
- h(
- Identicon,
- {
- address: fromAddress,
- diameter: 60,
- },
- ),
- h('span.confirm-screen-account-name', fromName),
- // h('span.confirm-screen-account-number', fromAddress.slice(fromAddress.length - 4)),
- ]),
- h('i.fa.fa-arrow-right.fa-lg'),
- h('div.confirm-screen-account-wrapper', [
- h(
- Identicon,
- {
- address: toAddress,
- diameter: 60,
- },
- ),
- h('span.confirm-screen-account-name', toName),
- // h('span.confirm-screen-account-number', toAddress.slice(toAddress.length - 4)),
- ]),
- ]),
-
- // h('h3.flex-center.confirm-screen-sending-to-message', {
- // style: {
- // textAlign: 'center',
- // fontSize: '16px',
- // },
- // }, [
- // `You're sending to Recipient ...${toAddress.slice(toAddress.length - 4)}`,
- // ]),
-
- this.renderHeroAmount(),
-
- h('div.confirm-screen-rows', [
- h('section.flex-row.flex-center.confirm-screen-row', [
- h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('from') ]),
- h('div.confirm-screen-section-column', [
- h('div.confirm-screen-row-info', fromName),
- h('div.confirm-screen-row-detail', `...${fromAddress.slice(fromAddress.length - 4)}`),
- ]),
- ]),
-
- toAddress && h('section.flex-row.flex-center.confirm-screen-row', [
- h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('to') ]),
- h('div.confirm-screen-section-column', [
- h('div.confirm-screen-row-info', toName),
- h('div.confirm-screen-row-detail', `...${toAddress.slice(toAddress.length - 4)}`),
- ]),
- ]),
-
- this.renderGasFee(),
-
- this.renderTotalPlusGas(),
-
- ]),
-
- ]),
-
- h('form#pending-tx-form', {
- className: 'confirm-screen-form',
- onSubmit: this.onSubmit,
- }, [
- this.renderErrorMessage('simulationFails'),
- h('.page-container__footer', [
- // Cancel Button
- h('button.btn-cancel.page-container__footer-button.allcaps', {
- onClick: (event) => this.cancel(event, txMeta),
- }, this.context.t('cancel')),
-
- // Accept Button
- h('button.btn-confirm.page-container__footer-button.allcaps', {
- onClick: event => this.onSubmit(event),
- }, [this.context.t('confirm')]),
- ]),
- ]),
- ]),
- ])
- )
-}
-
-ConfirmSendToken.prototype.onSubmit = function (event) {
- event.preventDefault()
- const { updateSendErrors } = this.props
- const txMeta = this.gatherTxMeta()
- const valid = this.checkValidity()
- const balanceIsSufficient = this.isBalanceSufficient(txMeta)
- this.setState({ valid, submitting: true })
-
- if (valid && this.verifyGasParams() && balanceIsSufficient) {
- this.props.sendTransaction(txMeta, event)
- } else if (!balanceIsSufficient) {
- updateSendErrors({ insufficientFunds: 'insufficientFunds' })
- } else {
- updateSendErrors({ invalidGasParams: 'invalidGasParams' })
- this.setState({ submitting: false })
- }
-}
-
-ConfirmSendToken.prototype.isBalanceSufficient = function (txMeta) {
- const {
- balance,
- conversionRate,
- } = this.props
- const {
- txParams: {
- gas,
- gasPrice,
- },
- } = txMeta
- const gasTotal = calcGasTotal(gas, gasPrice)
-
- return isBalanceSufficient({
- amount: '0',
- gasTotal,
- balance,
- conversionRate,
- })
-}
-
-
-ConfirmSendToken.prototype.cancel = function (event, txMeta) {
- event.preventDefault()
- const { cancelTransaction } = this.props
-
- cancelTransaction(txMeta)
- .then(() => this.props.history.push(DEFAULT_ROUTE))
-}
-
-ConfirmSendToken.prototype.checkValidity = function () {
- const form = this.getFormEl()
- const valid = form.checkValidity()
- return valid
-}
-
-ConfirmSendToken.prototype.getFormEl = function () {
- const form = document.querySelector('form#pending-tx-form')
- // Stub out form for unit tests:
- if (!form) {
- return { checkValidity () { return true } }
- }
- return form
-}
-
-// After a customizable state value has been updated,
-ConfirmSendToken.prototype.gatherTxMeta = function () {
- const props = this.props
- const state = this.state
- const txData = clone(state.txData) || clone(props.txData)
-
- const { gasPrice: sendGasPrice, gasLimit: sendGasLimit } = props.send
- const {
- lastGasPrice,
- txParams: {
- gasPrice: txGasPrice,
- gas: txGasLimit,
- },
- } = txData
-
- let forceGasMin
- if (lastGasPrice) {
- forceGasMin = ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, {
- multiplicandBase: 16,
- multiplierBase: 10,
- toNumericBase: 'hex',
- }))
- }
-
- txData.txParams.gasPrice = sendGasPrice || forceGasMin || txGasPrice
- txData.txParams.gas = sendGasLimit || txGasLimit
-
- // log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`)
- return txData
-}
-
-ConfirmSendToken.prototype.verifyGasParams = function () {
- // We call this in case the gas has not been modified at all
- if (!this.state) { return true }
- return (
- this._notZeroOrEmptyString(this.state.gas) &&
- this._notZeroOrEmptyString(this.state.gasPrice)
- )
-}
-
-ConfirmSendToken.prototype._notZeroOrEmptyString = function (obj) {
- return obj !== '' && obj !== '0x0'
-}
-
-ConfirmSendToken.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) {
- const numBN = new BN(numerator)
- const denomBN = new BN(denominator)
- return targetBN.mul(numBN).div(denomBN)
-}
diff --git a/ui/app/components/pending-tx/index.js b/ui/app/components/pending-tx/index.js
deleted file mode 100644
index 3f8cd8823..000000000
--- a/ui/app/components/pending-tx/index.js
+++ /dev/null
@@ -1,165 +0,0 @@
-const Component = require('react').Component
-const connect = require('react-redux').connect
-const h = require('react-hyperscript')
-const PropTypes = require('prop-types')
-const clone = require('clone')
-const abi = require('human-standard-token-abi')
-const abiDecoder = require('abi-decoder')
-abiDecoder.addABI(abi)
-const inherits = require('util').inherits
-const actions = require('../../actions')
-const { getSymbolAndDecimals } = require('../../token-util')
-const ConfirmSendEther = require('./confirm-send-ether')
-const ConfirmSendToken = require('./confirm-send-token')
-const ConfirmDeployContract = require('./confirm-deploy-contract')
-const Loading = require('../loading-screen')
-
-const TX_TYPES = {
- DEPLOY_CONTRACT: 'deploy_contract',
- SEND_ETHER: 'send_ether',
- SEND_TOKEN: 'send_token',
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(PendingTx)
-
-function mapStateToProps (state) {
- const {
- conversionRate,
- identities,
- tokens: existingTokens,
- } = state.metamask
- const accounts = state.metamask.accounts
- const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
- return {
- conversionRate,
- identities,
- selectedAddress,
- existingTokens,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)),
- cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })),
- }
-}
-
-inherits(PendingTx, Component)
-function PendingTx () {
- Component.call(this)
- this.state = {
- isFetching: true,
- transactionType: '',
- tokenAddress: '',
- tokenSymbol: '',
- tokenDecimals: '',
- }
-}
-
-PendingTx.prototype.componentDidMount = function () {
- this.setTokenData()
-}
-
-PendingTx.prototype.componentDidUpdate = function (prevProps, prevState) {
- if (prevState.isFetching) {
- this.setTokenData()
- }
-}
-
-PendingTx.prototype.setTokenData = async function () {
- const { existingTokens } = this.props
- const txMeta = this.gatherTxMeta()
- const txParams = txMeta.txParams || {}
-
- if (txMeta.loadingDefaults) {
- return
- }
-
- if (!txParams.to) {
- return this.setState({
- transactionType: TX_TYPES.DEPLOY_CONTRACT,
- isFetching: false,
- })
- }
-
- // inspect tx data for supported special confirmation screens
- let isTokenTransaction = false
- if (txParams.data) {
- const tokenData = abiDecoder.decodeMethod(txParams.data)
- const { name: tokenMethodName } = tokenData || {}
- isTokenTransaction = (tokenMethodName === 'transfer')
- }
-
- if (isTokenTransaction) {
- const { symbol, decimals } = await getSymbolAndDecimals(txParams.to, existingTokens)
-
- this.setState({
- transactionType: TX_TYPES.SEND_TOKEN,
- tokenAddress: txParams.to,
- tokenSymbol: symbol,
- tokenDecimals: decimals,
- isFetching: false,
- })
- } else {
- this.setState({
- transactionType: TX_TYPES.SEND_ETHER,
- isFetching: false,
- })
- }
-}
-
-PendingTx.prototype.gatherTxMeta = function () {
- const props = this.props
- const state = this.state
- const txData = clone(state.txData) || clone(props.txData)
-
- return txData
-}
-
-PendingTx.prototype.render = function () {
- const {
- isFetching,
- transactionType,
- tokenAddress,
- tokenSymbol,
- tokenDecimals,
- } = this.state
-
- const { sendTransaction } = this.props
-
- if (isFetching) {
- return h(Loading, {
- loadingMessage: this.context.t('generatingTransaction'),
- })
- }
-
- switch (transactionType) {
- case TX_TYPES.SEND_ETHER:
- return h(ConfirmSendEther, {
- txData: this.gatherTxMeta(),
- sendTransaction,
- })
- case TX_TYPES.SEND_TOKEN:
- return h(ConfirmSendToken, {
- txData: this.gatherTxMeta(),
- sendTransaction,
- token: {
- address: tokenAddress,
- symbol: tokenSymbol,
- decimals: tokenDecimals,
- },
- })
- case TX_TYPES.DEPLOY_CONTRACT:
- return h(ConfirmDeployContract, {
- txData: this.gatherTxMeta(),
- sendTransaction,
- })
- default:
- return h(Loading)
- }
-}
-
-PendingTx.contextTypes = {
- t: PropTypes.func,
-}
diff --git a/ui/app/components/selected-account/selected-account.component.js b/ui/app/components/selected-account/selected-account.component.js
index 3386a4196..6c202141e 100644
--- a/ui/app/components/selected-account/selected-account.component.js
+++ b/ui/app/components/selected-account/selected-account.component.js
@@ -1,17 +1,10 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import copyToClipboard from 'copy-to-clipboard'
+import { addressSlicer } from '../../util'
const Tooltip = require('../tooltip-v2.js')
-const addressStripper = (address = '') => {
- if (address.length < 4) {
- return address
- }
-
- return `${address.slice(0, 4)}...${address.slice(-4)}`
-}
-
class SelectedAccount extends Component {
state = {
copied: false,
@@ -48,7 +41,7 @@ class SelectedAccount extends Component {
{ selectedIdentity.name }
</div>
<div className="selected-account__address">
- { addressStripper(selectedAddress) }
+ { addressSlicer(selectedAddress) }
</div>
</div>
</Tooltip>
diff --git a/ui/app/components/send_/README.md b/ui/app/components/send/README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/README.md
+++ b/ui/app/components/send/README.md
diff --git a/ui/app/components/send_/account-list-item/account-list-item-README.md b/ui/app/components/send/account-list-item/account-list-item-README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/account-list-item/account-list-item-README.md
+++ b/ui/app/components/send/account-list-item/account-list-item-README.md
diff --git a/ui/app/components/send_/account-list-item/account-list-item.component.js b/ui/app/components/send/account-list-item/account-list-item.component.js
index b8407d147..9f4a96e61 100644
--- a/ui/app/components/send_/account-list-item/account-list-item.component.js
+++ b/ui/app/components/send/account-list-item/account-list-item.component.js
@@ -2,7 +2,7 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { checksumAddress } from '../../../util'
import Identicon from '../../identicon'
-import CurrencyDisplay from '../../send/currency-display'
+import CurrencyDisplay from '../currency-display'
export default class AccountListItem extends Component {
@@ -17,6 +17,10 @@ export default class AccountListItem extends Component {
icon: PropTypes.node,
};
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
render () {
const {
account,
@@ -67,8 +71,3 @@ export default class AccountListItem extends Component {
</div>)
}
}
-
-AccountListItem.contextTypes = {
- t: PropTypes.func,
-}
-
diff --git a/ui/app/components/send_/account-list-item/account-list-item.container.js b/ui/app/components/send/account-list-item/account-list-item.container.js
index 4b4519288..4b4519288 100644
--- a/ui/app/components/send_/account-list-item/account-list-item.container.js
+++ b/ui/app/components/send/account-list-item/account-list-item.container.js
diff --git a/ui/app/components/send_/account-list-item/account-list-item.scss b/ui/app/components/send/account-list-item/account-list-item.scss
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/account-list-item/account-list-item.scss
+++ b/ui/app/components/send/account-list-item/account-list-item.scss
diff --git a/ui/app/components/send_/account-list-item/index.js b/ui/app/components/send/account-list-item/index.js
index 907485cf7..907485cf7 100644
--- a/ui/app/components/send_/account-list-item/index.js
+++ b/ui/app/components/send/account-list-item/index.js
diff --git a/ui/app/components/send_/account-list-item/tests/account-list-item-component.test.js b/ui/app/components/send/account-list-item/tests/account-list-item-component.test.js
index bb7f3776c..ef152d2e7 100644
--- a/ui/app/components/send_/account-list-item/tests/account-list-item-component.test.js
+++ b/ui/app/components/send/account-list-item/tests/account-list-item-component.test.js
@@ -4,7 +4,7 @@ import { shallow } from 'enzyme'
import sinon from 'sinon'
import proxyquire from 'proxyquire'
import Identicon from '../../../identicon'
-import CurrencyDisplay from '../../../send/currency-display'
+import CurrencyDisplay from '../../currency-display'
const utilsMethodStubs = {
checksumAddress: sinon.stub().returns('mockCheckSumAddress'),
diff --git a/ui/app/components/send_/account-list-item/tests/account-list-item-container.test.js b/ui/app/components/send/account-list-item/tests/account-list-item-container.test.js
index af0859117..af0859117 100644
--- a/ui/app/components/send_/account-list-item/tests/account-list-item-container.test.js
+++ b/ui/app/components/send/account-list-item/tests/account-list-item-container.test.js
diff --git a/ui/app/components/send/currency-display.js b/ui/app/components/send/currency-display/currency-display.js
index 1cf55ce1a..2b8eaa41f 100644
--- a/ui/app/components/send/currency-display.js
+++ b/ui/app/components/send/currency-display/currency-display.js
@@ -1,11 +1,16 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
-const { conversionUtil, multiplyCurrencies } = require('../../conversion-util')
-const { removeLeadingZeroes } = require('../send_/send.utils')
+const { conversionUtil, multiplyCurrencies } = require('../../../conversion-util')
+const { removeLeadingZeroes } = require('../send.utils')
const currencyFormatter = require('currency-formatter')
const currencies = require('currency-formatter/currencies')
const ethUtil = require('ethereumjs-util')
+const PropTypes = require('prop-types')
+
+CurrencyDisplay.contextTypes = {
+ t: PropTypes.func,
+}
module.exports = CurrencyDisplay
@@ -75,6 +80,12 @@ CurrencyDisplay.prototype.getValueToRender = function ({ selectedToken, conversi
CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValue) {
const { primaryCurrency, convertedCurrency, conversionRate } = this.props
+ if (conversionRate === 0 || conversionRate === null || conversionRate === undefined) {
+ if (nonFormattedValue !== 0) {
+ return null
+ }
+ }
+
let convertedValue = conversionUtil(nonFormattedValue, {
fromNumericBase: 'dec',
fromCurrency: primaryCurrency,
@@ -82,16 +93,15 @@ CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValu
numberOfDecimals: 2,
conversionRate,
})
- convertedValue = Number(convertedValue).toFixed(2)
+ convertedValue = Number(convertedValue).toFixed(2)
const upperCaseCurrencyCode = convertedCurrency.toUpperCase()
-
return currencies.find(currency => currency.code === upperCaseCurrencyCode)
? currencyFormatter.format(Number(convertedValue), {
code: upperCaseCurrencyCode,
})
- : convertedValue
-}
+ : convertedValue
+ }
CurrencyDisplay.prototype.handleChange = function (newVal) {
this.setState({ valueToRender: removeLeadingZeroes(newVal) })
@@ -105,13 +115,24 @@ CurrencyDisplay.prototype.getInputWidth = function (valueToRender, readOnly) {
return (valueLength + decimalPointDeficit + 0.75) + 'ch'
}
+CurrencyDisplay.prototype.onlyRenderConversions = function (convertedValueToRender) {
+ const {
+ convertedBalanceClassName = 'currency-display__converted-value',
+ convertedCurrency,
+ } = this.props
+ return h('div', {
+ className: convertedBalanceClassName,
+ }, convertedValueToRender == null
+ ? this.context.t('noConversionRateAvailable')
+ : `${convertedValueToRender} ${convertedCurrency.toUpperCase()}`
+)
+ }
+
CurrencyDisplay.prototype.render = function () {
const {
className = 'currency-display',
primaryBalanceClassName = 'currency-display__input',
- convertedBalanceClassName = 'currency-display__converted-value',
primaryCurrency,
- convertedCurrency,
readOnly = false,
inError = false,
onBlur,
@@ -157,11 +178,7 @@ CurrencyDisplay.prototype.render = function () {
]),
- ]),
-
- h('div', {
- className: convertedBalanceClassName,
- }, `${convertedValueToRender} ${convertedCurrency.toUpperCase()}`),
+ ]), this.onlyRenderConversions(convertedValueToRender),
])
diff --git a/ui/app/components/send/currency-display/index.js b/ui/app/components/send/currency-display/index.js
new file mode 100644
index 000000000..5dc269c5a
--- /dev/null
+++ b/ui/app/components/send/currency-display/index.js
@@ -0,0 +1 @@
+export { default } from './currency-display.js'
diff --git a/ui/app/components/send_/index.js b/ui/app/components/send/index.js
index b5114babc..b5114babc 100644
--- a/ui/app/components/send_/index.js
+++ b/ui/app/components/send/index.js
diff --git a/ui/app/components/send_/send-content/index.js b/ui/app/components/send/send-content/index.js
index 891c17e6a..891c17e6a 100644
--- a/ui/app/components/send_/send-content/index.js
+++ b/ui/app/components/send/send-content/index.js
diff --git a/ui/app/components/send_/send-content/send-amount-row/README.md b/ui/app/components/send/send-content/send-amount-row/README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-amount-row/README.md
+++ b/ui/app/components/send/send-content/send-amount-row/README.md
diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.component.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js
index bdf12b738..4d0d36ab4 100644
--- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.component.js
+++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js
@@ -13,6 +13,10 @@ export default class AmountMaxButton extends Component {
tokenBalance: PropTypes.string,
};
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
setMaxAmount () {
const {
balance,
@@ -48,7 +52,3 @@ export default class AmountMaxButton extends Component {
}
}
-
-AmountMaxButton.contextTypes = {
- t: PropTypes.func,
-}
diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.container.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js
index 2d2ec42f7..2d2ec42f7 100644
--- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.container.js
+++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js
diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js
index 69fec1994..69fec1994 100644
--- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js
+++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js
diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js
index b490a7fd7..b490a7fd7 100644
--- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js
+++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js
diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/index.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/index.js
index ee8271494..ee8271494 100644
--- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/index.js
+++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/index.js
diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js
index 86a05ff21..86a05ff21 100644
--- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js
+++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js
diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js
index 2cc00d6d6..2cc00d6d6 100644
--- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js
+++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js
diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js
index 655fe1969..655fe1969 100644
--- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js
+++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js
diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js
index 816df6a12..816df6a12 100644
--- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js
+++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js
diff --git a/ui/app/components/send_/send-content/send-amount-row/index.js b/ui/app/components/send/send-content/send-amount-row/index.js
index abc6852fe..abc6852fe 100644
--- a/ui/app/components/send_/send-content/send-amount-row/index.js
+++ b/ui/app/components/send/send-content/send-amount-row/index.js
diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js b/ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js
index 196538c11..c548a5695 100644
--- a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js
+++ b/ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js
@@ -2,7 +2,7 @@ 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'
+import CurrencyDisplay from '../../currency-display'
export default class SendAmountRow extends Component {
@@ -21,10 +21,15 @@ export default class SendAmountRow extends Component {
selectedToken: PropTypes.object,
setMaxModeTo: PropTypes.func,
tokenBalance: PropTypes.string,
+ updateGasFeeError: PropTypes.func,
updateSendAmount: PropTypes.func,
updateSendAmountError: PropTypes.func,
updateGas: PropTypes.func,
- }
+ };
+
+ static contextTypes = {
+ t: PropTypes.func,
+ };
validateAmount (amount) {
const {
@@ -35,6 +40,7 @@ export default class SendAmountRow extends Component {
primaryCurrency,
selectedToken,
tokenBalance,
+ updateGasFeeError,
updateSendAmountError,
} = this.props
@@ -48,6 +54,19 @@ export default class SendAmountRow extends Component {
selectedToken,
tokenBalance,
})
+
+ if (selectedToken) {
+ updateGasFeeError({
+ amount,
+ amountConversionRate,
+ balance,
+ conversionRate,
+ gasTotal,
+ primaryCurrency,
+ selectedToken,
+ tokenBalance,
+ })
+ }
}
updateAmount (amount) {
@@ -102,8 +121,3 @@ export default class SendAmountRow extends Component {
}
}
-
-SendAmountRow.contextTypes = {
- t: PropTypes.func,
-}
-
diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js b/ui/app/components/send/send-content/send-amount-row/send-amount-row.container.js
index b816d948f..3504d1b73 100644
--- a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js
+++ b/ui/app/components/send/send-content/send-amount-row/send-amount-row.container.js
@@ -13,7 +13,7 @@ import {
import {
sendAmountIsInError,
} from './send-amount-row.selectors'
-import { getAmountErrorObject } from '../../send.utils'
+import { getAmountErrorObject, getGasFeeErrorObject } from '../../send.utils'
import {
setMaxModeTo,
updateSendAmount,
@@ -44,6 +44,9 @@ function mapDispatchToProps (dispatch) {
return {
setMaxModeTo: bool => dispatch(setMaxModeTo(bool)),
updateSendAmount: newAmount => dispatch(updateSendAmount(newAmount)),
+ updateGasFeeError: (amountDataObject) => {
+ dispatch(updateSendErrors(getGasFeeErrorObject(amountDataObject)))
+ },
updateSendAmountError: (amountDataObject) => {
dispatch(updateSendErrors(getAmountErrorObject(amountDataObject)))
},
diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.scss b/ui/app/components/send/send-content/send-amount-row/send-amount-row.scss
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.scss
+++ b/ui/app/components/send/send-content/send-amount-row/send-amount-row.scss
diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.selectors.js b/ui/app/components/send/send-content/send-amount-row/send-amount-row.selectors.js
index fb08c7ed7..fb08c7ed7 100644
--- a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.selectors.js
+++ b/ui/app/components/send/send-content/send-amount-row/send-amount-row.selectors.js
diff --git a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js b/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js
index 579e18585..8425e076e 100644
--- a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js
+++ b/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js
@@ -6,13 +6,14 @@ 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'
+import CurrencyDisplay from '../../../currency-display'
const propsMethodSpies = {
setMaxModeTo: sinon.spy(),
updateSendAmount: sinon.spy(),
updateSendAmountError: sinon.spy(),
updateGas: sinon.spy(),
+ updateGasFeeError: sinon.spy(),
}
sinon.spy(SendAmountRow.prototype, 'updateAmount')
@@ -36,6 +37,7 @@ describe('SendAmountRow Component', function () {
selectedToken={ { address: 'mockTokenAddress' } }
setMaxModeTo={propsMethodSpies.setMaxModeTo}
tokenBalance={'mockTokenBalance'}
+ updateGasFeeError={propsMethodSpies.updateGasFeeError}
updateSendAmount={propsMethodSpies.updateSendAmount}
updateSendAmountError={propsMethodSpies.updateSendAmountError}
updateGas={propsMethodSpies.updateGas}
@@ -47,6 +49,7 @@ describe('SendAmountRow Component', function () {
propsMethodSpies.setMaxModeTo.resetHistory()
propsMethodSpies.updateSendAmount.resetHistory()
propsMethodSpies.updateSendAmountError.resetHistory()
+ propsMethodSpies.updateGasFeeError.resetHistory()
SendAmountRow.prototype.validateAmount.resetHistory()
SendAmountRow.prototype.updateAmount.resetHistory()
})
@@ -72,6 +75,32 @@ describe('SendAmountRow Component', function () {
)
})
+ it('should call updateGasFeeError if selectedToken is truthy', () => {
+ assert.equal(propsMethodSpies.updateGasFeeError.callCount, 0)
+ instance.validateAmount('someAmount')
+ assert.equal(propsMethodSpies.updateGasFeeError.callCount, 1)
+ assert.deepEqual(
+ propsMethodSpies.updateGasFeeError.getCall(0).args,
+ [{
+ amount: 'someAmount',
+ amountConversionRate: 'mockAmountConversionRate',
+ balance: 'mockBalance',
+ conversionRate: 7,
+ gasTotal: 'mockGasTotal',
+ primaryCurrency: 'mockPrimaryCurrency',
+ selectedToken: { address: 'mockTokenAddress' },
+ tokenBalance: 'mockTokenBalance',
+ }]
+ )
+ })
+
+ it('should call not updateGasFeeError if selectedToken is falsey', () => {
+ wrapper.setProps({ selectedToken: null })
+ assert.equal(propsMethodSpies.updateGasFeeError.callCount, 0)
+ instance.validateAmount('someAmount')
+ assert.equal(propsMethodSpies.updateGasFeeError.callCount, 0)
+ })
+
})
describe('updateAmount', () => {
diff --git a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-container.test.js b/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-container.test.js
index 94d9918a7..52e351aee 100644
--- a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-container.test.js
+++ b/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-container.test.js
@@ -33,7 +33,10 @@ proxyquire('../send-amount-row.container.js', {
getTokenBalance: (s) => `mockTokenBalance:${s}`,
},
'./send-amount-row.selectors': { sendAmountIsInError: (s) => `mockInError:${s}` },
- '../../send.utils': { getAmountErrorObject: (mockDataObject) => ({ ...mockDataObject, mockChange: true }) },
+ '../../send.utils': {
+ getAmountErrorObject: (mockDataObject) => ({ ...mockDataObject, mockChange: true }),
+ getGasFeeErrorObject: (mockDataObject) => ({ ...mockDataObject, mockGasFeeErrorChange: true }),
+ },
'../../../../actions': actionSpies,
'../../../../ducks/send.duck': duckActionSpies,
})
@@ -66,6 +69,7 @@ describe('send-amount-row container', () => {
beforeEach(() => {
dispatchSpy = sinon.spy()
mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
+ duckActionSpies.updateSendErrors.resetHistory()
})
describe('setMaxModeTo()', () => {
@@ -92,6 +96,18 @@ describe('send-amount-row container', () => {
})
})
+ describe('updateGasFeeError()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.updateGasFeeError({ some: 'data' })
+ assert(dispatchSpy.calledOnce)
+ assert(duckActionSpies.updateSendErrors.calledOnce)
+ assert.deepEqual(
+ duckActionSpies.updateSendErrors.getCall(0).args[0],
+ { some: 'data', mockGasFeeErrorChange: true }
+ )
+ })
+ })
+
describe('updateSendAmountError()', () => {
it('should dispatch an action', () => {
mapDispatchToPropsObject.updateSendAmountError({ some: 'data' })
diff --git a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-selectors.test.js b/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js
index 4672cb8a7..4672cb8a7 100644
--- a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-selectors.test.js
+++ b/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js
diff --git a/ui/app/components/send_/send-content/send-content-README.md b/ui/app/components/send/send-content/send-content-README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-content-README.md
+++ b/ui/app/components/send/send-content/send-content-README.md
diff --git a/ui/app/components/send_/send-content/send-content.component.js b/ui/app/components/send/send-content/send-content.component.js
index adc114c0e..7a0b1a18e 100644
--- a/ui/app/components/send_/send-content/send-content.component.js
+++ b/ui/app/components/send/send-content/send-content.component.js
@@ -4,6 +4,7 @@ import PageContainerContent from '../../page-container/page-container-content.co
import SendAmountRow from './send-amount-row/'
import SendFromRow from './send-from-row/'
import SendGasRow from './send-gas-row/'
+import SendHexDataRow from './send-hex-data-row'
import SendToRow from './send-to-row/'
export default class SendContent extends Component {
@@ -20,6 +21,7 @@ export default class SendContent extends Component {
<SendToRow updateGas={(updateData) => this.props.updateGas(updateData)} />
<SendAmountRow updateGas={(updateData) => this.props.updateGas(updateData)} />
<SendGasRow />
+ <SendHexDataRow />
</div>
</PageContainerContent>
)
diff --git a/ui/app/components/send_/send-content/send-content.scss b/ui/app/components/send/send-content/send-content.scss
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-content.scss
+++ b/ui/app/components/send/send-content/send-content.scss
diff --git a/ui/app/components/send_/send-content/send-dropdown-list/index.js b/ui/app/components/send/send-content/send-dropdown-list/index.js
index 04af6536c..04af6536c 100644
--- a/ui/app/components/send_/send-content/send-dropdown-list/index.js
+++ b/ui/app/components/send/send-content/send-dropdown-list/index.js
diff --git a/ui/app/components/send_/send-content/send-dropdown-list/send-dropdown-list.component.js b/ui/app/components/send/send-content/send-dropdown-list/send-dropdown-list.component.js
index 5c7174ecf..bedac1259 100644
--- a/ui/app/components/send_/send-content/send-dropdown-list/send-dropdown-list.component.js
+++ b/ui/app/components/send/send-content/send-dropdown-list/send-dropdown-list.component.js
@@ -11,6 +11,10 @@ export default class SendDropdownList extends Component {
activeAddress: PropTypes.string,
};
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
getListItemIcon (accountAddress, activeAddress) {
return accountAddress === activeAddress
? <i className={`fa fa-check fa-lg`} style={ { color: '#02c9b1' } }/>
@@ -46,7 +50,3 @@ export default class SendDropdownList extends Component {
}
}
-
-SendDropdownList.contextTypes = {
- t: PropTypes.func,
-}
diff --git a/ui/app/components/send_/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js b/ui/app/components/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js
index b92dd4dfe..b92dd4dfe 100644
--- a/ui/app/components/send_/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js
+++ b/ui/app/components/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js
diff --git a/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown-README.md b/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown-README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown-README.md
+++ b/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown-README.md
diff --git a/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.component.js b/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.component.js
index 418766cd9..4f43a9d61 100644
--- a/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.component.js
+++ b/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.component.js
@@ -14,6 +14,10 @@ export default class FromDropdown extends Component {
selectedAccount: PropTypes.object,
};
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
render () {
const {
accounts,
@@ -40,7 +44,3 @@ export default class FromDropdown extends Component {
}
}
-
-FromDropdown.contextTypes = {
- t: PropTypes.func,
-}
diff --git a/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.scss b/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.scss
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.scss
+++ b/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.scss
diff --git a/ui/app/components/send_/send-content/send-from-row/from-dropdown/index.js b/ui/app/components/send/send-content/send-from-row/from-dropdown/index.js
index 2314ef4e3..2314ef4e3 100644
--- a/ui/app/components/send_/send-content/send-from-row/from-dropdown/index.js
+++ b/ui/app/components/send/send-content/send-from-row/from-dropdown/index.js
diff --git a/ui/app/components/send_/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js b/ui/app/components/send/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js
index 84fcb281e..84fcb281e 100644
--- a/ui/app/components/send_/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js
+++ b/ui/app/components/send/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js
diff --git a/ui/app/components/send_/send-content/send-from-row/index.js b/ui/app/components/send/send-content/send-from-row/index.js
index 0a79726b2..0a79726b2 100644
--- a/ui/app/components/send_/send-content/send-from-row/index.js
+++ b/ui/app/components/send/send-content/send-from-row/index.js
diff --git a/ui/app/components/send_/send-content/send-from-row/send-from-row-README.md b/ui/app/components/send/send-content/send-from-row/send-from-row-README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-from-row/send-from-row-README.md
+++ b/ui/app/components/send/send-content/send-from-row/send-from-row-README.md
diff --git a/ui/app/components/send_/send-content/send-from-row/send-from-row.component.js b/ui/app/components/send/send-content/send-from-row/send-from-row.component.js
index a580aef96..3e0e0de22 100644
--- a/ui/app/components/send_/send-content/send-from-row/send-from-row.component.js
+++ b/ui/app/components/send/send-content/send-from-row/send-from-row.component.js
@@ -17,6 +17,10 @@ export default class SendFromRow extends Component {
setSendTokenBalance: PropTypes.func,
};
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
async handleFromChange (newFrom) {
const {
updateSendFrom,
@@ -57,7 +61,3 @@ export default class SendFromRow extends Component {
}
}
-
-SendFromRow.contextTypes = {
- t: PropTypes.func,
-}
diff --git a/ui/app/components/send_/send-content/send-from-row/send-from-row.container.js b/ui/app/components/send/send-content/send-from-row/send-from-row.container.js
index 33cb63b43..33cb63b43 100644
--- a/ui/app/components/send_/send-content/send-from-row/send-from-row.container.js
+++ b/ui/app/components/send/send-content/send-from-row/send-from-row.container.js
diff --git a/ui/app/components/send_/send-content/send-from-row/send-from-row.selectors.js b/ui/app/components/send/send-content/send-from-row/send-from-row.selectors.js
index 03ef4806b..03ef4806b 100644
--- a/ui/app/components/send_/send-content/send-from-row/send-from-row.selectors.js
+++ b/ui/app/components/send/send-content/send-from-row/send-from-row.selectors.js
diff --git a/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-component.test.js b/ui/app/components/send/send-content/send-from-row/tests/send-from-row-component.test.js
index 9ba8d1739..9ba8d1739 100644
--- a/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-component.test.js
+++ b/ui/app/components/send/send-content/send-from-row/tests/send-from-row-component.test.js
diff --git a/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-container.test.js b/ui/app/components/send/send-content/send-from-row/tests/send-from-row-container.test.js
index e080b2fe3..e080b2fe3 100644
--- a/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-container.test.js
+++ b/ui/app/components/send/send-content/send-from-row/tests/send-from-row-container.test.js
diff --git a/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-selectors.test.js b/ui/app/components/send/send-content/send-from-row/tests/send-from-row-selectors.test.js
index ecb57bbc3..ecb57bbc3 100644
--- a/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-selectors.test.js
+++ b/ui/app/components/send/send-content/send-from-row/tests/send-from-row-selectors.test.js
diff --git a/ui/app/components/send_/send-content/send-gas-row/README.md b/ui/app/components/send/send-content/send-gas-row/README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-gas-row/README.md
+++ b/ui/app/components/send/send-content/send-gas-row/README.md
diff --git a/ui/app/components/send_/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js
index c8d619be5..bb9a94428 100644
--- a/ui/app/components/send_/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js
+++ b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js
@@ -14,6 +14,10 @@ export default class GasFeeDisplay extends Component {
onClick: PropTypes.func,
};
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
render () {
const {
conversionRate,
@@ -55,7 +59,3 @@ export default class GasFeeDisplay extends Component {
)
}
}
-
-GasFeeDisplay.contextTypes = {
- t: PropTypes.func,
-}
diff --git a/ui/app/components/send_/send-content/send-gas-row/gas-fee-display/index.js b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/index.js
index dba0edb7b..dba0edb7b 100644
--- a/ui/app/components/send_/send-content/send-gas-row/gas-fee-display/index.js
+++ b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/index.js
diff --git a/ui/app/components/send_/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js
index 7cbe8d0df..7cbe8d0df 100644
--- a/ui/app/components/send_/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js
+++ b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js
diff --git a/ui/app/components/send_/send-content/send-gas-row/index.js b/ui/app/components/send/send-content/send-gas-row/index.js
index 3c7ff1d5f..3c7ff1d5f 100644
--- a/ui/app/components/send_/send-content/send-gas-row/index.js
+++ b/ui/app/components/send/send-content/send-gas-row/index.js
diff --git a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js b/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js
index 17cea3d4e..91b58cfd0 100644
--- a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js
+++ b/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js
@@ -8,22 +8,32 @@ export default class SendGasRow extends Component {
static propTypes = {
conversionRate: PropTypes.number,
convertedCurrency: PropTypes.string,
+ gasFeeError: PropTypes.bool,
gasLoadingError: PropTypes.bool,
gasTotal: PropTypes.string,
showCustomizeGasModal: PropTypes.func,
};
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
render () {
const {
conversionRate,
convertedCurrency,
gasLoadingError,
gasTotal,
+ gasFeeError,
showCustomizeGasModal,
} = this.props
return (
- <SendRowWrapper label={`${this.context.t('gasFee')}:`}>
+ <SendRowWrapper
+ label={`${this.context.t('gasFee')}:`}
+ showError={gasFeeError}
+ errorType={'gasFee'}
+ >
<GasFeeDisplay
conversionRate={conversionRate}
convertedCurrency={convertedCurrency}
@@ -36,7 +46,3 @@ export default class SendGasRow extends Component {
}
}
-
-SendGasRow.contextTypes = {
- t: PropTypes.func,
-}
diff --git a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js b/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js
index 6e6fbc8a8..8f8e3e4dd 100644
--- a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js
+++ b/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js
@@ -4,7 +4,7 @@ import {
getCurrentCurrency,
getGasTotal,
} from '../../send.selectors.js'
-import { sendGasIsInError } from './send-gas-row.selectors.js'
+import { getGasLoadingError, gasFeeIsInError } from './send-gas-row.selectors.js'
import { showModal } from '../../../../actions'
import SendGasRow from './send-gas-row.component'
@@ -15,7 +15,8 @@ function mapStateToProps (state) {
conversionRate: getConversionRate(state),
convertedCurrency: getCurrentCurrency(state),
gasTotal: getGasTotal(state),
- gasLoadingError: sendGasIsInError(state),
+ gasFeeError: gasFeeIsInError(state),
+ gasLoadingError: getGasLoadingError(state),
}
}
diff --git a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.scss b/ui/app/components/send/send-content/send-gas-row/send-gas-row.scss
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.scss
+++ b/ui/app/components/send/send-content/send-gas-row/send-gas-row.scss
diff --git a/ui/app/components/send/send-content/send-gas-row/send-gas-row.selectors.js b/ui/app/components/send/send-content/send-gas-row/send-gas-row.selectors.js
new file mode 100644
index 000000000..96f6293c2
--- /dev/null
+++ b/ui/app/components/send/send-content/send-gas-row/send-gas-row.selectors.js
@@ -0,0 +1,14 @@
+const selectors = {
+ gasFeeIsInError,
+ getGasLoadingError,
+}
+
+module.exports = selectors
+
+function getGasLoadingError (state) {
+ return state.send.errors.gasLoading
+}
+
+function gasFeeIsInError (state) {
+ return Boolean(state.send.errors.gasFee)
+}
diff --git a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-component.test.js b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js
index db37f18be..54a92bd2d 100644
--- a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-component.test.js
+++ b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js
@@ -18,6 +18,7 @@ describe('SendGasRow Component', function () {
wrapper = shallow(<SendGasRow
conversionRate={20}
convertedCurrency={'mockConvertedCurrency'}
+ gasFeeError={'mockGasFeeError'}
gasLoadingError={false}
gasTotal={'mockGasTotal'}
showCustomizeGasModal={propsMethodSpies.showCustomizeGasModal}
@@ -36,9 +37,13 @@ describe('SendGasRow Component', function () {
it('should pass the correct props to SendRowWrapper', () => {
const {
label,
+ showError,
+ errorType,
} = wrapper.find(SendRowWrapper).props()
assert.equal(label, 'gasFee_t:')
+ assert.equal(showError, 'mockGasFeeError')
+ assert.equal(errorType, 'gasFee')
})
it('should render a GasFeeDisplay as a child of the SendRowWrapper', () => {
diff --git a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-container.test.js b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js
index e928c8aba..2ce062505 100644
--- a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-container.test.js
+++ b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js
@@ -22,7 +22,10 @@ proxyquire('../send-gas-row.container.js', {
getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`,
getGasTotal: (s) => `mockGasTotal:${s}`,
},
- './send-gas-row.selectors.js': { sendGasIsInError: (s) => `mockGasLoadingError:${s}` },
+ './send-gas-row.selectors.js': {
+ getGasLoadingError: (s) => `mockGasLoadingError:${s}`,
+ gasFeeIsInError: (s) => `mockGasFeeError:${s}`,
+ },
'../../../../actions': actionSpies,
})
@@ -35,6 +38,7 @@ describe('send-gas-row container', () => {
conversionRate: 'mockConversionRate:mockState',
convertedCurrency: 'mockConvertedCurrency:mockState',
gasTotal: 'mockGasTotal:mockState',
+ gasFeeError: 'mockGasFeeError:mockState',
gasLoadingError: 'mockGasLoadingError:mockState',
})
})
diff --git a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js
new file mode 100644
index 000000000..d46dd9d8b
--- /dev/null
+++ b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js
@@ -0,0 +1,49 @@
+import assert from 'assert'
+import {
+ gasFeeIsInError,
+ getGasLoadingError,
+} from '../send-gas-row.selectors.js'
+
+describe('send-gas-row selectors', () => {
+
+ describe('getGasLoadingError()', () => {
+ it('should return send.errors.gasLoading', () => {
+ const state = {
+ send: {
+ errors: {
+ gasLoading: 'abc',
+ },
+ },
+ }
+
+ assert.equal(getGasLoadingError(state), 'abc')
+ })
+ })
+
+ describe('gasFeeIsInError()', () => {
+ it('should return true if send.errors.gasFee is truthy', () => {
+ const state = {
+ send: {
+ errors: {
+ gasFee: 'def',
+ },
+ },
+ }
+
+ assert.equal(gasFeeIsInError(state), true)
+ })
+
+ it('should return false send.errors.gasFee is falsely', () => {
+ const state = {
+ send: {
+ errors: {
+ gasFee: null,
+ },
+ },
+ }
+
+ assert.equal(gasFeeIsInError(state), false)
+ })
+ })
+
+})
diff --git a/ui/app/components/send/send-content/send-hex-data-row/index.js b/ui/app/components/send/send-content/send-hex-data-row/index.js
new file mode 100644
index 000000000..08c341067
--- /dev/null
+++ b/ui/app/components/send/send-content/send-hex-data-row/index.js
@@ -0,0 +1 @@
+export { default } from './send-hex-data-row.container'
diff --git a/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js b/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js
new file mode 100644
index 000000000..063930db3
--- /dev/null
+++ b/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js
@@ -0,0 +1,40 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import SendRowWrapper from '../send-row-wrapper'
+
+export default class SendHexDataRow extends Component {
+ static propTypes = {
+ data: PropTypes.string,
+ inError: PropTypes.bool,
+ updateSendHexData: PropTypes.func.isRequired,
+ };
+
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
+ onInput = (event) => {
+ const {updateSendHexData} = this.props
+ event.target.value = event.target.value.replace(/\n/g, '')
+ updateSendHexData(event.target.value || null)
+ }
+
+ render () {
+ const {inError} = this.props
+ const {t} = this.context
+
+ return (
+ <SendRowWrapper
+ label={`${t('hexData')}:`}
+ showError={inError}
+ errorType={'amount'}
+ >
+ <textarea
+ onInput={this.onInput}
+ placeholder="Optional"
+ className="send-v2__hex-data__input"
+ />
+ </SendRowWrapper>
+ )
+ }
+}
diff --git a/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.container.js b/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.container.js
new file mode 100644
index 000000000..df554ca5f
--- /dev/null
+++ b/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.container.js
@@ -0,0 +1,21 @@
+import { connect } from 'react-redux'
+import {
+ updateSendHexData,
+} from '../../../../actions'
+import SendHexDataRow from './send-hex-data-row.component'
+
+export default connect(mapStateToProps, mapDispatchToProps)(SendHexDataRow)
+
+function mapStateToProps (state) {
+ return {
+ data: state.metamask.send.data,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ updateSendHexData (data) {
+ return dispatch(updateSendHexData(data))
+ },
+ }
+}
diff --git a/ui/app/components/send_/send-content/send-row-wrapper/index.js b/ui/app/components/send/send-content/send-row-wrapper/index.js
index d17545dcc..d17545dcc 100644
--- a/ui/app/components/send_/send-content/send-row-wrapper/index.js
+++ b/ui/app/components/send/send-content/send-row-wrapper/index.js
diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/index.js b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/index.js
index c00617f83..c00617f83 100644
--- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/index.js
+++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/index.js
diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md
+++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md
diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js
index 0d314208b..61bc7bab7 100644
--- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js
+++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js
@@ -8,6 +8,10 @@ export default class SendRowErrorMessage extends Component {
errorType: PropTypes.string,
};
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
render () {
const { errors, errorType } = this.props
@@ -21,7 +25,3 @@ export default class SendRowErrorMessage extends Component {
}
}
-
-SendRowErrorMessage.contextTypes = {
- t: PropTypes.func,
-}
diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js
index 59622047f..59622047f 100644
--- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js
+++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js
diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss
+++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss
diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js
index 2304a43d2..2304a43d2 100644
--- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js
+++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js
diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js
index eecff165d..eecff165d 100644
--- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js
+++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js
diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper-README.md b/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper-README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper-README.md
+++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper-README.md
diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.component.js b/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.component.js
index f484bd8d9..b7528a15f 100644
--- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.component.js
+++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.component.js
@@ -11,6 +11,10 @@ export default class SendRowWrapper extends Component {
showError: PropTypes.bool,
};
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
render () {
const {
children,
@@ -37,7 +41,3 @@ export default class SendRowWrapper extends Component {
}
}
-
-SendRowWrapper.contextTypes = {
- t: PropTypes.func,
-}
diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.scss b/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.scss
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.scss
+++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.scss
diff --git a/ui/app/components/send_/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js b/ui/app/components/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js
index 30280e1d0..30280e1d0 100644
--- a/ui/app/components/send_/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js
+++ b/ui/app/components/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js
diff --git a/ui/app/components/send_/send-content/send-to-row/index.js b/ui/app/components/send/send-content/send-to-row/index.js
index 121f15148..121f15148 100644
--- a/ui/app/components/send_/send-content/send-to-row/index.js
+++ b/ui/app/components/send/send-content/send-to-row/index.js
diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row-README.md b/ui/app/components/send/send-content/send-to-row/send-to-row-README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-content/send-to-row/send-to-row-README.md
+++ b/ui/app/components/send/send-content/send-to-row/send-to-row-README.md
diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row.component.js b/ui/app/components/send/send-content/send-to-row/send-to-row.component.js
index 1c2ecdf9c..892ad5d67 100644
--- a/ui/app/components/send_/send-content/send-to-row/send-to-row.component.js
+++ b/ui/app/components/send/send-content/send-to-row/send-to-row.component.js
@@ -19,6 +19,10 @@ export default class SendToRow extends Component {
updateSendToError: PropTypes.func,
};
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
handleToChange (to, nickname = '', toError) {
const { updateSendTo, updateSendToError, updateGas } = this.props
const toErrorObject = getToErrorObject(to, toError)
@@ -63,8 +67,3 @@ export default class SendToRow extends Component {
}
}
-
-SendToRow.contextTypes = {
- t: PropTypes.func,
-}
-
diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row.container.js b/ui/app/components/send/send-content/send-to-row/send-to-row.container.js
index 1c9c9d518..1c9c9d518 100644
--- a/ui/app/components/send_/send-content/send-to-row/send-to-row.container.js
+++ b/ui/app/components/send/send-content/send-to-row/send-to-row.container.js
diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row.selectors.js b/ui/app/components/send/send-content/send-to-row/send-to-row.selectors.js
index 8919014be..8919014be 100644
--- a/ui/app/components/send_/send-content/send-to-row/send-to-row.selectors.js
+++ b/ui/app/components/send/send-content/send-to-row/send-to-row.selectors.js
diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js b/ui/app/components/send/send-content/send-to-row/send-to-row.utils.js
index 6b90a9f09..6b90a9f09 100644
--- a/ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js
+++ b/ui/app/components/send/send-content/send-to-row/send-to-row.utils.js
diff --git a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-component.test.js b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js
index 781371004..781371004 100644
--- a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-component.test.js
+++ b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js
diff --git a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-container.test.js b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-container.test.js
index 92355c00a..92355c00a 100644
--- a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-container.test.js
+++ b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-container.test.js
diff --git a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-selectors.test.js b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-selectors.test.js
index 122ad3265..122ad3265 100644
--- a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-selectors.test.js
+++ b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-selectors.test.js
diff --git a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-utils.test.js b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-utils.test.js
index 4d2447c32..4d2447c32 100644
--- a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-utils.test.js
+++ b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-utils.test.js
diff --git a/ui/app/components/send_/send-content/tests/send-content-component.test.js b/ui/app/components/send/send-content/tests/send-content-component.test.js
index d5bb6693c..d5bb6693c 100644
--- a/ui/app/components/send_/send-content/tests/send-content-component.test.js
+++ b/ui/app/components/send/send-content/tests/send-content-component.test.js
diff --git a/ui/app/components/send_/send-footer/README.md b/ui/app/components/send/send-footer/README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-footer/README.md
+++ b/ui/app/components/send/send-footer/README.md
diff --git a/ui/app/components/send_/send-footer/index.js b/ui/app/components/send/send-footer/index.js
index 58e91d622..58e91d622 100644
--- a/ui/app/components/send_/send-footer/index.js
+++ b/ui/app/components/send/send-footer/index.js
diff --git a/ui/app/components/send_/send-footer/send-footer.component.js b/ui/app/components/send/send-footer/send-footer.component.js
index 6471ae1a3..518cff06e 100644
--- a/ui/app/components/send_/send-footer/send-footer.component.js
+++ b/ui/app/components/send/send-footer/send-footer.component.js
@@ -8,6 +8,7 @@ export default class SendFooter extends Component {
static propTypes = {
addToAddressBookIfNew: PropTypes.func,
amount: PropTypes.string,
+ data: PropTypes.string,
clearSend: PropTypes.func,
disabled: PropTypes.bool,
editingTransactionId: PropTypes.string,
@@ -27,6 +28,10 @@ export default class SendFooter extends Component {
update: PropTypes.func,
};
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
onCancel () {
this.props.clearSend()
this.props.history.push(DEFAULT_ROUTE)
@@ -37,6 +42,7 @@ export default class SendFooter extends Component {
const {
addToAddressBookIfNew,
amount,
+ data,
editingTransactionId,
from: {address: from},
gasLimit: gas,
@@ -48,6 +54,7 @@ export default class SendFooter extends Component {
// updateTx,
update,
toAccounts,
+ history,
} = this.props
// Should not be needed because submit should be disabled if there are errors.
@@ -60,9 +67,10 @@ export default class SendFooter extends Component {
// TODO: add nickname functionality
addToAddressBookIfNew(to, toAccounts)
- editingTransactionId
+ const promise = editingTransactionId
? update({
amount,
+ data,
editingTransactionId,
from,
gas,
@@ -71,9 +79,10 @@ export default class SendFooter extends Component {
to,
unapprovedTxs,
})
- : sign({ selectedToken, to, amount, from, gas, gasPrice })
+ : sign({ data, selectedToken, to, amount, from, gas, gasPrice })
- this.props.history.push(CONFIRM_TRANSACTION_ROUTE)
+ Promise.resolve(promise)
+ .then(() => history.push(CONFIRM_TRANSACTION_ROUTE))
}
formShouldBeDisabled () {
@@ -93,7 +102,3 @@ export default class SendFooter extends Component {
}
}
-
-SendFooter.contextTypes = {
- t: PropTypes.func,
-}
diff --git a/ui/app/components/send_/send-footer/send-footer.container.js b/ui/app/components/send/send-footer/send-footer.container.js
index 260ff40bc..60de4d030 100644
--- a/ui/app/components/send_/send-footer/send-footer.container.js
+++ b/ui/app/components/send/send-footer/send-footer.container.js
@@ -18,6 +18,7 @@ import {
getSendFromObject,
getSendTo,
getSendToAccounts,
+ getSendHexData,
getTokenBalance,
getUnapprovedTxs,
} from '../send.selectors'
@@ -35,6 +36,7 @@ export default connect(mapStateToProps, mapDispatchToProps)(SendFooter)
function mapStateToProps (state) {
return {
amount: getSendAmount(state),
+ data: getSendHexData(state),
editingTransactionId: getSendEditingTransactionId(state),
from: getSendFromObject(state),
gasLimit: getGasLimit(state),
@@ -52,9 +54,10 @@ function mapStateToProps (state) {
function mapDispatchToProps (dispatch) {
return {
clearSend: () => dispatch(clearSend()),
- sign: ({ selectedToken, to, amount, from, gas, gasPrice }) => {
+ sign: ({ selectedToken, to, amount, from, gas, gasPrice, data }) => {
const txParams = constructTxParams({
amount,
+ data,
from,
gas,
gasPrice,
@@ -68,6 +71,7 @@ function mapDispatchToProps (dispatch) {
},
update: ({
amount,
+ data,
editingTransactionId,
from,
gas,
@@ -78,6 +82,7 @@ function mapDispatchToProps (dispatch) {
}) => {
const editingTx = constructUpdatedTx({
amount,
+ data,
editingTransactionId,
from,
gas,
@@ -87,7 +92,7 @@ function mapDispatchToProps (dispatch) {
unapprovedTxs,
})
- dispatch(updateTransaction(editingTx))
+ return dispatch(updateTransaction(editingTx))
},
addToAddressBookIfNew: (newAddress, toAccounts, nickname = '') => {
const hexPrefixedAddress = ethUtil.addHexPrefix(newAddress)
diff --git a/ui/app/components/send_/send-footer/send-footer.scss b/ui/app/components/send/send-footer/send-footer.scss
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-footer/send-footer.scss
+++ b/ui/app/components/send/send-footer/send-footer.scss
diff --git a/ui/app/components/send_/send-footer/send-footer.selectors.js b/ui/app/components/send/send-footer/send-footer.selectors.js
index e20addfdc..e20addfdc 100644
--- a/ui/app/components/send_/send-footer/send-footer.selectors.js
+++ b/ui/app/components/send/send-footer/send-footer.selectors.js
diff --git a/ui/app/components/send_/send-footer/send-footer.utils.js b/ui/app/components/send/send-footer/send-footer.utils.js
index 875e7d948..f82ff1e9b 100644
--- a/ui/app/components/send_/send-footer/send-footer.utils.js
+++ b/ui/app/components/send/send-footer/send-footer.utils.js
@@ -8,8 +8,9 @@ function addHexPrefixToObjectValues (obj) {
}, {})
}
-function constructTxParams ({ selectedToken, to, amount, from, gas, gasPrice }) {
+function constructTxParams ({ selectedToken, data, to, amount, from, gas, gasPrice }) {
const txParams = {
+ data,
from,
value: '0',
gas,
@@ -21,13 +22,12 @@ function constructTxParams ({ selectedToken, to, amount, from, gas, gasPrice })
txParams.to = to
}
- const hexPrefixedTxParams = addHexPrefixToObjectValues(txParams)
-
- return hexPrefixedTxParams
+ return addHexPrefixToObjectValues(txParams)
}
function constructUpdatedTx ({
amount,
+ data,
editingTransactionId,
from,
gas,
@@ -36,9 +36,21 @@ function constructUpdatedTx ({
to,
unapprovedTxs,
}) {
+ const unapprovedTx = unapprovedTxs[editingTransactionId]
+ const txParamsData = unapprovedTx.txParams.data ? unapprovedTx.txParams.data : data
const editingTx = {
- ...unapprovedTxs[editingTransactionId],
- txParams: addHexPrefixToObjectValues({ from, gas, gasPrice }),
+ ...unapprovedTx,
+ txParams: Object.assign(
+ unapprovedTx.txParams,
+ addHexPrefixToObjectValues({
+ data: txParamsData,
+ to,
+ from,
+ gas,
+ gasPrice,
+ value: amount,
+ })
+ ),
}
if (selectedToken) {
@@ -52,18 +64,10 @@ function constructUpdatedTx ({
to: selectedToken.address,
data,
}))
- } else {
- const { data } = unapprovedTxs[editingTransactionId].txParams
-
- Object.assign(editingTx.txParams, addHexPrefixToObjectValues({
- value: amount,
- to,
- data,
- }))
+ }
- if (typeof editingTx.txParams.data === 'undefined') {
- delete editingTx.txParams.data
- }
+ if (typeof editingTx.txParams.data === 'undefined') {
+ delete editingTx.txParams.data
}
return editingTx
diff --git a/ui/app/components/send_/send-footer/tests/send-footer-component.test.js b/ui/app/components/send/send-footer/tests/send-footer-component.test.js
index e071fe54f..65e4bb654 100644
--- a/ui/app/components/send_/send-footer/tests/send-footer-component.test.js
+++ b/ui/app/components/send/send-footer/tests/send-footer-component.test.js
@@ -129,6 +129,7 @@ describe('SendFooter Component', function () {
assert.deepEqual(
propsMethodSpies.update.getCall(0).args[0],
{
+ data: undefined,
amount: 'mockAmount',
editingTransactionId: 'mockEditingTransactionId',
from: 'mockAddress',
@@ -152,6 +153,7 @@ describe('SendFooter Component', function () {
assert.deepEqual(
propsMethodSpies.sign.getCall(0).args[0],
{
+ data: undefined,
amount: 'mockAmount',
from: 'mockAddress',
gas: 'mockGasLimit',
@@ -166,10 +168,13 @@ describe('SendFooter Component', function () {
assert.equal(propsMethodSpies.update.callCount, 0)
})
- it('should call history.push', () => {
- wrapper.instance().onSubmit(MOCK_EVENT)
- assert.equal(historySpies.push.callCount, 1)
- assert.equal(historySpies.push.getCall(0).args[0], CONFIRM_TRANSACTION_ROUTE)
+ it('should call history.push', done => {
+ Promise.resolve(wrapper.instance().onSubmit(MOCK_EVENT))
+ .then(() => {
+ assert.equal(historySpies.push.callCount, 1)
+ assert.equal(historySpies.push.getCall(0).args[0], CONFIRM_TRANSACTION_ROUTE)
+ done()
+ })
})
})
diff --git a/ui/app/components/send_/send-footer/tests/send-footer-container.test.js b/ui/app/components/send/send-footer/tests/send-footer-container.test.js
index 39d6a7686..cf4c893ee 100644
--- a/ui/app/components/send_/send-footer/tests/send-footer-container.test.js
+++ b/ui/app/components/send/send-footer/tests/send-footer-container.test.js
@@ -38,6 +38,7 @@ proxyquire('../send-footer.container.js', {
getSendTo: (s) => `mockTo:${s}`,
getSendToAccounts: (s) => `mockToAccounts:${s}`,
getTokenBalance: (s) => `mockTokenBalance:${s}`,
+ getSendHexData: (s) => `mockHexData:${s}`,
getUnapprovedTxs: (s) => `mockUnapprovedTxs:${s}`,
},
'./send-footer.selectors': { isSendFormInError: (s) => `mockInError:${s}` },
@@ -51,6 +52,7 @@ describe('send-footer container', () => {
it('should map the correct properties to props', () => {
assert.deepEqual(mapStateToProps('mockState'), {
amount: 'mockAmount:mockState',
+ data: 'mockHexData:mockState',
selectedToken: 'mockSelectedToken:mockState',
editingTransactionId: 'mockEditingTransactionId:mockState',
from: 'mockFromObject:mockState',
@@ -100,6 +102,7 @@ describe('send-footer container', () => {
assert.deepEqual(
utilsStubs.constructTxParams.getCall(0).args[0],
{
+ data: undefined,
selectedToken: {
address: '0xabc',
},
@@ -129,6 +132,7 @@ describe('send-footer container', () => {
assert.deepEqual(
utilsStubs.constructTxParams.getCall(0).args[0],
{
+ data: undefined,
selectedToken: undefined,
to: 'mockTo',
amount: 'mockAmount',
@@ -160,6 +164,7 @@ describe('send-footer container', () => {
assert.deepEqual(
utilsStubs.constructUpdatedTx.getCall(0).args[0],
{
+ data: undefined,
to: 'mockTo',
amount: 'mockAmount',
from: 'mockFrom',
diff --git a/ui/app/components/send_/send-footer/tests/send-footer-selectors.test.js b/ui/app/components/send/send-footer/tests/send-footer-selectors.test.js
index 8de032f57..8de032f57 100644
--- a/ui/app/components/send_/send-footer/tests/send-footer-selectors.test.js
+++ b/ui/app/components/send/send-footer/tests/send-footer-selectors.test.js
diff --git a/ui/app/components/send_/send-footer/tests/send-footer-utils.test.js b/ui/app/components/send/send-footer/tests/send-footer-utils.test.js
index 2d3135995..28ff0c891 100644
--- a/ui/app/components/send_/send-footer/tests/send-footer-utils.test.js
+++ b/ui/app/components/send/send-footer/tests/send-footer-utils.test.js
@@ -65,6 +65,28 @@ describe('send-footer utils', () => {
})
describe('constructTxParams()', () => {
+ it('should return a new txParams object with data if there data is given', () => {
+ assert.deepEqual(
+ constructTxParams({
+ data: 'someData',
+ selectedToken: false,
+ to: 'mockTo',
+ amount: 'mockAmount',
+ from: 'mockFrom',
+ gas: 'mockGas',
+ gasPrice: 'mockGasPrice',
+ }),
+ {
+ data: '0xsomeData',
+ to: '0xmockTo',
+ value: '0xmockAmount',
+ from: '0xmockFrom',
+ gas: '0xmockGas',
+ gasPrice: '0xmockGasPrice',
+ }
+ )
+ })
+
it('should return a new txParams object with value and to properties if there is no selectedToken', () => {
assert.deepEqual(
constructTxParams({
@@ -76,6 +98,7 @@ describe('send-footer utils', () => {
gasPrice: 'mockGasPrice',
}),
{
+ data: undefined,
to: '0xmockTo',
value: '0xmockAmount',
from: '0xmockFrom',
@@ -96,6 +119,7 @@ describe('send-footer utils', () => {
gasPrice: 'mockGasPrice',
}),
{
+ data: undefined,
value: '0x0',
from: '0xmockFrom',
gas: '0xmockGas',
diff --git a/ui/app/components/send_/send-header/README.md b/ui/app/components/send/send-header/README.md
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send-header/README.md
+++ b/ui/app/components/send/send-header/README.md
diff --git a/ui/app/components/send_/send-header/index.js b/ui/app/components/send/send-header/index.js
index 0b17f0b7d..0b17f0b7d 100644
--- a/ui/app/components/send_/send-header/index.js
+++ b/ui/app/components/send/send-header/index.js
diff --git a/ui/app/components/send_/send-header/send-header.component.js b/ui/app/components/send/send-header/send-header.component.js
index 5f6617fce..efc4bbf27 100644
--- a/ui/app/components/send_/send-header/send-header.component.js
+++ b/ui/app/components/send/send-header/send-header.component.js
@@ -12,6 +12,10 @@ export default class SendHeader extends Component {
subtitleParams: PropTypes.array,
};
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
onClose () {
this.props.clearSend()
this.props.history.push(DEFAULT_ROUTE)
@@ -28,7 +32,3 @@ export default class SendHeader extends Component {
}
}
-
-SendHeader.contextTypes = {
- t: PropTypes.func,
-}
diff --git a/ui/app/components/send_/send-header/send-header.container.js b/ui/app/components/send/send-header/send-header.container.js
index 4bcd0d1b6..4bcd0d1b6 100644
--- a/ui/app/components/send_/send-header/send-header.container.js
+++ b/ui/app/components/send/send-header/send-header.container.js
diff --git a/ui/app/components/send_/send-header/send-header.selectors.js b/ui/app/components/send/send-header/send-header.selectors.js
index d7c9d3766..d7c9d3766 100644
--- a/ui/app/components/send_/send-header/send-header.selectors.js
+++ b/ui/app/components/send/send-header/send-header.selectors.js
diff --git a/ui/app/components/send_/send-header/tests/send-header-component.test.js b/ui/app/components/send/send-header/tests/send-header-component.test.js
index 930bfa387..930bfa387 100644
--- a/ui/app/components/send_/send-header/tests/send-header-component.test.js
+++ b/ui/app/components/send/send-header/tests/send-header-component.test.js
diff --git a/ui/app/components/send_/send-header/tests/send-header-container.test.js b/ui/app/components/send/send-header/tests/send-header-container.test.js
index 41a7e8a89..41a7e8a89 100644
--- a/ui/app/components/send_/send-header/tests/send-header-container.test.js
+++ b/ui/app/components/send/send-header/tests/send-header-container.test.js
diff --git a/ui/app/components/send_/send-header/tests/send-header-selectors.test.js b/ui/app/components/send/send-header/tests/send-header-selectors.test.js
index e0c6a3ab3..e0c6a3ab3 100644
--- a/ui/app/components/send_/send-header/tests/send-header-selectors.test.js
+++ b/ui/app/components/send/send-header/tests/send-header-selectors.test.js
diff --git a/ui/app/components/send_/send.component.js b/ui/app/components/send/send.component.js
index 219b362f2..6f1b20c55 100644
--- a/ui/app/components/send_/send.component.js
+++ b/ui/app/components/send/send.component.js
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
import PersistentForm from '../../../lib/persistent-form'
import {
getAmountErrorObject,
+ getGasFeeErrorObject,
getToAddressForGasUpdate,
doesAmountErrorRequireUpdate,
} from './send.utils'
@@ -39,6 +40,10 @@ export default class SendTransactionScreen extends PersistentForm {
updateSendTokenBalance: PropTypes.func,
};
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
updateGas ({ to: updatedToAddress, amount: value } = {}) {
const {
amount,
@@ -112,7 +117,19 @@ export default class SendTransactionScreen extends PersistentForm {
selectedToken,
tokenBalance,
})
- updateSendErrors(amountErrorObject)
+ const gasFeeErrorObject = selectedToken
+ ? getGasFeeErrorObject({
+ amount,
+ amountConversionRate,
+ balance,
+ conversionRate,
+ gasTotal,
+ primaryCurrency,
+ selectedToken,
+ tokenBalance,
+ })
+ : { gasFee: null }
+ updateSendErrors(Object.assign(amountErrorObject, gasFeeErrorObject))
}
if (!uninitialized) {
@@ -143,6 +160,10 @@ export default class SendTransactionScreen extends PersistentForm {
this.updateGas()
}
+ componentWillUnmount () {
+ this.props.resetSendState()
+ }
+
render () {
const { history } = this.props
@@ -156,7 +177,3 @@ export default class SendTransactionScreen extends PersistentForm {
}
}
-
-SendTransactionScreen.contextTypes = {
- t: PropTypes.func,
-}
diff --git a/ui/app/components/send_/send.constants.js b/ui/app/components/send/send.constants.js
index 8acdf0641..8acdf0641 100644
--- a/ui/app/components/send_/send.constants.js
+++ b/ui/app/components/send/send.constants.js
diff --git a/ui/app/components/send_/send.container.js b/ui/app/components/send/send.container.js
index 185653c5f..44ebd2792 100644
--- a/ui/app/components/send_/send.container.js
+++ b/ui/app/components/send/send.container.js
@@ -28,6 +28,7 @@ import {
setGasTotal,
} from '../../actions'
import {
+ resetSendState,
updateSendErrors,
} from '../../ducks/send.duck'
import {
@@ -87,5 +88,6 @@ function mapDispatchToProps (dispatch) {
}))
},
updateSendErrors: newError => dispatch(updateSendErrors(newError)),
+ resetSendState: () => dispatch(resetSendState()),
}
}
diff --git a/ui/app/components/send_/send.scss b/ui/app/components/send/send.scss
index e69de29bb..e69de29bb 100644
--- a/ui/app/components/send_/send.scss
+++ b/ui/app/components/send/send.scss
diff --git a/ui/app/components/send_/send.selectors.js b/ui/app/components/send/send.selectors.js
index f910f7caf..cf07eafe1 100644
--- a/ui/app/components/send_/send.selectors.js
+++ b/ui/app/components/send/send.selectors.js
@@ -33,6 +33,7 @@ const selectors = {
getSelectedTokenExchangeRate,
getSelectedTokenToFiatRate,
getSendAmount,
+ getSendHexData,
getSendEditingTransactionId,
getSendErrors,
getSendFrom,
@@ -210,6 +211,10 @@ function getSendAmount (state) {
return state.metamask.send.amount
}
+function getSendHexData (state) {
+ return state.metamask.send.data
+}
+
function getSendEditingTransactionId (state) {
return state.metamask.send.editingTransactionId
}
diff --git a/ui/app/components/send_/send.utils.js b/ui/app/components/send/send.utils.js
index 34275248f..aa255c3d4 100644
--- a/ui/app/components/send_/send.utils.js
+++ b/ui/app/components/send/send.utils.js
@@ -30,13 +30,14 @@ module.exports = {
estimateGasPriceFromRecentBlocks,
generateTokenTransferData,
getAmountErrorObject,
+ getGasFeeErrorObject,
getToAddressForGasUpdate,
isBalanceSufficient,
isTokenBalanceSufficient,
removeLeadingZeroes,
}
-function calcGasTotal (gasLimit, gasPrice) {
+function calcGasTotal (gasLimit = '0', gasPrice = '0') {
return multiplyCurrencies(gasLimit, gasPrice, {
toNumericBase: 'hex',
multiplicandBase: 16,
@@ -46,9 +47,9 @@ function calcGasTotal (gasLimit, gasPrice) {
function isBalanceSufficient ({
amount = '0x0',
- amountConversionRate = 0,
- balance,
- conversionRate,
+ amountConversionRate = 1,
+ balance = '0x0',
+ conversionRate = 1,
gasTotal = '0x0',
primaryCurrency,
}) {
@@ -110,9 +111,9 @@ function getAmountErrorObject ({
tokenBalance,
}) {
let insufficientFunds = false
- if (gasTotal && conversionRate) {
+ if (gasTotal && conversionRate && !selectedToken) {
insufficientFunds = !isBalanceSufficient({
- amount: selectedToken ? '0x0' : amount,
+ amount,
amountConversionRate,
balance,
conversionRate,
@@ -149,6 +150,34 @@ function getAmountErrorObject ({
return { amount: amountError }
}
+function getGasFeeErrorObject ({
+ amount,
+ amountConversionRate,
+ balance,
+ conversionRate,
+ gasTotal,
+ primaryCurrency,
+}) {
+ let gasFeeError = null
+
+ if (gasTotal && conversionRate) {
+ const insufficientFunds = !isBalanceSufficient({
+ amount: '0x0',
+ amountConversionRate,
+ balance,
+ conversionRate,
+ gasTotal,
+ primaryCurrency,
+ })
+
+ if (insufficientFunds) {
+ gasFeeError = INSUFFICIENT_FUNDS_ERROR
+ }
+ }
+
+ return { gasFee: gasFeeError }
+}
+
function calcTokenBalance ({ selectedToken, usersToken }) {
const { decimals } = selectedToken || {}
return calcTokenAmount(usersToken.balance.toString(), decimals) + ''
diff --git a/ui/app/components/send_/tests/send-component.test.js b/ui/app/components/send/tests/send-component.test.js
index 4ba9b226d..6194ec508 100644
--- a/ui/app/components/send_/tests/send-component.test.js
+++ b/ui/app/components/send/tests/send-component.test.js
@@ -12,9 +12,11 @@ const propsMethodSpies = {
updateAndSetGasTotal: sinon.spy(),
updateSendErrors: sinon.spy(),
updateSendTokenBalance: sinon.spy(),
+ resetSendState: sinon.spy(),
}
const utilsMethodStubs = {
getAmountErrorObject: sinon.stub().returns({ amount: 'mockAmountError' }),
+ getGasFeeErrorObject: sinon.stub().returns({ gasFee: 'mockGasFeeError' }),
doesAmountErrorRequireUpdate: sinon.stub().callsFake(obj => obj.balance !== obj.prevBalance),
}
@@ -50,6 +52,7 @@ describe('Send Component', function () {
updateAndSetGasTotal={propsMethodSpies.updateAndSetGasTotal}
updateSendErrors={propsMethodSpies.updateSendErrors}
updateSendTokenBalance={propsMethodSpies.updateSendTokenBalance}
+ resetSendState={propsMethodSpies.resetSendState}
/>)
})
@@ -58,6 +61,7 @@ describe('Send Component', function () {
SendTransactionScreen.prototype.updateGas.resetHistory()
utilsMethodStubs.doesAmountErrorRequireUpdate.resetHistory()
utilsMethodStubs.getAmountErrorObject.resetHistory()
+ utilsMethodStubs.getGasFeeErrorObject.resetHistory()
propsMethodSpies.updateAndSetGasTotal.resetHistory()
propsMethodSpies.updateSendErrors.resetHistory()
propsMethodSpies.updateSendTokenBalance.resetHistory()
@@ -77,6 +81,15 @@ describe('Send Component', function () {
})
})
+ describe('componentWillUnmount', () => {
+ it('should call this.props.resetSendState', () => {
+ propsMethodSpies.resetSendState.resetHistory()
+ assert.equal(propsMethodSpies.resetSendState.callCount, 0)
+ wrapper.instance().componentWillUnmount()
+ assert.equal(propsMethodSpies.resetSendState.callCount, 1)
+ })
+ })
+
describe('componentDidUpdate', () => {
it('should call doesAmountErrorRequireUpdate with the expected params', () => {
utilsMethodStubs.getAmountErrorObject.resetHistory()
@@ -133,8 +146,66 @@ describe('Send Component', function () {
)
})
- it('should call updateSendErrors with the expected params', () => {
+ it('should call getGasFeeErrorObject if doesAmountErrorRequireUpdate returns true and selectedToken is truthy', () => {
+ utilsMethodStubs.getGasFeeErrorObject.resetHistory()
+ wrapper.instance().componentDidUpdate({
+ from: {
+ balance: 'balanceChanged',
+ },
+ })
+ assert.equal(utilsMethodStubs.getGasFeeErrorObject.callCount, 1)
+ assert.deepEqual(
+ utilsMethodStubs.getGasFeeErrorObject.getCall(0).args[0],
+ {
+ amount: 'mockAmount',
+ amountConversionRate: 'mockAmountConversionRate',
+ balance: 'mockBalance',
+ conversionRate: 10,
+ gasTotal: 'mockGasTotal',
+ primaryCurrency: 'mockPrimaryCurrency',
+ selectedToken: 'mockSelectedToken',
+ tokenBalance: 'mockTokenBalance',
+ }
+ )
+ })
+
+ it('should not call getGasFeeErrorObject if doesAmountErrorRequireUpdate returns false', () => {
+ utilsMethodStubs.getGasFeeErrorObject.resetHistory()
+ wrapper.instance().componentDidUpdate({
+ from: { address: 'mockAddress', balance: 'mockBalance' },
+ })
+ assert.equal(utilsMethodStubs.getGasFeeErrorObject.callCount, 0)
+ })
+
+ it('should not call getGasFeeErrorObject if doesAmountErrorRequireUpdate returns true but selectedToken is falsy', () => {
+ utilsMethodStubs.getGasFeeErrorObject.resetHistory()
+ wrapper.setProps({ selectedToken: null })
+ wrapper.instance().componentDidUpdate({
+ from: {
+ balance: 'balanceChanged',
+ },
+ })
+ assert.equal(utilsMethodStubs.getGasFeeErrorObject.callCount, 0)
+ })
+
+ it('should call updateSendErrors with the expected params if selectedToken is falsy', () => {
+ propsMethodSpies.updateSendErrors.resetHistory()
+ wrapper.setProps({ selectedToken: null })
+ wrapper.instance().componentDidUpdate({
+ from: {
+ balance: 'balanceChanged',
+ },
+ })
+ assert.equal(propsMethodSpies.updateSendErrors.callCount, 1)
+ assert.deepEqual(
+ propsMethodSpies.updateSendErrors.getCall(0).args[0],
+ { amount: 'mockAmountError', gasFee: null }
+ )
+ })
+
+ it('should call updateSendErrors with the expected params if selectedToken is truthy', () => {
propsMethodSpies.updateSendErrors.resetHistory()
+ wrapper.setProps({ selectedToken: 'someToken' })
wrapper.instance().componentDidUpdate({
from: {
balance: 'balanceChanged',
@@ -143,7 +214,7 @@ describe('Send Component', function () {
assert.equal(propsMethodSpies.updateSendErrors.callCount, 1)
assert.deepEqual(
propsMethodSpies.updateSendErrors.getCall(0).args[0],
- { amount: 'mockAmountError'}
+ { amount: 'mockAmountError', gasFee: 'mockGasFeeError' }
)
})
diff --git a/ui/app/components/send_/tests/send-container.test.js b/ui/app/components/send/tests/send-container.test.js
index 91484f4d8..7a9120d24 100644
--- a/ui/app/components/send_/tests/send-container.test.js
+++ b/ui/app/components/send/tests/send-container.test.js
@@ -12,6 +12,7 @@ const actionSpies = {
}
const duckActionSpies = {
updateSendErrors: sinon.spy(),
+ resetSendState: sinon.spy(),
}
proxyquire('../send.container.js', {
@@ -152,6 +153,17 @@ describe('send container', () => {
})
})
+ describe('resetSendState()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.resetSendState()
+ assert(dispatchSpy.calledOnce)
+ assert.equal(
+ duckActionSpies.resetSendState.getCall(0).args.length,
+ 0
+ )
+ })
+ })
+
})
})
diff --git a/ui/app/components/send_/tests/send-selectors-test-data.js b/ui/app/components/send/tests/send-selectors-test-data.js
index 8f9c19314..8f9c19314 100644
--- a/ui/app/components/send_/tests/send-selectors-test-data.js
+++ b/ui/app/components/send/tests/send-selectors-test-data.js
diff --git a/ui/app/components/send_/tests/send-selectors.test.js b/ui/app/components/send/tests/send-selectors.test.js
index 218da656b..218da656b 100644
--- a/ui/app/components/send_/tests/send-selectors.test.js
+++ b/ui/app/components/send/tests/send-selectors.test.js
diff --git a/ui/app/components/send_/tests/send-utils.test.js b/ui/app/components/send/tests/send-utils.test.js
index a518a64e9..18dde495a 100644
--- a/ui/app/components/send_/tests/send-utils.test.js
+++ b/ui/app/components/send/tests/send-utils.test.js
@@ -17,7 +17,11 @@ const {
} = require('../send.constants')
const stubs = {
- addCurrencies: sinon.stub().callsFake((a, b, obj) => a + b),
+ addCurrencies: sinon.stub().callsFake((a, b, obj) => {
+ if (String(a).match(/^0x.+/)) a = Number(String(a).slice(2))
+ if (String(b).match(/^0x.+/)) b = Number(String(b).slice(2))
+ return a + b
+ }),
conversionUtil: sinon.stub().callsFake((val, obj) => parseInt(val, 16)),
conversionGTE: sinon.stub().callsFake((obj1, obj2) => obj1.value >= obj2.value),
multiplyCurrencies: sinon.stub().callsFake((a, b) => `${a}x${b}`),
@@ -49,10 +53,12 @@ const {
estimateGasPriceFromRecentBlocks,
generateTokenTransferData,
getAmountErrorObject,
+ getGasFeeErrorObject,
getToAddressForGasUpdate,
calcTokenBalance,
isBalanceSufficient,
isTokenBalanceSufficient,
+ removeLeadingZeroes,
} = sendUtils
describe('send utils', () => {
@@ -143,6 +149,18 @@ describe('send utils', () => {
primaryCurrency: 'ABC',
expectedResult: { amount: INSUFFICIENT_FUNDS_ERROR },
},
+ 'should not return insufficientFunds error if selectedToken is truthy': {
+ amount: '0x0',
+ amountConversionRate: 2,
+ balance: 1,
+ conversionRate: 3,
+ gasTotal: 17,
+ primaryCurrency: 'ABC',
+ selectedToken: { symbole: 'DEF', decimals: 0 },
+ decimals: 0,
+ tokenBalance: 'sometokenbalance',
+ expectedResult: { amount: null },
+ },
'should return insufficientTokens error if token is selected and isTokenBalanceSufficient returns false': {
amount: '0x10',
amountConversionRate: 2,
@@ -163,6 +181,32 @@ describe('send utils', () => {
})
})
+ describe('getGasFeeErrorObject()', () => {
+ const config = {
+ 'should return insufficientFunds error if isBalanceSufficient returns false': {
+ amountConversionRate: 2,
+ balance: 16,
+ conversionRate: 3,
+ gasTotal: 17,
+ primaryCurrency: 'ABC',
+ expectedResult: { gasFee: INSUFFICIENT_FUNDS_ERROR },
+ },
+ 'should return null error if isBalanceSufficient returns true': {
+ amountConversionRate: 2,
+ balance: 16,
+ conversionRate: 3,
+ gasTotal: 15,
+ primaryCurrency: 'ABC',
+ expectedResult: { gasFee: null },
+ },
+ }
+ Object.entries(config).map(([description, obj]) => {
+ it(description, () => {
+ assert.deepEqual(getGasFeeErrorObject(obj), obj.expectedResult)
+ })
+ })
+ })
+
describe('calcTokenBalance()', () => {
it('should return the calculated token blance', () => {
assert.equal(calcTokenBalance({
@@ -222,6 +266,7 @@ describe('send utils', () => {
describe('isTokenBalanceSufficient()', () => {
it('should correctly call conversionUtil and return the result of calling conversionGTE', () => {
stubs.conversionGTE.resetHistory()
+ stubs.conversionUtil.resetHistory()
const result = isTokenBalanceSufficient({
amount: '0x10',
tokenBalance: 123,
@@ -439,4 +484,29 @@ describe('send utils', () => {
assert.equal(getToAddressForGasUpdate(undefined, 'B'), 'b')
})
})
+
+ describe('removeLeadingZeroes()', () => {
+ it('should remove leading zeroes from int when user types', () => {
+ assert.equal(removeLeadingZeroes('0'), '0')
+ assert.equal(removeLeadingZeroes('1'), '1')
+ assert.equal(removeLeadingZeroes('00'), '0')
+ assert.equal(removeLeadingZeroes('01'), '1')
+ })
+
+ it('should remove leading zeroes from int when user copy/paste', () => {
+ assert.equal(removeLeadingZeroes('001'), '1')
+ })
+
+ it('should remove leading zeroes from float when user types', () => {
+ assert.equal(removeLeadingZeroes('0.'), '0.')
+ assert.equal(removeLeadingZeroes('0.0'), '0.0')
+ assert.equal(removeLeadingZeroes('0.00'), '0.00')
+ assert.equal(removeLeadingZeroes('0.001'), '0.001')
+ assert.equal(removeLeadingZeroes('0.10'), '0.10')
+ })
+
+ it('should remove leading zeroes from float when user copy/paste', () => {
+ assert.equal(removeLeadingZeroes('00.1'), '0.1')
+ })
+ })
})
diff --git a/ui/app/components/send/to-autocomplete.component.js b/ui/app/components/send/to-autocomplete.component.js
new file mode 100644
index 000000000..9e270db75
--- /dev/null
+++ b/ui/app/components/send/to-autocomplete.component.js
@@ -0,0 +1,141 @@
+import React, {Component} from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import AccountListItem from '../send/account-list-item/account-list-item.component'
+
+
+export default class ToAutoComplete extends Component {
+
+ static propTypes = {
+ dropdownOpen: PropTypes.bool,
+ openDropdown: PropTypes.func,
+ closeDropdown: PropTypes.func,
+ onChange: PropTypes.func,
+ to: PropTypes.string,
+ accounts: PropTypes.array,
+ inError: PropTypes.bool,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ state = {
+ accountsToRender: [],
+ }
+
+ getListItemIcon (listItemAddress, toAddress) {
+ return toAddress && listItemAddress === toAddress
+ ? <i className={'fa fa-check fa-lg'}
+ style={{
+ color: '#02c9b1',
+ }}
+ />
+ : null
+ }
+
+ renderDropdown () {
+ const {
+ closeDropdown,
+ onChange,
+ to,
+ } = this.props
+ const {accountsToRender} = this.state
+
+ if (!accountsToRender.length) {
+ return null
+ }
+
+ return (
+ <div>
+ <div className={'send-v2__from-dropdown__close-area'} onClick={closeDropdown} />
+ <div className={'send-v2__from-dropdown__list'}>
+ {accountsToRender.map((account, i) => (
+ <AccountListItem
+ key={i}
+ account={account}
+ className={'account-list-item__dropdown'}
+ handleClick={() => {
+ onChange(account.address)
+ closeDropdown()
+ }}
+ icon={this.getListItemIcon(account.address, to)}
+ displayBalance={false}
+ displayAddress={true}
+ />
+ ))}
+ </div>
+ </div>
+ )
+ }
+
+ handleInputEvent (event = {}, cb) {
+ const {
+ to,
+ accounts,
+ closeDropdown,
+ openDropdown,
+ } = this.props
+
+ const matchingAccounts = accounts.filter(({address}) => address.match(to || ''))
+ const matches = matchingAccounts.length
+
+ if (!matches || matchingAccounts[0].address === to) {
+ this.setState({accountsToRender: []})
+ event.target && event.target.select()
+ closeDropdown()
+ } else {
+ this.setState({accountsToRender: matchingAccounts})
+ openDropdown()
+ }
+ cb && cb(event.target.value)
+ }
+
+ componentDidUpdate (nextProps) {
+ if (this.props.to !== nextProps.to) {
+ this.handleInputEvent()
+ }
+ }
+
+ render () {
+ const {
+ to,
+ dropdownOpen,
+ onChange,
+ inError,
+ } = this.props
+
+ return (
+ <div className={'send-v2__to-autocomplete'}>
+ <input
+ className={classnames('send-v2__to-autocomplete__input', {
+ 'send-v2__error-border': inError,
+ })}
+ placeholder={this.context.t('recipientAddress')}
+ value={to}
+ onChange={event => onChange(event.target.value)}
+ onFocus={event => this.handleInputEvent(event)}
+ style={{
+ borderColor: inError ? 'red' : null,
+ }}
+ />
+ {
+ to
+ ? null
+ : <i className={'fa fa-caret-down fa-lg send-v2__to-autocomplete__down-caret'}
+ onClick={() => this.handleInputEvent()}
+ style={{
+ style: {color: '#dedede'},
+ }}
+ />
+ }
+ {
+ dropdownOpen
+ ? this.renderDropdown()
+ : null
+ }
+ </div>
+ )
+ }
+
+}
diff --git a/ui/app/components/send/to-autocomplete/index.js b/ui/app/components/send/to-autocomplete/index.js
new file mode 100644
index 000000000..244d301d1
--- /dev/null
+++ b/ui/app/components/send/to-autocomplete/index.js
@@ -0,0 +1 @@
+export { default } from './to-autocomplete.js'
diff --git a/ui/app/components/send/to-autocomplete.js b/ui/app/components/send/to-autocomplete/to-autocomplete.js
index df74ef194..80cfa7a85 100644
--- a/ui/app/components/send/to-autocomplete.js
+++ b/ui/app/components/send/to-autocomplete/to-autocomplete.js
@@ -2,7 +2,7 @@ const Component = require('react').Component
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const inherits = require('util').inherits
-const AccountListItem = require('../send_/account-list-item/account-list-item.component').default
+const AccountListItem = require('../account-list-item/account-list-item.component').default
const connect = require('react-redux').connect
ToAutoComplete.contextTypes = {
diff --git a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.selectors.js b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.selectors.js
deleted file mode 100644
index d069ae8c6..000000000
--- a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.selectors.js
+++ /dev/null
@@ -1,9 +0,0 @@
-const selectors = {
- sendGasIsInError,
-}
-
-module.exports = selectors
-
-function sendGasIsInError (state) {
- return state.send.errors.gasLoading
-}
diff --git a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-selectors.test.js b/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-selectors.test.js
deleted file mode 100644
index a5196334e..000000000
--- a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-selectors.test.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import assert from 'assert'
-import {
- sendGasIsInError,
-} from '../send-gas-row.selectors.js'
-
-describe('send-gas-row selectors', () => {
-
- describe('sendGasIsInError()', () => {
- it('should return send.errors.gasLoading', () => {
- const state = {
- send: {
- errors: {
- gasLoading: 'abc',
- },
- },
- }
-
- assert.equal(sendGasIsInError(state), 'abc')
- })
- })
-
-})
diff --git a/ui/app/components/send_/send.utils.test.js b/ui/app/components/send_/send.utils.test.js
deleted file mode 100644
index 36f3a5c10..000000000
--- a/ui/app/components/send_/send.utils.test.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import assert from 'assert'
-import { removeLeadingZeroes } from './send.utils'
-
-
-describe('send utils', () => {
- describe('removeLeadingZeroes()', () => {
- it('should remove leading zeroes from int when user types', () => {
- assert.equal(removeLeadingZeroes('0'), '0')
- assert.equal(removeLeadingZeroes('1'), '1')
- assert.equal(removeLeadingZeroes('00'), '0')
- assert.equal(removeLeadingZeroes('01'), '1')
- })
-
- it('should remove leading zeroes from int when user copy/paste', () => {
- assert.equal(removeLeadingZeroes('001'), '1')
- })
-
- it('should remove leading zeroes from float when user types', () => {
- assert.equal(removeLeadingZeroes('0.'), '0.')
- assert.equal(removeLeadingZeroes('0.0'), '0.0')
- assert.equal(removeLeadingZeroes('0.00'), '0.00')
- assert.equal(removeLeadingZeroes('0.001'), '0.001')
- assert.equal(removeLeadingZeroes('0.10'), '0.10')
- })
-
- it('should remove leading zeroes from float when user copy/paste', () => {
- assert.equal(removeLeadingZeroes('00.1'), '0.1')
- })
- })
-})
diff --git a/ui/app/components/sender-to-recipient.js b/ui/app/components/sender-to-recipient.js
deleted file mode 100644
index 9cef8e401..000000000
--- a/ui/app/components/sender-to-recipient.js
+++ /dev/null
@@ -1,72 +0,0 @@
-const { Component } = require('react')
-const h = require('react-hyperscript')
-const connect = require('react-redux').connect
-const PropTypes = require('prop-types')
-const Identicon = require('./identicon')
-
-class SenderToRecipient extends Component {
- renderRecipientIcon () {
- const { recipientAddress } = this.props
- return (
- recipientAddress
- ? h(Identicon, { address: recipientAddress, diameter: 20 })
- : h('i.fa.fa-file-text-o')
- )
- }
-
- renderRecipient () {
- const { recipientName } = this.props
- return (
- h('.sender-to-recipient__recipient', [
- this.renderRecipientIcon(),
- h(
- '.sender-to-recipient__name.sender-to-recipient__recipient-name',
- recipientName || this.context.t('newContract')
- ),
- ])
- )
- }
-
- render () {
- const { senderName, senderAddress } = this.props
-
- return (
- h('.sender-to-recipient__container', [
- h('.sender-to-recipient__sender', [
- h('.sender-to-recipient__sender-icon', [
- h(Identicon, {
- address: senderAddress,
- diameter: 20,
- }),
- ]),
- h('.sender-to-recipient__name.sender-to-recipient__sender-name', senderName),
- ]),
- h('.sender-to-recipient__arrow-container', [
- h('.sender-to-recipient__arrow-circle', [
- h('img', {
- height: 15,
- width: 15,
- src: './images/arrow-right.svg',
- }),
- ]),
- ]),
- this.renderRecipient(),
- ])
- )
- }
-}
-
-SenderToRecipient.propTypes = {
- senderName: PropTypes.string,
- senderAddress: PropTypes.string,
- recipientName: PropTypes.string,
- recipientAddress: PropTypes.string,
- t: PropTypes.func,
-}
-
-SenderToRecipient.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect()(SenderToRecipient)
-
diff --git a/ui/app/components/sender-to-recipient/index.js b/ui/app/components/sender-to-recipient/index.js
new file mode 100644
index 000000000..f515c4ac4
--- /dev/null
+++ b/ui/app/components/sender-to-recipient/index.js
@@ -0,0 +1 @@
+export { default } from './sender-to-recipient.component'
diff --git a/ui/app/css/itcss/components/sender-to-recipient.scss b/ui/app/components/sender-to-recipient/index.scss
index f16013cdf..a97393b8f 100644
--- a/ui/app/css/itcss/components/sender-to-recipient.scss
+++ b/ui/app/components/sender-to-recipient/index.scss
@@ -6,6 +6,16 @@
justify-content: center;
border-bottom: 1px solid $geyser;
position: relative;
+ flex: 0 0 auto;
+ height: 42px;
+ }
+
+ &__tooltip-wrapper {
+ min-width: 0;
+ }
+
+ &__tooltip-container {
+ max-width: 100%;
}
&__sender,
@@ -14,7 +24,7 @@
flex-direction: row;
align-items: center;
flex: 1;
- padding: 10px 20px;
+ padding: 0 16px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -22,11 +32,16 @@
&__sender {
padding-right: 30px;
+ cursor: pointer;
}
&__recipient {
- border-left: 1px solid $geyser;
padding-left: 30px;
+ border-left: 1px solid $geyser;
+
+ &--with-address {
+ cursor: pointer;
+ }
}
&__arrow-container {
@@ -42,17 +57,18 @@
padding: 5px;
border: 1px solid $geyser;
border-radius: 20px;
- height: 30px;
- width: 30px;
+ height: 32px;
+ width: 32px;
display: flex;
justify-content: center;
align-items: center;
}
&__name {
- padding-left: 5px;
+ padding-left: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
+ font-size: .875rem;
}
}
diff --git a/ui/app/components/sender-to-recipient/sender-to-recipient.component.js b/ui/app/components/sender-to-recipient/sender-to-recipient.component.js
new file mode 100644
index 000000000..cae173b56
--- /dev/null
+++ b/ui/app/components/sender-to-recipient/sender-to-recipient.component.js
@@ -0,0 +1,117 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import Identicon from '../identicon'
+import Tooltip from '../tooltip-v2'
+import copyToClipboard from 'copy-to-clipboard'
+
+export default class SenderToRecipient extends Component {
+ static propTypes = {
+ senderName: PropTypes.string,
+ senderAddress: PropTypes.string,
+ recipientName: PropTypes.string,
+ recipientAddress: PropTypes.string,
+ t: PropTypes.func,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ state = {
+ senderAddressCopied: false,
+ recipientAddressCopied: false,
+ }
+
+ renderRecipientWithAddress () {
+ const { t } = this.context
+ const { recipientName, recipientAddress } = this.props
+
+ return (
+ <div
+ className="sender-to-recipient__recipient sender-to-recipient__recipient--with-address"
+ onClick={() => {
+ this.setState({ recipientAddressCopied: true })
+ copyToClipboard(recipientAddress)
+ }}
+ >
+ <div className="sender-to-recipient__sender-icon">
+ <Identicon
+ address={recipientAddress}
+ diameter={24}
+ />
+ </div>
+ <Tooltip
+ position="bottom"
+ title={this.state.recipientAddressCopied ? t('copiedExclamation') : t('copyAddress')}
+ wrapperClassName="sender-to-recipient__tooltip-wrapper"
+ containerClassName="sender-to-recipient__tooltip-container"
+ onHidden={() => this.setState({ recipientAddressCopied: false })}
+ >
+ <div className="sender-to-recipient__name sender-to-recipient__recipient-name">
+ { recipientName || this.context.t('newContract') }
+ </div>
+ </Tooltip>
+ </div>
+ )
+ }
+
+ renderRecipientWithoutAddress () {
+ return (
+ <div className="sender-to-recipient__recipient">
+ <i className="fa fa-file-text-o" />
+ <div className="sender-to-recipient__name sender-to-recipient__recipient-name">
+ { this.context.t('newContract') }
+ </div>
+ </div>
+ )
+ }
+
+ render () {
+ const { t } = this.context
+ const { senderName, senderAddress, recipientAddress } = this.props
+
+ return (
+ <div className="sender-to-recipient__container">
+ <div
+ className="sender-to-recipient__sender"
+ onClick={() => {
+ this.setState({ senderAddressCopied: true })
+ copyToClipboard(senderAddress)
+ }}
+ >
+ <div className="sender-to-recipient__sender-icon">
+ <Identicon
+ address={senderAddress}
+ diameter={24}
+ />
+ </div>
+ <Tooltip
+ position="bottom"
+ title={this.state.senderAddressCopied ? t('copiedExclamation') : t('copyAddress')}
+ wrapperClassName="sender-to-recipient__tooltip-wrapper"
+ containerClassName="sender-to-recipient__tooltip-container"
+ onHidden={() => this.setState({ senderAddressCopied: false })}
+ >
+ <div className="sender-to-recipient__name sender-to-recipient__sender-name">
+ { senderName }
+ </div>
+ </Tooltip>
+ </div>
+ <div className="sender-to-recipient__arrow-container">
+ <div className="sender-to-recipient__arrow-circle">
+ <img
+ height={15}
+ width={15}
+ src="./images/arrow-right.svg"
+ />
+ </div>
+ </div>
+ {
+ recipientAddress
+ ? this.renderRecipientWithAddress()
+ : this.renderRecipientWithoutAddress()
+ }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/signature-request.js b/ui/app/components/signature-request.js
index bbb340fcf..2e0102d1a 100644
--- a/ui/app/components/signature-request.js
+++ b/ui/app/components/signature-request.js
@@ -22,6 +22,8 @@ const {
conversionRateSelector,
} = require('../selectors.js')
+import { clearConfirmTransaction } from '../ducks/confirm-transaction.duck'
+
const { DEFAULT_ROUTE } = require('../routes')
function mapStateToProps (state) {
@@ -39,6 +41,7 @@ function mapStateToProps (state) {
function mapDispatchToProps (dispatch) {
return {
goHome: () => dispatch(actions.goHome()),
+ clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
}
}
@@ -247,12 +250,18 @@ SignatureRequest.prototype.renderFooter = function () {
return h('div.request-signature__footer', [
h('button.btn-default.btn--large.request-signature__footer__cancel-button', {
onClick: event => {
- cancel(event).then(() => this.props.history.push(DEFAULT_ROUTE))
+ cancel(event).then(() => {
+ this.props.clearConfirmTransaction()
+ this.props.history.push(DEFAULT_ROUTE)
+ })
},
}, this.context.t('cancel')),
h('button.btn-primary.btn--large', {
onClick: event => {
- sign(event).then(() => this.props.history.push(DEFAULT_ROUTE))
+ sign(event).then(() => {
+ this.props.clearConfirmTransaction()
+ this.props.history.push(DEFAULT_ROUTE)
+ })
},
}, this.context.t('sign')),
])
diff --git a/ui/app/components/tabs/index.js b/ui/app/components/tabs/index.js
new file mode 100644
index 000000000..3a8d18248
--- /dev/null
+++ b/ui/app/components/tabs/index.js
@@ -0,0 +1,3 @@
+import Tabs from './tabs.component'
+import Tab from './tab'
+export { Tabs, Tab }
diff --git a/ui/app/components/tabs/index.scss b/ui/app/components/tabs/index.scss
new file mode 100644
index 000000000..a3b42f8e3
--- /dev/null
+++ b/ui/app/components/tabs/index.scss
@@ -0,0 +1,11 @@
+@import './tab/index';
+
+.tabs {
+ &__list {
+ display: flex;
+ justify-content: flex-start;
+ background-color: #f9fafa;
+ border-bottom: 1px solid $geyser;
+ padding: 0 16px;
+ }
+}
diff --git a/ui/app/components/tabs/tab/index.js b/ui/app/components/tabs/tab/index.js
new file mode 100644
index 000000000..fbc309e8e
--- /dev/null
+++ b/ui/app/components/tabs/tab/index.js
@@ -0,0 +1,2 @@
+import Tab from './tab.component'
+module.exports = Tab
diff --git a/ui/app/components/tabs/tab/index.scss b/ui/app/components/tabs/tab/index.scss
new file mode 100644
index 000000000..1de6ffa0e
--- /dev/null
+++ b/ui/app/components/tabs/tab/index.scss
@@ -0,0 +1,15 @@
+.tab {
+ color: #8C8E94;
+ font-size: .75rem;
+ text-transform: uppercase;
+ cursor: pointer;
+ padding: 8px 0;
+ margin: 0 8px;
+ min-width: 50px;
+ text-align: center;
+
+ &--active {
+ color: $black;
+ border-bottom: 2px solid $curious-blue;
+ }
+}
diff --git a/ui/app/components/tabs/tab/tab.component.js b/ui/app/components/tabs/tab/tab.component.js
new file mode 100644
index 000000000..a59da8904
--- /dev/null
+++ b/ui/app/components/tabs/tab/tab.component.js
@@ -0,0 +1,31 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+
+const Tab = props => {
+ const { name, onClick, isActive, tabIndex } = props
+
+ return (
+ <li
+ className={classnames(
+ 'tab',
+ isActive && 'tab--active',
+ )}
+ onClick={event => {
+ event.preventDefault()
+ onClick(tabIndex)
+ }}
+ >
+ { name }
+ </li>
+ )
+}
+
+Tab.propTypes = {
+ name: PropTypes.string.isRequired,
+ onClick: PropTypes.func,
+ isActive: PropTypes.bool,
+ tabIndex: PropTypes.number,
+}
+
+export default Tab
diff --git a/ui/app/components/tabs/tabs.component.js b/ui/app/components/tabs/tabs.component.js
new file mode 100644
index 000000000..d26dcff2f
--- /dev/null
+++ b/ui/app/components/tabs/tabs.component.js
@@ -0,0 +1,62 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+
+export default class Tabs extends Component {
+ static propTypes = {
+ defaultActiveTabIndex: PropTypes.number,
+ children: PropTypes.node,
+ }
+
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ activeTabIndex: props.defaultActiveTabIndex || 0,
+ }
+ }
+
+ handleTabClick (tabIndex) {
+ const { activeTabIndex } = this.state
+
+ if (tabIndex !== activeTabIndex) {
+ this.setState({
+ activeTabIndex: tabIndex,
+ })
+ }
+ }
+
+ renderTabs () {
+ const numberOfTabs = React.Children.count(this.props.children)
+
+ return React.Children.map(this.props.children, (child, index) => {
+ return child && React.cloneElement(child, {
+ onClick: index => this.handleTabClick(index),
+ tabIndex: index,
+ isActive: numberOfTabs > 1 && index === this.state.activeTabIndex,
+ key: index,
+ })
+ })
+ }
+
+ renderActiveTabContent () {
+ const { children } = this.props
+ const { activeTabIndex } = this.state
+
+ return children[activeTabIndex]
+ ? children[activeTabIndex].props.children
+ : children.props.children
+ }
+
+ render () {
+ return (
+ <div className="tabs">
+ <ul className="tabs__list">
+ { this.renderTabs() }
+ </ul>
+ <div className="tabs__content">
+ { this.renderActiveTabContent() }
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/token-balance.js b/ui/app/components/token-balance.js
index df3bd59bb..99ca7335c 100644
--- a/ui/app/components/token-balance.js
+++ b/ui/app/components/token-balance.js
@@ -98,6 +98,10 @@ TokenBalance.prototype.componentDidUpdate = function (nextProps) {
}
TokenBalance.prototype.updateBalance = function (tokens = []) {
+ if (!this.tracker.running) {
+ return
+ }
+
const [{ string, symbol }] = tokens
this.setState({
@@ -110,5 +114,7 @@ TokenBalance.prototype.updateBalance = function (tokens = []) {
TokenBalance.prototype.componentWillUnmount = function () {
if (!this.tracker) return
this.tracker.stop()
+ this.tracker.removeListener('update', this.balanceUpdater)
+ this.tracker.removeListener('error', this.showError)
}
diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js
index 4189cf801..42351cf89 100644
--- a/ui/app/components/token-list.js
+++ b/ui/app/components/token-list.js
@@ -158,12 +158,17 @@ TokenList.prototype.componentDidUpdate = function (nextProps) {
}
TokenList.prototype.updateBalances = function (tokens) {
+ if (!this.tracker.running) {
+ return
+ }
this.setState({ tokens, isLoading: false })
}
TokenList.prototype.componentWillUnmount = function () {
if (!this.tracker) return
this.tracker.stop()
+ this.tracker.removeListener('update', this.balanceUpdater)
+ this.tracker.removeListener('error', this.showError)
}
// function uniqueMergeTokens (tokensA, tokensB = []) {
diff --git a/ui/app/components/tooltip-v2.js b/ui/app/components/tooltip-v2.js
index 133a0f16a..05a5efc80 100644
--- a/ui/app/components/tooltip-v2.js
+++ b/ui/app/components/tooltip-v2.js
@@ -12,7 +12,7 @@ function Tooltip () {
Tooltip.prototype.render = function () {
const props = this.props
- const { position, title, children, wrapperClassName } = props
+ const { position, title, children, wrapperClassName, containerClassName, onHidden } = props
return h('div', {
className: wrapperClassName,
@@ -25,6 +25,8 @@ Tooltip.prototype.render = function () {
hideOnClick: false,
size: 'small',
arrow: true,
+ className: containerClassName,
+ onHidden,
}, children),
])
diff --git a/ui/app/components/tx-list-item.js b/ui/app/components/tx-list-item.js
index 9a2fb5311..0d693b805 100644
--- a/ui/app/components/tx-list-item.js
+++ b/ui/app/components/tx-list-item.js
@@ -54,6 +54,8 @@ function TxListItem () {
fiatTotal: null,
isTokenTx: null,
}
+
+ this.unmounted = false
}
TxListItem.prototype.componentDidMount = async function () {
@@ -67,9 +69,16 @@ TxListItem.prototype.componentDidMount = async function () {
? await this.getSendTokenTotal()
: this.getSendEtherTotal()
+ if (this.unmounted) {
+ return
+ }
this.setState({ total, fiatTotal, isTokenTx })
}
+TxListItem.prototype.componentWillUnmount = function () {
+ this.unmounted = true
+}
+
TxListItem.prototype.getAddressText = function () {
const {
address,
@@ -298,20 +307,16 @@ TxListItem.prototype.render = function () {
]),
]),
- this.showRetryButton() && h('div.tx-list-item-retry-container', [
-
- h('span.tx-list-item-retry-copy', 'Taking too long?'),
-
- h('span.tx-list-item-retry-link', {
- onClick: (event) => {
- event.stopPropagation()
- if (isTokenTx) {
- this.setSelectedToken(txParams.to)
- }
- this.resubmit()
- },
- }, 'Increase the gas price on your transaction'),
-
+ this.showRetryButton() && h('.tx-list-item-retry-container', {
+ onClick: (event) => {
+ event.stopPropagation()
+ if (isTokenTx) {
+ this.setSelectedToken(txParams.to)
+ }
+ this.resubmit()
+ },
+ }, [
+ h('span', 'Taking too long? Increase the gas price on your transaction'),
]),
]), // holding on icon from design
diff --git a/ui/app/components/tx-view.js b/ui/app/components/tx-view.js
index 014497fcd..654090da6 100644
--- a/ui/app/components/tx-view.js
+++ b/ui/app/components/tx-view.js
@@ -11,6 +11,7 @@ const { SEND_ROUTE } = require('../routes')
const { checksumAddress: toChecksumAddress } = require('../util')
const BalanceComponent = require('./balance-component')
+const Tooltip = require('./tooltip')
const TxList = require('./tx-list')
const SelectedAccount = require('./selected-account')
@@ -103,7 +104,8 @@ TxView.prototype.renderButtons = function () {
}
TxView.prototype.render = function () {
- const { isMascara } = this.props
+ const { hideSidebar, isMascara, showSidebar, sidebarOpen } = this.props
+ const { t } = this.context
return h('div.tx-view.flex-column', {
style: {},
@@ -120,21 +122,30 @@ TxView.prototype.render = function () {
},
}, [
- h('div.fa.fa-bars', {
- style: {
- fontSize: '1.3em',
- cursor: 'pointer',
- padding: '10px',
- },
- onClick: () => this.props.sidebarOpen ? this.props.hideSidebar() : this.props.showSidebar(),
- }),
+ h(Tooltip, {
+ title: t('menu'),
+ position: 'bottom',
+ }, [
+ h('div.fa.fa-bars', {
+ style: {
+ fontSize: '1.3em',
+ cursor: 'pointer',
+ padding: '10px',
+ },
+ onClick: () => sidebarOpen ? hideSidebar() : showSidebar(),
+ }),
+ ]),
h(SelectedAccount),
- !isMascara && h('div.open-in-browser', {
- onClick: () => global.platform.openExtensionInBrowser(),
- }, [h('img', { src: 'images/popout.svg' })]),
-
+ !isMascara && h(Tooltip, {
+ title: t('openInTab'),
+ position: 'bottom',
+ }, [
+ h('div.open-in-browser', {
+ onClick: () => global.platform.openExtensionInBrowser(),
+ }, [h('img', { src: 'images/popout.svg' })]),
+ ]),
]),
this.renderHeroBalance(),
diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js
index da142fad8..20c2be0f1 100644
--- a/ui/app/components/wallet-view.js
+++ b/ui/app/components/wallet-view.js
@@ -175,7 +175,7 @@ WalletView.prototype.render = function () {
this.setState({ copyToClipboardPressed: false })
},
}, [
- `${checksummedAddress.slice(0, 4)}...${checksummedAddress.slice(-4)}`,
+ `${checksummedAddress.slice(0, 6)}...${checksummedAddress.slice(-4)}`,
h('i.fa.fa-clipboard', { style: { marginLeft: '8px' } }),
]),
]),
diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js
index 461587cb1..112ea6bca 100644
--- a/ui/app/conf-tx.js
+++ b/ui/app/conf-tx.js
@@ -9,11 +9,7 @@ const txHelper = require('../lib/tx-helper')
const log = require('loglevel')
const R = require('ramda')
-const PendingTx = require('./components/pending-tx')
const SignatureRequest = require('./components/signature-request')
-// const PendingMsg = require('./components/pending-msg')
-// const PendingPersonalMsg = require('./components/pending-personal-msg')
-// const PendingTypedMsg = require('./components/pending-typed-msg')
const Loading = require('./components/loading-screen')
const { DEFAULT_ROUTE } = require('./routes')
@@ -105,7 +101,7 @@ ConfirmTxScreen.prototype.componentDidUpdate = function (prevProps) {
const unconfTxList = txHelper(unapprovedTxs, {}, {}, {}, network)
- if (prevTx.status === 'dropped') {
+ if (prevTx && prevTx.status === 'dropped') {
this.props.dispatch(actions.showModal({
name: 'TRANSACTION_CONFIRMED',
onHide: () => history.push(DEFAULT_ROUTE),
@@ -151,102 +147,32 @@ ConfirmTxScreen.prototype.render = function () {
currentCurrency,
conversionRate,
blockGasLimit,
- // provider,
- // computedBalances,
} = props
var txData = this.getTxData() || {}
- var txParams = txData.params || {}
-
- // var isNotification = isPopupOrNotification() === 'notification'
- /*
- Client is using the flag above to render the following in conf screen
- // subtitle and nav
- h('.section-title.flex-row.flex-center', [
- !isNotification ? h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
- onClick: this.goHome.bind(this),
- }) : null,
- h('h2.page-subtitle', 'Confirm Transaction'),
- isNotification ? h(NetworkIndicator, {
- network: network,
- provider: provider,
- }) : null,
- ]),
- */
-
-
- return currentTxView({
- // Properties
- txData: txData,
- key: txData.id,
- selectedAddress: props.selectedAddress,
- accounts: props.accounts,
- identities: props.identities,
- conversionRate,
- currentCurrency,
- blockGasLimit,
- // Actions
- buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress),
- sendTransaction: this.sendTransaction.bind(this),
- cancelTransaction: this.cancelTransaction.bind(this, txData),
- signMessage: this.signMessage.bind(this, txData),
- signPersonalMessage: this.signPersonalMessage.bind(this, txData),
- signTypedMessage: this.signTypedMessage.bind(this, txData),
- cancelMessage: this.cancelMessage.bind(this, txData),
- cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData),
- cancelTypedMessage: this.cancelTypedMessage.bind(this, txData),
- })
-}
-
-function currentTxView (opts) {
- log.info('rendering current tx view')
- const { txData } = opts
- const { txParams, msgParams } = txData
-
- if (txParams) {
- log.debug('txParams detected, rendering pending tx')
- return h(PendingTx, opts)
- } else if (msgParams) {
- log.debug('msgParams detected, rendering pending msg')
-
- return h(SignatureRequest, opts)
-
- // if (type === 'eth_sign') {
- // log.debug('rendering eth_sign message')
- // return h(PendingMsg, opts)
- // } else if (type === 'personal_sign') {
- // log.debug('rendering personal_sign message')
- // return h(PendingPersonalMsg, opts)
- // } else if (type === 'eth_signTypedData') {
- // log.debug('rendering eth_signTypedData message')
- // return h(PendingTypedMsg, opts)
- // }
- }
-
- return h(Loading)
-}
-
-ConfirmTxScreen.prototype.buyEth = function (address, event) {
- event.preventDefault()
- this.props.dispatch(actions.buyEthView(address))
-}
-
-ConfirmTxScreen.prototype.sendTransaction = function (txData, event) {
- this.stopPropagation(event)
- this.props.dispatch(actions.updateAndApproveTx(txData))
- .then(() => this.props.history.push(DEFAULT_ROUTE))
-}
-
-ConfirmTxScreen.prototype.cancelTransaction = function (txData, event) {
- this.stopPropagation(event)
- event.preventDefault()
- this.props.dispatch(actions.cancelTx(txData))
-}
-
-ConfirmTxScreen.prototype.cancelAllTransactions = function (unconfTxList, event) {
- this.stopPropagation(event)
- event.preventDefault()
- this.props.dispatch(actions.cancelAllTx(unconfTxList))
+ const { msgParams } = txData
+ log.debug('msgParams detected, rendering pending msg')
+
+ return msgParams
+ ? h(SignatureRequest, {
+ // Properties
+ txData: txData,
+ key: txData.id,
+ selectedAddress: props.selectedAddress,
+ accounts: props.accounts,
+ identities: props.identities,
+ conversionRate,
+ currentCurrency,
+ blockGasLimit,
+ // Actions
+ signMessage: this.signMessage.bind(this, txData),
+ signPersonalMessage: this.signPersonalMessage.bind(this, txData),
+ signTypedMessage: this.signTypedMessage.bind(this, txData),
+ cancelMessage: this.cancelMessage.bind(this, txData),
+ cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData),
+ cancelTypedMessage: this.cancelTypedMessage.bind(this, txData),
+ })
+ : h(Loading)
}
ConfirmTxScreen.prototype.signMessage = function (msgData, event) {
@@ -296,20 +222,3 @@ ConfirmTxScreen.prototype.cancelTypedMessage = function (msgData, event) {
this.stopPropagation(event)
return this.props.dispatch(actions.cancelTypedMsg(msgData))
}
-
-ConfirmTxScreen.prototype.goHome = function (event) {
- this.stopPropagation(event)
- this.props.dispatch(actions.goHome())
-}
-
-// function warningIfExists (warning) {
-// if (warning &&
-// // Do not display user rejections on this screen:
-// warning.indexOf('User denied transaction signature') === -1) {
-// return h('.error', {
-// style: {
-// margin: 'auto',
-// },
-// }, warning)
-// }
-// }
diff --git a/ui/app/constants/error-keys.js b/ui/app/constants/error-keys.js
new file mode 100644
index 000000000..1b89be62e
--- /dev/null
+++ b/ui/app/constants/error-keys.js
@@ -0,0 +1,3 @@
+export const INSUFFICIENT_FUNDS_ERROR_KEY = 'insufficientFunds'
+export const GAS_LIMIT_TOO_LOW_ERROR_KEY = 'gasLimitTooLow'
+export const TRANSACTION_ERROR = 'transactionError'
diff --git a/ui/app/css/itcss/components/account-menu.scss b/ui/app/css/itcss/components/account-menu.scss
index 96fba890c..b14753e23 100644
--- a/ui/app/css/itcss/components/account-menu.scss
+++ b/ui/app/css/itcss/components/account-menu.scss
@@ -72,6 +72,7 @@
background-color: $dusty-gray;
color: $black;
font-weight: normal;
+ letter-spacing: .5px;
}
}
@@ -84,6 +85,23 @@
@media screen and (max-width: 575px) {
padding: 12px 14px;
}
+
+ .remove-account-icon {
+ width: 15px;
+ margin-left: 10px;
+ height: 15px;
+ }
+
+ &:hover {
+ .remove-account-icon::after {
+ content: '\00D7';
+ font-size: 25px;
+ color: $white;
+ cursor: pointer;
+ position: absolute;
+ margin-top: -5px;
+ }
+ }
}
&__account-info {
diff --git a/ui/app/css/itcss/components/alert.scss b/ui/app/css/itcss/components/alert.scss
new file mode 100644
index 000000000..930fc3f54
--- /dev/null
+++ b/ui/app/css/itcss/components/alert.scss
@@ -0,0 +1,57 @@
+.global-alert {
+ position: relative;
+ width: 100%;
+ background-color: #33A4E7;
+
+ .msg {
+ width: 100%;
+ display: block;
+ color: white;
+ font-size: 12px;
+ text-align: center;
+ }
+}
+
+.global-alert.hidden {
+ animation: alertHidden .5s ease forwards;
+}
+
+.global-alert.visible {
+ animation: alert .5s ease forwards;
+}
+
+/* Animation */
+@keyframes alert {
+ 0% {
+ opacity: 0;
+ top: -50px;
+ padding: 0px;
+ line-height: 12px;
+ }
+
+ 50% {
+ opacity: 1;
+ }
+
+ 100% {
+ top: 0;
+ padding: 8px;
+ line-height: 12px;
+ }
+}
+
+@keyframes alertHidden {
+ 0% {
+ top: 0;
+ opacity: 1;
+ padding: 8px;
+ line-height: 12px;
+ }
+
+ 100% {
+ opacity: 0;
+ top: -50px;
+ padding: 0px;
+ line-height: 0px;
+ }
+}
diff --git a/ui/app/css/itcss/components/buttons.scss b/ui/app/css/itcss/components/buttons.scss
index f93daec04..34565767f 100644
--- a/ui/app/css/itcss/components/buttons.scss
+++ b/ui/app/css/itcss/components/buttons.scss
@@ -4,7 +4,8 @@
.btn-default,
.btn-primary,
-.btn-secondary {
+.btn-secondary,
+.btn-confirm {
height: 44px;
background: $white;
display: flex;
@@ -13,13 +14,14 @@
box-sizing: border-box;
border-radius: 4px;
font-size: 14px;
- font-weight: 500;
+ font-weight: 400;
transition: border-color .3s ease;
padding: 0 16px;
min-width: 140px;
width: 100%;
text-transform: uppercase;
outline: none;
+ font-family: Roboto;
&--disabled,
&[disabled] {
@@ -71,6 +73,12 @@
}
}
+.btn-confirm {
+ color: $white;
+ border: 2px solid $curious-blue;
+ background-color: $curious-blue;
+}
+
.btn--large {
height: 54px;
}
@@ -119,19 +127,6 @@
}
}
-.btn-confirm {
- background-color: $caribbean-green; // TODO: reusable color in colors.css
- text-align: center;
- padding: .8rem 1rem;
- color: $white;
- border: 2px solid $caribbean-green;
- border-radius: 4px;
- font-size: .85rem;
- font-weight: 400;
- transition: border-color .3s ease;
- width: 100%;
-}
-
// No longer used in flat design, remove when modal buttons done
// div.wallet-btn {
// border: 1px solid rgb(91, 93, 103);
diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss
index 1d87b8004..96ad5fe64 100644
--- a/ui/app/css/itcss/components/index.scss
+++ b/ui/app/css/itcss/components/index.scss
@@ -8,6 +8,8 @@
@import './modal.scss';
+@import './alert.scss';
+
@import './newui-sections.scss';
@import './account-dropdown.scss';
@@ -58,6 +60,4 @@
@import './welcome-screen.scss';
-@import './sender-to-recipient.scss';
-
@import '../../../components/index';
diff --git a/ui/app/css/itcss/components/network.scss b/ui/app/css/itcss/components/network.scss
index 6c8be0b6d..545a2a940 100644
--- a/ui/app/css/itcss/components/network.scss
+++ b/ui/app/css/itcss/components/network.scss
@@ -159,15 +159,3 @@
.network-caret {
margin: 0 8px 2px;
}
-
-.network-display {
- &__container {
- display: flex;
- align-items: center;
- justify-content: flex-start;
-
- @media screen and (min-width: 576px) {
- display: none;
- }
- }
-}
diff --git a/ui/app/css/itcss/components/new-account.scss b/ui/app/css/itcss/components/new-account.scss
index 293579058..b12afb124 100644
--- a/ui/app/css/itcss/components/new-account.scss
+++ b/ui/app/css/itcss/components/new-account.scss
@@ -1,9 +1,8 @@
.new-account {
- width: 376px;
+ width: 375px;
background-color: #FFFFFF;
box-shadow: 0 0 7px 0 rgba(0,0,0,0.08);
z-index: 25;
- padding-bottom: 31px;
&__header {
display: flex;
@@ -28,7 +27,6 @@
&__tab {
height: 54px;
- width: 75px;
padding: 15px 10px;
color: $dusty-gray;
font-family: Roboto;
@@ -38,10 +36,6 @@
cursor: pointer;
}
- &__tab:first-of-type {
- margin-right: 20px;
- }
-
&__tab:hover {
color: $black;
border-bottom: none;
@@ -69,7 +63,7 @@
display: flex;
flex-flow: column;
align-items: center;
- padding: 0 30px;
+ padding: 0 30px 30px;
&__select-section {
display: flex;
@@ -158,11 +152,296 @@
}
}
+.hw-tutorial {
+ width: 375px;
+ border-top: 1px solid #D2D8DD;
+ border-bottom: 1px solid #D2D8DD;
+ overflow: visible;
+ display: block;
+ padding: 15px 30px;
+}
+
+.hw-connect {
+ &__header {
+ &__title {
+ margin-top: 5px;
+ margin-bottom: 15px;
+ font-size: 22px;
+ text-align: center;
+ }
+
+ &__msg {
+ font-size: 14px;
+ color: #9b9b9b;
+ margin-top: 10px;
+ margin-bottom: 0px;
+ }
+ }
+
+ &__learn-more {
+ margin-top: 15px;
+ font-size: 14px;
+ color: #5B5D67;
+ line-height: 19px;
+ text-align: center;
+ cursor: pointer;
+
+ &__arrow {
+ transform: rotate(90deg);
+ display: block;
+ text-align: center;
+ height: 30px;
+ margin: 0px auto 10px;
+ }
+ }
+
+ &__title {
+ padding-top: 10px;
+ font-weight: 400;
+ font-size: 18px;
+ }
+
+ &__msg {
+ font-size: 14px;
+ color: #9b9b9b;
+ margin-top: 10px;
+ margin-bottom: 15px;
+ }
+
+ &__link {
+ color: #2f9ae0;
+ }
+
+ &__footer {
+ width: 100%;
+
+ &__title {
+ padding-top: 15px;
+ padding-bottom: 12px;
+ font-weight: 400;
+ font-size: 18px;
+ text-align: center;
+ }
+
+ &__msg {
+ font-size: 14px;
+ color: #9b9b9b;
+ margin-top: 12px;
+ margin-bottom: 27px;
+ }
+
+ &__link {
+ color: #2f9ae0;
+ margin-left: 5px;
+ }
+ }
+
+ &__get-trezor {
+ width: 100%;
+ padding-bottom: 20px;
+ padding-top: 20px;
+
+ &__msg {
+ font-size: 14px;
+ color: #9b9b9b;
+ }
+
+ &__link {
+ font-size: 14px;
+ text-align: center;
+ color: #2f9ae0;
+ cursor: pointer;
+ }
+ }
+
+ &__step-asset {
+ margin: 0px auto 20px;
+ display: flex;
+ }
+}
+
+.hw-account-list {
+ display: flex;
+ flex: 1;
+ flex-flow: column;
+ width: 100%;
+
+ &__title_wrapper {
+ display: flex;
+ flex-direction: row;
+ flex: 1;
+ }
+
+ &__title {
+ margin-bottom: 23px;
+ align-self: flex-start;
+ color: $scorpion;
+ font-family: Roboto;
+ font-size: 16px;
+ line-height: 21px;
+ font-weight: bold;
+ display: flex;
+ flex: 1;
+ }
+
+ &__device {
+ margin-bottom: 23px;
+ align-self: flex-end;
+ color: $scorpion;
+ font-family: Roboto;
+ font-size: 16px;
+ line-height: 21px;
+ font-weight: normal;
+ display: flex;
+ }
+
+ &__item {
+ font-size: 15px;
+ flex-direction: row;
+ display: flex;
+ padding-left: 10px;
+ padding-right: 10px;
+ }
+
+ &__item:nth-of-type(even) {
+ background-color: #fbfbfb;
+ }
+
+ &__item:nth-of-type(odd) {
+ background: rgba(0, 0, 0, 0.03);
+ }
+
+ &__item:hover {
+ background-color: rgba(0, 0, 0, 0.06);
+ }
+
+ &__item__index {
+ display: flex;
+ width: 24px;
+ }
+
+ &__item__radio {
+ display: flex;
+ flex: 1;
+
+ input {
+ padding: 10px;
+ margin-top: 13px;
+ }
+ }
+
+ &__item__label {
+ display: flex;
+ flex: 1;
+ padding-left: 10px;
+ padding-top: 10px;
+ padding-bottom: 10px;
+ }
+
+ &__item__balance {
+ display: flex;
+ flex: 1;
+ justify-content: center;
+ }
+
+ &__item__link {
+ display: flex;
+ margin-top: 13px;
+ }
+
+ &__item__link img {
+ width: 15px;
+ height: 15px;
+ }
+}
+
+.hw-list-pagination {
+ display: flex;
+ align-self: flex-end;
+ margin-top: 10px;
+
+ &__button {
+ height: 19px;
+ display: flex;
+ color: #33a4e7;
+ font-size: 14px;
+ line-height: 19px;
+ border: none;
+ min-width: 46px;
+ margin-right: 0px;
+ margin-left: 16px;
+ padding: 0px;
+ text-transform: uppercase;
+ font-family: Roboto;
+ }
+}
+
+.new-account-connect-form {
+ display: flex;
+ flex-flow: column;
+ align-items: center;
+ padding: 15px 30px 0;
+ height: 710px;
+ overflow: auto;
+
+ &.unsupported-browser {
+ height: 210px;
+ }
+
+ &.account-list {
+ height: auto;
+ }
+
+ &__buttons {
+ margin-top: 39px;
+ display: flex;
+ width: 100%;
+ justify-content: space-between;
+ }
+
+ &__button {
+ width: 150px;
+ min-width: initial;
+ }
+
+ .btn-primary {
+ background-color: #259DE5;
+ color: #FFFFFF;
+ border: none;
+ width: 100%;
+ min-height: 54px;
+ font-weight: 300;
+ font-size: 14px;
+ }
+
+ &__button.unlock {
+ width: 50%;
+ }
+
+ &__button.btn-primary--disabled {
+ cursor: not-allowed;
+ opacity: .5;
+ }
+}
+
+.hw-forget-device-container {
+ display: flex;
+ flex-flow: column;
+ align-items: center;
+ padding: 22px;
+
+ a {
+ color: #2f9ae0;
+ font-size: 14px;
+ cursor: pointer;
+ }
+}
+
.new-account-create-form {
display: flex;
flex-flow: column;
align-items: center;
- padding: 30px 30px 0;
+ padding: 30px;
&__input-label {
color: $scorpion;
diff --git a/ui/app/css/itcss/components/newui-sections.scss b/ui/app/css/itcss/components/newui-sections.scss
index 667e45ba2..bbfd85c90 100644
--- a/ui/app/css/itcss/components/newui-sections.scss
+++ b/ui/app/css/itcss/components/newui-sections.scss
@@ -332,3 +332,12 @@ $wallet-view-bg: $alabaster;
align-items: center;
flex: 1 0 auto;
}
+
+.first-view-main-wrapper {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ justify-content: center;
+ padding: 0 10px;
+ background: white;
+}
diff --git a/ui/app/css/itcss/components/request-signature.scss b/ui/app/css/itcss/components/request-signature.scss
index 4707ff60e..b607aded3 100644
--- a/ui/app/css/itcss/components/request-signature.scss
+++ b/ui/app/css/itcss/components/request-signature.scss
@@ -181,6 +181,7 @@
overflow-wrap: break-word;
border-bottom: 1px solid #d2d8dd;
padding: 6px 18px 15px;
+ white-space: pre-line;
}
&__help-link {
diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss
index c168242cf..e9c872ea7 100644
--- a/ui/app/css/itcss/components/send.scss
+++ b/ui/app/css/itcss/components/send.scss
@@ -628,7 +628,7 @@
}
}
- &__to-autocomplete, &__memo-text-area {
+ &__to-autocomplete, &__memo-text-area, &__hex-data {
&__input {
height: 54px;
width: 100%;
@@ -899,4 +899,4 @@
.sliders-icon {
color: $curious-blue;
-} \ No newline at end of file
+}
diff --git a/ui/app/css/itcss/components/token-list.scss b/ui/app/css/itcss/components/token-list.scss
index 4b706abce..49d0c290e 100644
--- a/ui/app/css/itcss/components/token-list.scss
+++ b/ui/app/css/itcss/components/token-list.scss
@@ -34,6 +34,7 @@ $wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and (
&__fiat-amount {
margin-top: .25%;
font-size: 105%;
+ width: 100%;
text-transform: uppercase;
@media #{$wallet-balance-breakpoint-range} {
diff --git a/ui/app/css/itcss/components/transaction-list.scss b/ui/app/css/itcss/components/transaction-list.scss
index d03faf486..1d45ff13b 100644
--- a/ui/app/css/itcss/components/transaction-list.scss
+++ b/ui/app/css/itcss/components/transaction-list.scss
@@ -129,12 +129,14 @@
.tx-list-item-retry-container {
background: #d1edff;
width: 100%;
- border-radius: 4px;
- font-size: 0.8em;
+ border-radius: 12px;
+ font-size: .75rem;
display: flex;
justify-content: center;
margin-left: 44px;
width: calc(100% - 44px);
+ padding: 4px;
+ cursor: pointer;
@media screen and (min-width: 576px) and (max-width: 679px) {
flex-flow: column;
@@ -151,10 +153,6 @@
}
}
-.tx-list-item-retry-copy {
- font-family: Roboto;
-}
-
.tx-list-item-retry-link {
text-decoration: underline;
margin-left: 6px;
diff --git a/ui/app/css/itcss/generic/index.scss b/ui/app/css/itcss/generic/index.scss
index 3525d2003..d1c65afed 100644
--- a/ui/app/css/itcss/generic/index.scss
+++ b/ui/app/css/itcss/generic/index.scss
@@ -73,195 +73,6 @@ input.large-input {
text-transform: uppercase;
}
-.page-container {
- width: 408px;
- background-color: $white;
- box-shadow: 0 0 7px 0 rgba(0, 0, 0, .08);
- z-index: 25;
- display: flex;
- flex-flow: column;
- border-radius: 8px;
-
- &__header {
- display: flex;
- flex-flow: column;
- border-bottom: 1px solid $geyser;
- padding: 16px;
- flex: 0 0 auto;
- position: relative;
-
- &--no-padding-bottom {
- padding-bottom: 0;
- }
- }
-
- &__header-close {
- color: $tundora;
- position: absolute;
- top: 16px;
- right: 16px;
- cursor: pointer;
- overflow: hidden;
-
- &::after {
- content: '\00D7';
- font-size: 40px;
- line-height: 20px;
- }
- }
-
- &__header-row {
- padding-bottom: 10px;
- display: flex;
- justify-content: space-between;
- }
-
- &__footer {
- display: flex;
- flex-flow: row;
- justify-content: center;
- border-top: 1px solid $geyser;
- padding: 16px;
- flex: 0 0 auto;
-
- .btn-clear,
- .btn-cancel,
- .btn-confirm {
- font-size: 1rem;
- }
- }
-
- &__footer-button {
- height: 55px;
- font-size: 1rem;
- text-transform: uppercase;
- margin-right: 16px;
- border-radius: 2px;
-
- &:last-of-type {
- margin-right: 0;
- }
- }
-
- &__back-button {
- color: #2f9ae0;
- font-size: 1rem;
- cursor: pointer;
- font-weight: 400;
- }
-
- &__title {
- color: $black;
- font-size: 2rem;
- font-weight: 500;
- line-height: 2rem;
- }
-
- &__subtitle {
- padding-top: .5rem;
- line-height: initial;
- font-size: .9rem;
- color: $gray;
- }
-
- &__tabs {
- display: flex;
- margin-top: 16px;
- }
-
- &__tab {
- min-width: 5rem;
- padding: 8px;
- color: $dusty-gray;
- font-family: Roboto;
- font-size: 1rem;
- text-align: center;
- cursor: pointer;
- border-bottom: none;
- margin-right: 16px;
-
- &:last-of-type {
- margin-right: 0;
- }
-
- &--selected {
- color: $curious-blue;
- border-bottom: 3px solid $curious-blue;
- }
- }
-
- &--full-width {
- width: 100% !important;
- }
-
- &--full-height {
- height: 100% !important;
- max-height: initial !important;
- min-height: initial !important;
- }
-
- &__content {
- overflow-y: auto;
- flex: 1;
- }
-
- &__warning-container {
- background: $linen;
- padding: 20px;
- display: flex;
- align-items: start;
- }
-
- &__warning-message {
- padding-left: 15px;
- }
-
- &__warning-title {
- font-weight: 500;
- }
-
- &__warning-icon {
- padding-top: 5px;
- }
-}
-
-@media screen and (max-width: 250px) {
- .page-container {
- &__footer {
- flex-flow: column-reverse;
- }
-
- &__footer-button {
- width: 100%;
- margin-bottom: 1rem;
- margin-right: 0;
-
- &:first-of-type {
- margin-bottom: 0;
- }
- }
- }
-}
-
-@media screen and (max-width: 575px) {
- .page-container {
- height: 100%;
- width: 100%;
- overflow-y: auto;
- background-color: $white;
- border-radius: 0;
- flex: 1;
- }
-}
-
-@media screen and (min-width: 576px) {
- .page-container {
- max-height: 82vh;
- min-height: 570px;
- flex: 0 0 auto;
- }
-}
-
.input-label {
padding-bottom: 10px;
font-weight: 400;
diff --git a/ui/app/css/itcss/settings/variables.scss b/ui/app/css/itcss/settings/variables.scss
index 814d7a382..f90c8edc3 100644
--- a/ui/app/css/itcss/settings/variables.scss
+++ b/ui/app/css/itcss/settings/variables.scss
@@ -55,6 +55,7 @@ $dodger-blue: #3099f2;
$zumthor: #edf7ff;
$ecstasy: #f7861c;
$linen: #fdf4f4;
+$oslo-gray: #8C8E94;
/*
Z-Indicies
diff --git a/ui/app/css/itcss/tools/utilities.scss b/ui/app/css/itcss/tools/utilities.scss
index ee867640d..209614c6b 100644
--- a/ui/app/css/itcss/tools/utilities.scss
+++ b/ui/app/css/itcss/tools/utilities.scss
@@ -165,7 +165,7 @@
}
.bold {
- font-weight: 700;
+ font-weight: 500;
}
.text-transform-uppercase {
diff --git a/ui/app/ducks/confirm-transaction.duck.js b/ui/app/ducks/confirm-transaction.duck.js
new file mode 100644
index 000000000..1885e12d1
--- /dev/null
+++ b/ui/app/ducks/confirm-transaction.duck.js
@@ -0,0 +1,386 @@
+import {
+ conversionRateSelector,
+ currentCurrencySelector,
+ unconfirmedTransactionsHashSelector,
+} from '../selectors/confirm-transaction'
+
+import {
+ getTokenData,
+ getMethodData,
+ getTransactionAmount,
+ getTransactionFee,
+ getHexGasTotal,
+ addFiat,
+ addEth,
+ increaseLastGasPrice,
+ hexGreaterThan,
+} from '../helpers/confirm-transaction/util'
+
+import { getSymbolAndDecimals } from '../token-util'
+import { conversionUtil } from '../conversion-util'
+
+// Actions
+const createActionType = action => `metamask/confirm-transaction/${action}`
+
+const UPDATE_TX_DATA = createActionType('UPDATE_TX_DATA')
+const CLEAR_TX_DATA = createActionType('CLEAR_TX_DATA')
+const UPDATE_TOKEN_DATA = createActionType('UPDATE_TOKEN_DATA')
+const CLEAR_TOKEN_DATA = createActionType('CLEAR_TOKEN_DATA')
+const UPDATE_METHOD_DATA = createActionType('UPDATE_METHOD_DATA')
+const CLEAR_METHOD_DATA = createActionType('CLEAR_METHOD_DATA')
+const CLEAR_CONFIRM_TRANSACTION = createActionType('CLEAR_CONFIRM_TRANSACTION')
+const UPDATE_TRANSACTION_AMOUNTS = createActionType('UPDATE_TRANSACTION_AMOUNTS')
+const UPDATE_TRANSACTION_FEES = createActionType('UPDATE_TRANSACTION_FEES')
+const UPDATE_TRANSACTION_TOTALS = createActionType('UPDATE_TRANSACTION_TOTALS')
+const UPDATE_HEX_GAS_TOTAL = createActionType('UPDATE_HEX_GAS_TOTAL')
+const UPDATE_TOKEN_PROPS = createActionType('UPDATE_TOKEN_PROPS')
+const UPDATE_NONCE = createActionType('UPDATE_NONCE')
+const FETCH_METHOD_DATA_START = createActionType('FETCH_METHOD_DATA_START')
+const FETCH_METHOD_DATA_END = createActionType('FETCH_METHOD_DATA_END')
+
+// Initial state
+const initState = {
+ txData: {},
+ tokenData: {},
+ methodData: {},
+ tokenProps: {
+ tokenDecimals: '',
+ tokenSymbol: '',
+ },
+ fiatTransactionAmount: '',
+ fiatTransactionFee: '',
+ fiatTransactionTotal: '',
+ ethTransactionAmount: '',
+ ethTransactionFee: '',
+ ethTransactionTotal: '',
+ hexGasTotal: '',
+ nonce: '',
+ fetchingMethodData: false,
+}
+
+// Reducer
+export default function reducer ({ confirmTransaction: confirmState = initState }, action = {}) {
+ switch (action.type) {
+ case UPDATE_TX_DATA:
+ return {
+ ...confirmState,
+ txData: {
+ ...action.payload,
+ },
+ }
+ case CLEAR_TX_DATA:
+ return {
+ ...confirmState,
+ txData: {},
+ }
+ case UPDATE_TOKEN_DATA:
+ return {
+ ...confirmState,
+ tokenData: {
+ ...action.payload,
+ },
+ }
+ case CLEAR_TOKEN_DATA:
+ return {
+ ...confirmState,
+ tokenData: {},
+ }
+ case UPDATE_METHOD_DATA:
+ return {
+ ...confirmState,
+ methodData: {
+ ...action.payload,
+ },
+ }
+ case CLEAR_METHOD_DATA:
+ return {
+ ...confirmState,
+ methodData: {},
+ }
+ case UPDATE_TRANSACTION_AMOUNTS:
+ const { fiatTransactionAmount, ethTransactionAmount } = action.payload
+ return {
+ ...confirmState,
+ fiatTransactionAmount: fiatTransactionAmount || confirmState.fiatTransactionAmount,
+ ethTransactionAmount: ethTransactionAmount || confirmState.ethTransactionAmount,
+ }
+ case UPDATE_TRANSACTION_FEES:
+ const { fiatTransactionFee, ethTransactionFee } = action.payload
+ return {
+ ...confirmState,
+ fiatTransactionFee: fiatTransactionFee || confirmState.fiatTransactionFee,
+ ethTransactionFee: ethTransactionFee || confirmState.ethTransactionFee,
+ }
+ case UPDATE_TRANSACTION_TOTALS:
+ const { fiatTransactionTotal, ethTransactionTotal } = action.payload
+ return {
+ ...confirmState,
+ fiatTransactionTotal: fiatTransactionTotal || confirmState.fiatTransactionTotal,
+ ethTransactionTotal: ethTransactionTotal || confirmState.ethTransactionTotal,
+ }
+ case UPDATE_HEX_GAS_TOTAL:
+ return {
+ ...confirmState,
+ hexGasTotal: action.payload,
+ }
+ case UPDATE_TOKEN_PROPS:
+ const { tokenSymbol = '', tokenDecimals = '' } = action.payload
+ return {
+ ...confirmState,
+ tokenProps: {
+ ...confirmState.tokenProps,
+ tokenSymbol,
+ tokenDecimals,
+ },
+ }
+ case UPDATE_NONCE:
+ return {
+ ...confirmState,
+ nonce: action.payload,
+ }
+ case FETCH_METHOD_DATA_START:
+ return {
+ ...confirmState,
+ fetchingMethodData: true,
+ }
+ case FETCH_METHOD_DATA_END:
+ return {
+ ...confirmState,
+ fetchingMethodData: false,
+ }
+ case CLEAR_CONFIRM_TRANSACTION:
+ return initState
+ default:
+ return confirmState
+ }
+}
+
+// Action Creators
+export function updateTxData (txData) {
+ return {
+ type: UPDATE_TX_DATA,
+ payload: txData,
+ }
+}
+
+export function clearTxData () {
+ return {
+ type: CLEAR_TX_DATA,
+ }
+}
+
+export function updateTokenData (tokenData) {
+ return {
+ type: UPDATE_TOKEN_DATA,
+ payload: tokenData,
+ }
+}
+
+export function clearTokenData () {
+ return {
+ type: CLEAR_TOKEN_DATA,
+ }
+}
+
+export function updateMethodData (methodData) {
+ return {
+ type: UPDATE_METHOD_DATA,
+ payload: methodData,
+ }
+}
+
+export function clearMethodData () {
+ return {
+ type: CLEAR_METHOD_DATA,
+ }
+}
+
+export function updateTransactionAmounts (amounts) {
+ return {
+ type: UPDATE_TRANSACTION_AMOUNTS,
+ payload: amounts,
+ }
+}
+
+export function updateTransactionFees (fees) {
+ return {
+ type: UPDATE_TRANSACTION_FEES,
+ payload: fees,
+ }
+}
+
+export function updateTransactionTotals (totals) {
+ return {
+ type: UPDATE_TRANSACTION_TOTALS,
+ payload: totals,
+ }
+}
+
+export function updateHexGasTotal (hexGasTotal) {
+ return {
+ type: UPDATE_HEX_GAS_TOTAL,
+ payload: hexGasTotal,
+ }
+}
+
+export function updateTokenProps (tokenProps) {
+ return {
+ type: UPDATE_TOKEN_PROPS,
+ payload: tokenProps,
+ }
+}
+
+export function updateNonce (nonce) {
+ return {
+ type: UPDATE_NONCE,
+ payload: nonce,
+ }
+}
+
+export function setFetchingMethodData (isFetching) {
+ return {
+ type: isFetching ? FETCH_METHOD_DATA_START : FETCH_METHOD_DATA_END,
+ }
+}
+
+export function updateGasAndCalculate ({ gasLimit, gasPrice }) {
+ return (dispatch, getState) => {
+ const { confirmTransaction: { txData } } = getState()
+ const newTxData = {
+ ...txData,
+ txParams: {
+ ...txData.txParams,
+ gas: gasLimit,
+ gasPrice,
+ },
+ }
+
+ dispatch(updateTxDataAndCalculate(newTxData))
+ }
+}
+
+function increaseFromLastGasPrice (txData) {
+ const { lastGasPrice, txParams: { gasPrice: previousGasPrice } = {} } = txData
+
+ // Set the minimum to a 10% increase from the lastGasPrice.
+ const minimumGasPrice = increaseLastGasPrice(lastGasPrice)
+ const gasPriceBelowMinimum = hexGreaterThan(minimumGasPrice, previousGasPrice)
+ const gasPrice = (!previousGasPrice || gasPriceBelowMinimum) ? minimumGasPrice : previousGasPrice
+
+ return {
+ ...txData,
+ txParams: {
+ ...txData.txParams,
+ gasPrice,
+ },
+ }
+}
+
+export function updateTxDataAndCalculate (txData) {
+ return (dispatch, getState) => {
+ const state = getState()
+ const currentCurrency = currentCurrencySelector(state)
+ const conversionRate = conversionRateSelector(state)
+
+ dispatch(updateTxData(txData))
+
+ const { txParams: { value, gas: gasLimit = '0x0', gasPrice = '0x0' } = {} } = txData
+
+ const fiatTransactionAmount = getTransactionAmount({
+ value, toCurrency: currentCurrency, conversionRate, numberOfDecimals: 2,
+ })
+ const ethTransactionAmount = getTransactionAmount({
+ value, toCurrency: 'ETH', conversionRate, numberOfDecimals: 6,
+ })
+
+ dispatch(updateTransactionAmounts({ fiatTransactionAmount, ethTransactionAmount }))
+
+ const hexGasTotal = getHexGasTotal({ gasLimit, gasPrice })
+
+ dispatch(updateHexGasTotal(hexGasTotal))
+
+ const fiatTransactionFee = getTransactionFee({
+ value: hexGasTotal,
+ toCurrency: currentCurrency,
+ numberOfDecimals: 2,
+ conversionRate,
+ })
+ const ethTransactionFee = getTransactionFee({
+ value: hexGasTotal,
+ toCurrency: 'ETH',
+ numberOfDecimals: 6,
+ conversionRate,
+ })
+
+ dispatch(updateTransactionFees({ fiatTransactionFee, ethTransactionFee }))
+
+ const fiatTransactionTotal = addFiat(fiatTransactionFee, fiatTransactionAmount)
+ const ethTransactionTotal = addEth(ethTransactionFee, ethTransactionAmount)
+
+ dispatch(updateTransactionTotals({ fiatTransactionTotal, ethTransactionTotal }))
+ }
+}
+
+export function setTransactionToConfirm (transactionId) {
+ return async (dispatch, getState) => {
+ const state = getState()
+ const unconfirmedTransactionsHash = unconfirmedTransactionsHashSelector(state)
+ const transaction = unconfirmedTransactionsHash[transactionId]
+
+ if (!transaction) {
+ console.error(`Transaction with id ${transactionId} not found`)
+ return
+ }
+
+ if (transaction.txParams) {
+ const { lastGasPrice } = transaction
+ const txData = lastGasPrice ? increaseFromLastGasPrice(transaction) : transaction
+ dispatch(updateTxDataAndCalculate(txData))
+
+ const { txParams } = transaction
+
+ if (txParams.data) {
+ const { tokens: existingTokens } = state
+ const { data, to: tokenAddress } = txParams
+
+ try {
+ dispatch(setFetchingMethodData(true))
+ const methodData = await getMethodData(data)
+ dispatch(updateMethodData(methodData))
+ dispatch(setFetchingMethodData(false))
+ } catch (error) {
+ dispatch(updateMethodData({}))
+ dispatch(setFetchingMethodData(false))
+ }
+
+ const tokenData = getTokenData(data)
+ dispatch(updateTokenData(tokenData))
+
+ try {
+ const tokenSymbolData = await getSymbolAndDecimals(tokenAddress, existingTokens) || {}
+ const { symbol: tokenSymbol = '', decimals: tokenDecimals = '' } = tokenSymbolData
+ dispatch(updateTokenProps({ tokenSymbol, tokenDecimals }))
+ } catch (error) {
+ dispatch(updateTokenProps({ tokenSymbol: '', tokenDecimals: '' }))
+ }
+ }
+
+ if (txParams.nonce) {
+ const nonce = conversionUtil(txParams.nonce, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ })
+
+ dispatch(updateNonce(nonce))
+ }
+ } else {
+ dispatch(updateTxData(transaction))
+ }
+ }
+}
+
+export function clearConfirmTransaction () {
+ return {
+ type: CLEAR_CONFIRM_TRANSACTION,
+ }
+}
diff --git a/ui/app/ducks/send.duck.js b/ui/app/ducks/send.duck.js
index 055cc05c1..db01bbaa9 100644
--- a/ui/app/ducks/send.duck.js
+++ b/ui/app/ducks/send.duck.js
@@ -6,6 +6,7 @@ const CLOSE_FROM_DROPDOWN = 'metamask/send/CLOSE_FROM_DROPDOWN'
const OPEN_TO_DROPDOWN = 'metamask/send/OPEN_TO_DROPDOWN'
const CLOSE_TO_DROPDOWN = 'metamask/send/CLOSE_TO_DROPDOWN'
const UPDATE_SEND_ERRORS = 'metamask/send/UPDATE_SEND_ERRORS'
+const RESET_SEND_STATE = 'metamask/send/RESET_SEND_STATE'
// TODO: determine if this approach to initState is consistent with conventional ducks pattern
const initState = {
@@ -42,6 +43,8 @@ export default function reducer ({ send: sendState = initState }, action = {}) {
...action.value,
},
})
+ case RESET_SEND_STATE:
+ return extend({}, initState)
default:
return newState
}
@@ -70,3 +73,7 @@ export function updateSendErrors (errorObject) {
value: errorObject,
}
}
+
+export function resetSendState () {
+ return { type: RESET_SEND_STATE }
+}
diff --git a/ui/app/ducks/tests/confirm-transaction.duck.test.js b/ui/app/ducks/tests/confirm-transaction.duck.test.js
new file mode 100644
index 000000000..111674e33
--- /dev/null
+++ b/ui/app/ducks/tests/confirm-transaction.duck.test.js
@@ -0,0 +1,675 @@
+import assert from 'assert'
+import configureMockStore from 'redux-mock-store'
+import thunk from 'redux-thunk'
+
+import ConfirmTransactionReducer, * as actions from '../confirm-transaction.duck.js'
+
+const initialState = {
+ txData: {},
+ tokenData: {},
+ methodData: {},
+ tokenProps: {
+ tokenDecimals: '',
+ tokenSymbol: '',
+ },
+ fiatTransactionAmount: '',
+ fiatTransactionFee: '',
+ fiatTransactionTotal: '',
+ ethTransactionAmount: '',
+ ethTransactionFee: '',
+ ethTransactionTotal: '',
+ hexGasTotal: '',
+ nonce: '',
+ fetchingMethodData: false,
+}
+
+const UPDATE_TX_DATA = 'metamask/confirm-transaction/UPDATE_TX_DATA'
+const CLEAR_TX_DATA = 'metamask/confirm-transaction/CLEAR_TX_DATA'
+const UPDATE_TOKEN_DATA = 'metamask/confirm-transaction/UPDATE_TOKEN_DATA'
+const CLEAR_TOKEN_DATA = 'metamask/confirm-transaction/CLEAR_TOKEN_DATA'
+const UPDATE_METHOD_DATA = 'metamask/confirm-transaction/UPDATE_METHOD_DATA'
+const CLEAR_METHOD_DATA = 'metamask/confirm-transaction/CLEAR_METHOD_DATA'
+const UPDATE_TRANSACTION_AMOUNTS = 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS'
+const UPDATE_TRANSACTION_FEES = 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES'
+const UPDATE_TRANSACTION_TOTALS = 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS'
+const UPDATE_HEX_GAS_TOTAL = 'metamask/confirm-transaction/UPDATE_HEX_GAS_TOTAL'
+const UPDATE_TOKEN_PROPS = 'metamask/confirm-transaction/UPDATE_TOKEN_PROPS'
+const UPDATE_NONCE = 'metamask/confirm-transaction/UPDATE_NONCE'
+const FETCH_METHOD_DATA_START = 'metamask/confirm-transaction/FETCH_METHOD_DATA_START'
+const FETCH_METHOD_DATA_END = 'metamask/confirm-transaction/FETCH_METHOD_DATA_END'
+const CLEAR_CONFIRM_TRANSACTION = 'metamask/confirm-transaction/CLEAR_CONFIRM_TRANSACTION'
+
+describe('Confirm Transaction Duck', () => {
+ describe('State changes', () => {
+ const mockState = {
+ confirmTransaction: {
+ txData: {
+ id: 1,
+ },
+ tokenData: {
+ name: 'abcToken',
+ },
+ methodData: {
+ name: 'approve',
+ },
+ tokenProps: {
+ tokenDecimals: '3',
+ tokenSymbol: 'ABC',
+ },
+ fiatTransactionAmount: '469.26',
+ fiatTransactionFee: '0.01',
+ fiatTransactionTotal: '1.000021',
+ ethTransactionAmount: '1',
+ ethTransactionFee: '0.000021',
+ ethTransactionTotal: '469.27',
+ hexGasTotal: '0x1319718a5000',
+ nonce: '0x0',
+ fetchingMethodData: false,
+ },
+ }
+
+ it('should initialize state', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer({}),
+ initialState
+ )
+ })
+
+ it('should return state unchanged if it does not match a dispatched actions type', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: 'someOtherAction',
+ value: 'someValue',
+ }),
+ { ...mockState.confirmTransaction },
+ )
+ })
+
+ it('should set txData when receiving a UPDATE_TX_DATA action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: UPDATE_TX_DATA,
+ payload: {
+ id: 2,
+ },
+ }),
+ {
+ ...mockState.confirmTransaction,
+ txData: {
+ ...mockState.confirmTransaction.txData,
+ id: 2,
+ },
+ }
+ )
+ })
+
+ it('should clear txData when receiving a CLEAR_TX_DATA action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: CLEAR_TX_DATA,
+ }),
+ {
+ ...mockState.confirmTransaction,
+ txData: {},
+ }
+ )
+ })
+
+ it('should set tokenData when receiving a UPDATE_TOKEN_DATA action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: UPDATE_TOKEN_DATA,
+ payload: {
+ name: 'defToken',
+ },
+ }),
+ {
+ ...mockState.confirmTransaction,
+ tokenData: {
+ ...mockState.confirmTransaction.tokenData,
+ name: 'defToken',
+ },
+ }
+ )
+ })
+
+ it('should clear tokenData when receiving a CLEAR_TOKEN_DATA action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: CLEAR_TOKEN_DATA,
+ }),
+ {
+ ...mockState.confirmTransaction,
+ tokenData: {},
+ }
+ )
+ })
+
+ it('should set methodData when receiving a UPDATE_METHOD_DATA action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: UPDATE_METHOD_DATA,
+ payload: {
+ name: 'transferFrom',
+ },
+ }),
+ {
+ ...mockState.confirmTransaction,
+ methodData: {
+ ...mockState.confirmTransaction.methodData,
+ name: 'transferFrom',
+ },
+ }
+ )
+ })
+
+ it('should clear methodData when receiving a CLEAR_METHOD_DATA action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: CLEAR_METHOD_DATA,
+ }),
+ {
+ ...mockState.confirmTransaction,
+ methodData: {},
+ }
+ )
+ })
+
+ it('should update transaction amounts when receiving an UPDATE_TRANSACTION_AMOUNTS action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: UPDATE_TRANSACTION_AMOUNTS,
+ payload: {
+ fiatTransactionAmount: '123.45',
+ ethTransactionAmount: '.5',
+ },
+ }),
+ {
+ ...mockState.confirmTransaction,
+ fiatTransactionAmount: '123.45',
+ ethTransactionAmount: '.5',
+ }
+ )
+ })
+
+ it('should update transaction fees when receiving an UPDATE_TRANSACTION_FEES action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: UPDATE_TRANSACTION_FEES,
+ payload: {
+ fiatTransactionFee: '123.45',
+ ethTransactionFee: '.5',
+ },
+ }),
+ {
+ ...mockState.confirmTransaction,
+ fiatTransactionFee: '123.45',
+ ethTransactionFee: '.5',
+ }
+ )
+ })
+
+ it('should update transaction totals when receiving an UPDATE_TRANSACTION_TOTALS action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: UPDATE_TRANSACTION_TOTALS,
+ payload: {
+ fiatTransactionTotal: '123.45',
+ ethTransactionTotal: '.5',
+ },
+ }),
+ {
+ ...mockState.confirmTransaction,
+ fiatTransactionTotal: '123.45',
+ ethTransactionTotal: '.5',
+ }
+ )
+ })
+
+ it('should update hexGasTotal when receiving an UPDATE_HEX_GAS_TOTAL action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: UPDATE_HEX_GAS_TOTAL,
+ payload: '0x0',
+ }),
+ {
+ ...mockState.confirmTransaction,
+ hexGasTotal: '0x0',
+ }
+ )
+ })
+
+ it('should update tokenProps when receiving an UPDATE_TOKEN_PROPS action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: UPDATE_TOKEN_PROPS,
+ payload: {
+ tokenSymbol: 'DEF',
+ tokenDecimals: '1',
+ },
+ }),
+ {
+ ...mockState.confirmTransaction,
+ tokenProps: {
+ tokenSymbol: 'DEF',
+ tokenDecimals: '1',
+ },
+ }
+ )
+ })
+
+ it('should update nonce when receiving an UPDATE_NONCE action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: UPDATE_NONCE,
+ payload: '0x1',
+ }),
+ {
+ ...mockState.confirmTransaction,
+ nonce: '0x1',
+ }
+ )
+ })
+
+ it('should set fetchingMethodData to true when receiving a FETCH_METHOD_DATA_START action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: FETCH_METHOD_DATA_START,
+ }),
+ {
+ ...mockState.confirmTransaction,
+ fetchingMethodData: true,
+ }
+ )
+ })
+
+ it('should set fetchingMethodData to false when receiving a FETCH_METHOD_DATA_END action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer({ confirmTransaction: { fetchingMethodData: true } }, {
+ type: FETCH_METHOD_DATA_END,
+ }),
+ {
+ fetchingMethodData: false,
+ }
+ )
+ })
+
+ it('should clear confirmTransaction when receiving a FETCH_METHOD_DATA_END action', () => {
+ assert.deepEqual(
+ ConfirmTransactionReducer(mockState, {
+ type: CLEAR_CONFIRM_TRANSACTION,
+ }),
+ {
+ ...initialState,
+ }
+ )
+ })
+ })
+
+ describe('Single actions', () => {
+ it('should create an action to update txData', () => {
+ const txData = { test: 123 }
+ const expectedAction = {
+ type: UPDATE_TX_DATA,
+ payload: txData,
+ }
+
+ assert.deepEqual(
+ actions.updateTxData(txData),
+ expectedAction
+ )
+ })
+
+ it('should create an action to clear txData', () => {
+ const expectedAction = {
+ type: CLEAR_TX_DATA,
+ }
+
+ assert.deepEqual(
+ actions.clearTxData(),
+ expectedAction
+ )
+ })
+
+ it('should create an action to update tokenData', () => {
+ const tokenData = { test: 123 }
+ const expectedAction = {
+ type: UPDATE_TOKEN_DATA,
+ payload: tokenData,
+ }
+
+ assert.deepEqual(
+ actions.updateTokenData(tokenData),
+ expectedAction
+ )
+ })
+
+ it('should create an action to clear tokenData', () => {
+ const expectedAction = {
+ type: CLEAR_TOKEN_DATA,
+ }
+
+ assert.deepEqual(
+ actions.clearTokenData(),
+ expectedAction
+ )
+ })
+
+ it('should create an action to update methodData', () => {
+ const methodData = { test: 123 }
+ const expectedAction = {
+ type: UPDATE_METHOD_DATA,
+ payload: methodData,
+ }
+
+ assert.deepEqual(
+ actions.updateMethodData(methodData),
+ expectedAction
+ )
+ })
+
+ it('should create an action to clear methodData', () => {
+ const expectedAction = {
+ type: CLEAR_METHOD_DATA,
+ }
+
+ assert.deepEqual(
+ actions.clearMethodData(),
+ expectedAction
+ )
+ })
+
+ it('should create an action to update transaction amounts', () => {
+ const transactionAmounts = { test: 123 }
+ const expectedAction = {
+ type: UPDATE_TRANSACTION_AMOUNTS,
+ payload: transactionAmounts,
+ }
+
+ assert.deepEqual(
+ actions.updateTransactionAmounts(transactionAmounts),
+ expectedAction
+ )
+ })
+
+ it('should create an action to update transaction fees', () => {
+ const transactionFees = { test: 123 }
+ const expectedAction = {
+ type: UPDATE_TRANSACTION_FEES,
+ payload: transactionFees,
+ }
+
+ assert.deepEqual(
+ actions.updateTransactionFees(transactionFees),
+ expectedAction
+ )
+ })
+
+ it('should create an action to update transaction totals', () => {
+ const transactionTotals = { test: 123 }
+ const expectedAction = {
+ type: UPDATE_TRANSACTION_TOTALS,
+ payload: transactionTotals,
+ }
+
+ assert.deepEqual(
+ actions.updateTransactionTotals(transactionTotals),
+ expectedAction
+ )
+ })
+
+ it('should create an action to update hexGasTotal', () => {
+ const hexGasTotal = '0x0'
+ const expectedAction = {
+ type: UPDATE_HEX_GAS_TOTAL,
+ payload: hexGasTotal,
+ }
+
+ assert.deepEqual(
+ actions.updateHexGasTotal(hexGasTotal),
+ expectedAction
+ )
+ })
+
+ it('should create an action to update tokenProps', () => {
+ const tokenProps = {
+ tokenDecimals: '1',
+ tokenSymbol: 'abc',
+ }
+ const expectedAction = {
+ type: UPDATE_TOKEN_PROPS,
+ payload: tokenProps,
+ }
+
+ assert.deepEqual(
+ actions.updateTokenProps(tokenProps),
+ expectedAction
+ )
+ })
+
+ it('should create an action to update nonce', () => {
+ const nonce = '0x1'
+ const expectedAction = {
+ type: UPDATE_NONCE,
+ payload: nonce,
+ }
+
+ assert.deepEqual(
+ actions.updateNonce(nonce),
+ expectedAction
+ )
+ })
+
+ it('should create an action to set fetchingMethodData to true', () => {
+ const expectedAction = {
+ type: FETCH_METHOD_DATA_START,
+ }
+
+ assert.deepEqual(
+ actions.setFetchingMethodData(true),
+ expectedAction
+ )
+ })
+
+ it('should create an action to set fetchingMethodData to false', () => {
+ const expectedAction = {
+ type: FETCH_METHOD_DATA_END,
+ }
+
+ assert.deepEqual(
+ actions.setFetchingMethodData(false),
+ expectedAction
+ )
+ })
+
+ it('should create an action to clear confirmTransaction', () => {
+ const expectedAction = {
+ type: CLEAR_CONFIRM_TRANSACTION,
+ }
+
+ assert.deepEqual(
+ actions.clearConfirmTransaction(),
+ expectedAction
+ )
+ })
+ })
+
+ describe('Thunk actions', done => {
+ it('updates txData and gas on an existing transaction in confirmTransaction', () => {
+ const mockState = {
+ metamask: {
+ conversionRate: 468.58,
+ currentCurrency: 'usd',
+ },
+ confirmTransaction: {
+ ethTransactionAmount: '1',
+ ethTransactionFee: '0.000021',
+ ethTransactionTotal: '1.000021',
+ fetchingMethodData: false,
+ fiatTransactionAmount: '469.26',
+ fiatTransactionFee: '0.01',
+ fiatTransactionTotal: '469.27',
+ hexGasTotal: '0x1319718a5000',
+ methodData: {},
+ nonce: '',
+ tokenData: {},
+ tokenProps: {
+ tokenDecimals: '',
+ tokenSymbol: '',
+ },
+ txData: {
+ estimatedGas: '0x5208',
+ gasLimitSpecified: false,
+ gasPriceSpecified: false,
+ history: [],
+ id: 2603411941761054,
+ loadingDefaults: false,
+ metamaskNetworkId: '3',
+ origin: 'faucet.metamask.io',
+ simpleSend: true,
+ status: 'unapproved',
+ time: 1530838113716,
+ },
+ },
+ }
+
+ const middlewares = [thunk]
+ const mockStore = configureMockStore(middlewares)
+ const store = mockStore(mockState)
+ const expectedActions = [
+ 'metamask/confirm-transaction/UPDATE_TX_DATA',
+ 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS',
+ 'metamask/confirm-transaction/UPDATE_HEX_GAS_TOTAL',
+ 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES',
+ 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS',
+ ]
+
+ store.dispatch(actions.updateGasAndCalculate({ gasLimit: '0x2', gasPrice: '0x25' }))
+
+ const storeActions = store.getActions()
+ assert.equal(storeActions.length, expectedActions.length)
+ storeActions.forEach((action, index) => assert.equal(action.type, expectedActions[index]))
+ })
+
+ it('updates txData and updates gas values in confirmTransaction', () => {
+ const txData = {
+ estimatedGas: '0x5208',
+ gasLimitSpecified: false,
+ gasPriceSpecified: false,
+ history: [],
+ id: 2603411941761054,
+ loadingDefaults: false,
+ metamaskNetworkId: '3',
+ origin: 'faucet.metamask.io',
+ simpleSend: true,
+ status: 'unapproved',
+ time: 1530838113716,
+ txParams: {
+ from: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
+ gas: '0x33450',
+ gasPrice: '0x2540be400',
+ to: '0x81b7e08f65bdf5648606c89998a9cc8164397647',
+ value: '0xde0b6b3a7640000',
+ },
+ }
+ const mockState = {
+ metamask: {
+ conversionRate: 468.58,
+ currentCurrency: 'usd',
+ },
+ confirmTransaction: {
+ ethTransactionAmount: '1',
+ ethTransactionFee: '0.000021',
+ ethTransactionTotal: '1.000021',
+ fetchingMethodData: false,
+ fiatTransactionAmount: '469.26',
+ fiatTransactionFee: '0.01',
+ fiatTransactionTotal: '469.27',
+ hexGasTotal: '0x1319718a5000',
+ methodData: {},
+ nonce: '',
+ tokenData: {},
+ tokenProps: {
+ tokenDecimals: '',
+ tokenSymbol: '',
+ },
+ txData: {
+ ...txData,
+ txParams: {
+ ...txData.txParams,
+ },
+ },
+ },
+ }
+
+ const middlewares = [thunk]
+ const mockStore = configureMockStore(middlewares)
+ const store = mockStore(mockState)
+ const expectedActions = [
+ 'metamask/confirm-transaction/UPDATE_TX_DATA',
+ 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS',
+ 'metamask/confirm-transaction/UPDATE_HEX_GAS_TOTAL',
+ 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES',
+ 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS',
+ ]
+
+ store.dispatch(actions.updateTxDataAndCalculate(txData))
+
+ const storeActions = store.getActions()
+ assert.equal(storeActions.length, expectedActions.length)
+ storeActions.forEach((action, index) => assert.equal(action.type, expectedActions[index]))
+ })
+
+ it('updates confirmTransaction transaction', done => {
+ const mockState = {
+ metamask: {
+ conversionRate: 468.58,
+ currentCurrency: 'usd',
+ network: '3',
+ unapprovedTxs: {
+ 2603411941761054: {
+ estimatedGas: '0x5208',
+ gasLimitSpecified: false,
+ gasPriceSpecified: false,
+ history: [],
+ id: 2603411941761054,
+ loadingDefaults: false,
+ metamaskNetworkId: '3',
+ origin: 'faucet.metamask.io',
+ simpleSend: true,
+ status: 'unapproved',
+ time: 1530838113716,
+ txParams: {
+ from: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
+ gas: '0x33450',
+ gasPrice: '0x2540be400',
+ to: '0x81b7e08f65bdf5648606c89998a9cc8164397647',
+ value: '0xde0b6b3a7640000',
+ },
+ },
+ },
+ },
+ confirmTransaction: {},
+ }
+
+ const middlewares = [thunk]
+ const mockStore = configureMockStore(middlewares)
+ const store = mockStore(mockState)
+ const expectedActions = [
+ 'metamask/confirm-transaction/UPDATE_TX_DATA',
+ 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS',
+ 'metamask/confirm-transaction/UPDATE_HEX_GAS_TOTAL',
+ 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES',
+ 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS',
+ ]
+
+ store.dispatch(actions.setTransactionToConfirm(2603411941761054))
+ .then(() => {
+ const storeActions = store.getActions()
+ assert.equal(storeActions.length, expectedActions.length)
+ storeActions.forEach((action, index) => assert.equal(action.type, expectedActions[index]))
+ done()
+ })
+ })
+ })
+})
diff --git a/ui/app/ducks/tests/send-duck.test.js b/ui/app/ducks/tests/send-duck.test.js
index c06cf55d2..c101132d9 100644
--- a/ui/app/ducks/tests/send-duck.test.js
+++ b/ui/app/ducks/tests/send-duck.test.js
@@ -24,6 +24,7 @@ describe('Send Duck', () => {
const OPEN_TO_DROPDOWN = 'metamask/send/OPEN_TO_DROPDOWN'
const CLOSE_TO_DROPDOWN = 'metamask/send/CLOSE_TO_DROPDOWN'
const UPDATE_SEND_ERRORS = 'metamask/send/UPDATE_SEND_ERRORS'
+ const RESET_SEND_STATE = 'metamask/send/RESET_SEND_STATE'
describe('SendReducer()', () => {
it('should initialize state', () => {
@@ -105,6 +106,15 @@ describe('Send Duck', () => {
})
)
})
+
+ it('should return the initial state in response to a RESET_SEND_STATE action', () => {
+ assert.deepEqual(
+ SendReducer(mockState, {
+ type: RESET_SEND_STATE,
+ }),
+ Object.assign({}, initState)
+ )
+ })
})
describe('openFromDropdown', () => {
diff --git a/ui/app/helpers/confirm-transaction/util.js b/ui/app/helpers/confirm-transaction/util.js
new file mode 100644
index 000000000..f015b2bf5
--- /dev/null
+++ b/ui/app/helpers/confirm-transaction/util.js
@@ -0,0 +1,139 @@
+import currencyFormatter from 'currency-formatter'
+import currencies from 'currency-formatter/currencies'
+import abi from 'human-standard-token-abi'
+import abiDecoder from 'abi-decoder'
+import ethUtil from 'ethereumjs-util'
+
+abiDecoder.addABI(abi)
+
+import MethodRegistry from 'eth-method-registry'
+const registry = new MethodRegistry({ provider: global.ethereumProvider })
+
+import {
+ conversionUtil,
+ addCurrencies,
+ multiplyCurrencies,
+ conversionGreaterThan,
+} from '../../conversion-util'
+
+import { unconfirmedTransactionsCountSelector } from '../../selectors/confirm-transaction'
+
+export function getTokenData (data = {}) {
+ return abiDecoder.decodeMethod(data)
+}
+
+export async function getMethodData (data = {}) {
+ const prefixedData = ethUtil.addHexPrefix(data)
+ const fourBytePrefix = prefixedData.slice(0, 10)
+ const sig = await registry.lookup(fourBytePrefix)
+ const parsedResult = registry.parse(sig)
+
+ return {
+ name: parsedResult.name,
+ params: parsedResult.args,
+ }
+}
+
+export function increaseLastGasPrice (lastGasPrice) {
+ return ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, {
+ multiplicandBase: 16,
+ multiplierBase: 10,
+ toNumericBase: 'hex',
+ }))
+}
+
+export function hexGreaterThan (a, b) {
+ return conversionGreaterThan(
+ { value: a, fromNumericBase: 'hex' },
+ { value: b, fromNumericBase: 'hex' },
+ )
+}
+
+export function getHexGasTotal ({ gasLimit, gasPrice }) {
+ return ethUtil.addHexPrefix(multiplyCurrencies(gasLimit, gasPrice, {
+ toNumericBase: 'hex',
+ multiplicandBase: 16,
+ multiplierBase: 16,
+ }))
+}
+
+export function addEth (...args) {
+ return args.reduce((acc, base) => {
+ return addCurrencies(acc, base, {
+ toNumericBase: 'dec',
+ numberOfDecimals: 6,
+ })
+ })
+}
+
+export function addFiat (...args) {
+ return args.reduce((acc, base) => {
+ return addCurrencies(acc, base, {
+ toNumericBase: 'dec',
+ numberOfDecimals: 2,
+ })
+ })
+}
+
+export function getTransactionAmount ({
+ value,
+ toCurrency,
+ conversionRate,
+ numberOfDecimals,
+}) {
+ return conversionUtil(value, {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ fromCurrency: 'ETH',
+ toCurrency,
+ numberOfDecimals,
+ fromDenomination: 'WEI',
+ conversionRate,
+ })
+}
+
+export function getTransactionFee ({
+ value,
+ toCurrency,
+ conversionRate,
+ numberOfDecimals,
+}) {
+ return conversionUtil(value, {
+ fromNumericBase: 'BN',
+ toNumericBase: 'dec',
+ fromDenomination: 'WEI',
+ fromCurrency: 'ETH',
+ toCurrency,
+ numberOfDecimals,
+ conversionRate,
+ })
+}
+
+export function formatCurrency (value, currencyCode) {
+ const upperCaseCurrencyCode = currencyCode.toUpperCase()
+
+ return currencies.find(currency => currency.code === upperCaseCurrencyCode)
+ ? currencyFormatter.format(Number(value), { code: upperCaseCurrencyCode })
+ : value
+}
+
+export function convertTokenToFiat ({
+ value,
+ toCurrency,
+ conversionRate,
+ contractExchangeRate,
+}) {
+ const totalExchangeRate = conversionRate * contractExchangeRate
+
+ return conversionUtil(value, {
+ fromNumericBase: 'dec',
+ toNumericBase: 'dec',
+ toCurrency,
+ numberOfDecimals: 2,
+ conversionRate: totalExchangeRate,
+ })
+}
+
+export function hasUnconfirmedTransactions (state) {
+ return unconfirmedTransactionsCountSelector(state) > 0
+}
diff --git a/ui/app/helpers/confirm-transaction/util.test.js b/ui/app/helpers/confirm-transaction/util.test.js
new file mode 100644
index 000000000..a9c8fae34
--- /dev/null
+++ b/ui/app/helpers/confirm-transaction/util.test.js
@@ -0,0 +1,137 @@
+import * as utils from './util'
+import assert from 'assert'
+
+describe('Confirm Transaction utils', () => {
+ describe('increaseLastGasPrice', () => {
+ it('should increase the gasPrice by 10%', () => {
+ const increasedGasPrice = utils.increaseLastGasPrice('0xa')
+ assert.equal(increasedGasPrice, '0xb')
+ })
+
+ it('should prefix the result with 0x', () => {
+ const increasedGasPrice = utils.increaseLastGasPrice('a')
+ assert.equal(increasedGasPrice, '0xb')
+ })
+ })
+
+ describe('hexGreaterThan', () => {
+ it('should return true if the first value is greater than the second value', () => {
+ assert.equal(
+ utils.hexGreaterThan('0xb', '0xa'),
+ true
+ )
+ })
+
+ it('should return false if the first value is less than the second value', () => {
+ assert.equal(
+ utils.hexGreaterThan('0xa', '0xb'),
+ false
+ )
+ })
+
+ it('should return false if the first value is equal to the second value', () => {
+ assert.equal(
+ utils.hexGreaterThan('0xa', '0xa'),
+ false
+ )
+ })
+
+ it('should correctly compare prefixed and non-prefixed hex values', () => {
+ assert.equal(
+ utils.hexGreaterThan('0xb', 'a'),
+ true
+ )
+ })
+ })
+
+ describe('getHexGasTotal', () => {
+ it('should multiply the hex gasLimit and hex gasPrice values together', () => {
+ assert.equal(
+ utils.getHexGasTotal({ gasLimit: '0x5208', gasPrice: '0x3b9aca00' }),
+ '0x1319718a5000'
+ )
+ })
+
+ it('should prefix the result with 0x', () => {
+ assert.equal(
+ utils.getHexGasTotal({ gasLimit: '5208', gasPrice: '3b9aca00' }),
+ '0x1319718a5000'
+ )
+ })
+ })
+
+ describe('addEth', () => {
+ it('should add two values together rounding to 6 decimal places', () => {
+ assert.equal(
+ utils.addEth('0.12345678', '0'),
+ '0.123457'
+ )
+ })
+
+ it('should add any number of values together rounding to 6 decimal places', () => {
+ assert.equal(
+ utils.addEth('0.1', '0.02', '0.003', '0.0004', '0.00005', '0.000006', '0.0000007'),
+ '0.123457'
+ )
+ })
+ })
+
+ describe('addFiat', () => {
+ it('should add two values together rounding to 2 decimal places', () => {
+ assert.equal(
+ utils.addFiat('0.12345678', '0'),
+ '0.12'
+ )
+ })
+
+ it('should add any number of values together rounding to 2 decimal places', () => {
+ assert.equal(
+ utils.addFiat('0.1', '0.02', '0.003', '0.0004', '0.00005', '0.000006', '0.0000007'),
+ '0.12'
+ )
+ })
+ })
+
+ describe('getTransactionAmount', () => {
+ it('should get the transaction amount in ETH', () => {
+ const ethTransactionAmount = utils.getTransactionAmount({
+ value: '0xde0b6b3a7640000', toCurrency: 'ETH', conversionRate: 468.58, numberOfDecimals: 6,
+ })
+
+ assert.equal(ethTransactionAmount, '1')
+ })
+
+ it('should get the transaction amount in fiat', () => {
+ const fiatTransactionAmount = utils.getTransactionAmount({
+ value: '0xde0b6b3a7640000', toCurrency: 'usd', conversionRate: 468.58, numberOfDecimals: 2,
+ })
+
+ assert.equal(fiatTransactionAmount, '468.58')
+ })
+ })
+
+ describe('getTransactionFee', () => {
+ it('should get the transaction fee in ETH', () => {
+ const ethTransactionFee = utils.getTransactionFee({
+ value: '0x1319718a5000', toCurrency: 'ETH', conversionRate: 468.58, numberOfDecimals: 6,
+ })
+
+ assert.equal(ethTransactionFee, '0.000021')
+ })
+
+ it('should get the transaction fee in fiat', () => {
+ const fiatTransactionFee = utils.getTransactionFee({
+ value: '0x1319718a5000', toCurrency: 'usd', conversionRate: 468.58, numberOfDecimals: 2,
+ })
+
+ assert.equal(fiatTransactionFee, '0.01')
+ })
+ })
+
+ describe('formatCurrency', () => {
+ it('should format USD values', () => {
+ const value = utils.formatCurrency('123.45', 'usd')
+ assert.equal(value, '$123.45')
+ })
+ })
+})
diff --git a/ui/app/helpers/with-token-tracker.js b/ui/app/helpers/with-token-tracker.js
index e24517c18..8608b15f4 100644
--- a/ui/app/helpers/with-token-tracker.js
+++ b/ui/app/helpers/with-token-tracker.js
@@ -75,6 +75,9 @@ const withTokenTracker = WrappedComponent => {
}
updateBalance (tokens = []) {
+ if (!this.tracker.running) {
+ return
+ }
const [{ string, symbol }] = tokens
this.setState({ string, symbol, error: null })
}
diff --git a/ui/app/i18n-provider.js b/ui/app/i18n-provider.js
index 2856e0ed6..d46911f7c 100644
--- a/ui/app/i18n-provider.js
+++ b/ui/app/i18n-provider.js
@@ -8,8 +8,11 @@ const t = require('../i18n-helper').getMessage
class I18nProvider extends Component {
getChildContext () {
const { localeMessages } = this.props
+ const { current, en } = localeMessages
return {
- t: t.bind(null, localeMessages),
+ t (key, ...args) {
+ return t(current, key, ...args) || t(en, key, ...args) || `[${key}]`
+ },
}
}
diff --git a/ui/app/main-container.js b/ui/app/main-container.js
index b49a52363..8a0708025 100644
--- a/ui/app/main-container.js
+++ b/ui/app/main-container.js
@@ -3,9 +3,10 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits
const AccountAndTransactionDetails = require('./account-and-transaction-details')
const Settings = require('./components/pages/settings')
-const UnlockScreen = require('./components/pages/unlock-page')
const log = require('loglevel')
+import UnlockScreen from './components/pages/unlock-page'
+
module.exports = MainContainer
inherits(MainContainer, Component)
diff --git a/ui/app/metamask-connect.js b/ui/app/metamask-connect.js
deleted file mode 100644
index 81fa7e403..000000000
--- a/ui/app/metamask-connect.js
+++ /dev/null
@@ -1,27 +0,0 @@
-const connect = require('react-redux').connect
-const t = require('../i18n-helper').getMessage
-
-const metamaskConnect = (mapStateToProps, mapDispatchToProps) => {
- return connect(
- _higherOrderMapStateToProps(mapStateToProps),
- mapDispatchToProps
- )
-}
-
-const _higherOrderMapStateToProps = (mapStateToProps) => {
- let _t
- let currentLocale
- return (state, ownProps = {}) => {
- const stateProps = mapStateToProps
- ? mapStateToProps(state, ownProps)
- : ownProps
- if (currentLocale !== state.metamask.currentLocale) {
- currentLocale = state.metamask.currentLocale
- _t = t.bind(null, state.localeMessages)
- }
- stateProps.t = _t
- return stateProps
- }
-}
-
-module.exports = metamaskConnect
diff --git a/ui/app/reducers.js b/ui/app/reducers.js
index 0b158a778..80e76d570 100644
--- a/ui/app/reducers.js
+++ b/ui/app/reducers.js
@@ -8,6 +8,7 @@ const reduceMetamask = require('./reducers/metamask')
const reduceApp = require('./reducers/app')
const reduceLocale = require('./reducers/locale')
const reduceSend = require('./ducks/send.duck').default
+import reduceConfirmTransaction from './ducks/confirm-transaction.duck'
window.METAMASK_CACHED_LOG_STATE = null
@@ -45,6 +46,8 @@ function rootReducer (state, action) {
state.send = reduceSend(state, action)
+ state.confirmTransaction = reduceConfirmTransaction(state, action)
+
window.METAMASK_CACHED_LOG_STATE = state
return state
}
diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js
index f453812b9..50d8bcba7 100644
--- a/ui/app/reducers/app.js
+++ b/ui/app/reducers/app.js
@@ -49,6 +49,8 @@ function reduceApp (state, action) {
},
},
sidebarOpen: false,
+ alertOpen: false,
+ alertMessage: null,
networkDropdownOpen: false,
currentView: seedWords ? seedConfView : defaultView,
accountDetail: {
@@ -88,6 +90,19 @@ function reduceApp (state, action) {
sidebarOpen: false,
})
+ // sidebar methods
+ case actions.ALERT_OPEN:
+ return extend(appState, {
+ alertOpen: true,
+ alertMessage: action.value,
+ })
+
+ case actions.ALERT_CLOSE:
+ return extend(appState, {
+ alertOpen: false,
+ alertMessage: null,
+ })
+
// modal methods:
case actions.MODAL_OPEN:
const { name, ...modalProps } = action.payload
diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js
index 6c8ac9ed7..3f1d3394f 100644
--- a/ui/app/reducers/metamask.js
+++ b/ui/app/reducers/metamask.js
@@ -222,6 +222,14 @@ function reduceMetamask (state, action) {
},
})
+ case actions.UPDATE_SEND_HEX_DATA:
+ return extend(metamaskState, {
+ send: {
+ ...metamaskState.send,
+ data: action.value,
+ },
+ })
+
case actions.UPDATE_SEND_FROM:
return extend(metamaskState, {
send: {
diff --git a/ui/app/routes.js b/ui/app/routes.js
index 0ff3f644d..f6b2a7a55 100644
--- a/ui/app/routes.js
+++ b/ui/app/routes.js
@@ -9,9 +9,8 @@ const ADD_TOKEN_ROUTE = '/add-token'
const CONFIRM_ADD_TOKEN_ROUTE = '/confirm-add-token'
const NEW_ACCOUNT_ROUTE = '/new-account'
const IMPORT_ACCOUNT_ROUTE = '/new-account/import'
+const CONNECT_HARDWARE_ROUTE = '/new-account/connect'
const SEND_ROUTE = '/send'
-const CONFIRM_TRANSACTION_ROUTE = '/confirm-transaction'
-const SIGNATURE_REQUEST_ROUTE = '/confirm-transaction/signature-request'
const NOTICE_ROUTE = '/notice'
const WELCOME_ROUTE = '/welcome'
const INITIALIZE_ROUTE = '/initialize'
@@ -23,6 +22,15 @@ const INITIALIZE_NOTICE_ROUTE = '/initialize/notice'
const INITIALIZE_BACKUP_PHRASE_ROUTE = '/initialize/backup-phrase'
const INITIALIZE_CONFIRM_SEED_ROUTE = '/initialize/confirm-phrase'
+const CONFIRM_TRANSACTION_ROUTE = '/confirm-transaction'
+const CONFIRM_SEND_ETHER_PATH = '/send-ether'
+const CONFIRM_SEND_TOKEN_PATH = '/send-token'
+const CONFIRM_DEPLOY_CONTRACT_PATH = '/deploy-contract'
+const CONFIRM_APPROVE_PATH = '/approve'
+const CONFIRM_TRANSFER_FROM_PATH = '/transfer-from'
+const CONFIRM_TOKEN_METHOD_PATH = '/token-method'
+const SIGNATURE_REQUEST_PATH = '/signature-request'
+
module.exports = {
DEFAULT_ROUTE,
UNLOCK_ROUTE,
@@ -35,10 +43,9 @@ module.exports = {
CONFIRM_ADD_TOKEN_ROUTE,
NEW_ACCOUNT_ROUTE,
IMPORT_ACCOUNT_ROUTE,
+ CONNECT_HARDWARE_ROUTE,
SEND_ROUTE,
- CONFIRM_TRANSACTION_ROUTE,
NOTICE_ROUTE,
- SIGNATURE_REQUEST_ROUTE,
WELCOME_ROUTE,
INITIALIZE_ROUTE,
INITIALIZE_CREATE_PASSWORD_ROUTE,
@@ -48,4 +55,12 @@ module.exports = {
INITIALIZE_NOTICE_ROUTE,
INITIALIZE_BACKUP_PHRASE_ROUTE,
INITIALIZE_CONFIRM_SEED_ROUTE,
+ CONFIRM_TRANSACTION_ROUTE,
+ CONFIRM_SEND_ETHER_PATH,
+ CONFIRM_SEND_TOKEN_PATH,
+ CONFIRM_DEPLOY_CONTRACT_PATH,
+ CONFIRM_APPROVE_PATH,
+ CONFIRM_TRANSFER_FROM_PATH,
+ CONFIRM_TOKEN_METHOD_PATH,
+ SIGNATURE_REQUEST_PATH,
}
diff --git a/ui/app/selectors.js b/ui/app/selectors.js
index cf0affe9c..d86462275 100644
--- a/ui/app/selectors.js
+++ b/ui/app/selectors.js
@@ -17,8 +17,6 @@ const selectors = {
accountsWithSendEtherInfoSelector,
getCurrentAccountWithSendEtherInfo,
getGasIsLoading,
- getGasPrice,
- getGasLimit,
getForceGasMin,
getAddressBook,
getSendFrom,
@@ -29,6 +27,7 @@ const selectors = {
autoAddToBetaUI,
getSendMaxModeState,
getCurrentViewContext,
+ getTotalUnapprovedCount,
}
module.exports = selectors
@@ -122,14 +121,6 @@ function getGasIsLoading (state) {
return state.appState.gasIsLoading
}
-function getGasPrice (state) {
- return state.metamask.send.gasPrice
-}
-
-function getGasLimit (state) {
- return state.metamask.send.gasLimit
-}
-
function getForceGasMin (state) {
return state.metamask.send.forceGasMin
}
@@ -191,3 +182,15 @@ function getCurrentViewContext (state) {
const { currentView = {} } = state.appState
return currentView.context
}
+
+function getTotalUnapprovedCount ({ metamask }) {
+ const {
+ unapprovedTxs = {},
+ unapprovedMsgCount,
+ unapprovedPersonalMsgCount,
+ unapprovedTypedMessagesCount,
+ } = metamask
+
+ return Object.keys(unapprovedTxs).length + unapprovedMsgCount + unapprovedPersonalMsgCount +
+ unapprovedTypedMessagesCount
+}
diff --git a/ui/app/selectors/confirm-transaction.js b/ui/app/selectors/confirm-transaction.js
new file mode 100644
index 000000000..8f8e0ea74
--- /dev/null
+++ b/ui/app/selectors/confirm-transaction.js
@@ -0,0 +1,191 @@
+import { createSelector } from 'reselect'
+import txHelper from '../../lib/tx-helper'
+import { calcTokenAmount } from '../token-util'
+
+const unapprovedTxsSelector = state => state.metamask.unapprovedTxs
+const unapprovedMsgsSelector = state => state.metamask.unapprovedMsgs
+const unapprovedPersonalMsgsSelector = state => state.metamask.unapprovedPersonalMsgs
+const unapprovedTypedMessagesSelector = state => state.metamask.unapprovedTypedMessages
+const networkSelector = state => state.metamask.network
+
+export const unconfirmedTransactionsListSelector = createSelector(
+ unapprovedTxsSelector,
+ unapprovedMsgsSelector,
+ unapprovedPersonalMsgsSelector,
+ unapprovedTypedMessagesSelector,
+ networkSelector,
+ (
+ unapprovedTxs = {},
+ unapprovedMsgs = {},
+ unapprovedPersonalMsgs = {},
+ unapprovedTypedMessages = {},
+ network
+ ) => txHelper(
+ unapprovedTxs,
+ unapprovedMsgs,
+ unapprovedPersonalMsgs,
+ unapprovedTypedMessages,
+ network
+ ) || []
+)
+
+export const unconfirmedTransactionsHashSelector = createSelector(
+ unapprovedTxsSelector,
+ unapprovedMsgsSelector,
+ unapprovedPersonalMsgsSelector,
+ unapprovedTypedMessagesSelector,
+ networkSelector,
+ (
+ unapprovedTxs = {},
+ unapprovedMsgs = {},
+ unapprovedPersonalMsgs = {},
+ unapprovedTypedMessages = {},
+ network
+ ) => {
+ const filteredUnapprovedTxs = Object.keys(unapprovedTxs).reduce((acc, address) => {
+ const { metamaskNetworkId } = unapprovedTxs[address]
+ const transactions = { ...acc }
+
+ if (metamaskNetworkId === network) {
+ transactions[address] = unapprovedTxs[address]
+ }
+
+ return transactions
+ }, {})
+
+ return {
+ ...filteredUnapprovedTxs,
+ ...unapprovedMsgs,
+ ...unapprovedPersonalMsgs,
+ ...unapprovedTypedMessages,
+ }
+ }
+)
+
+const unapprovedMsgCountSelector = state => state.metamask.unapprovedMsgCount
+const unapprovedPersonalMsgCountSelector = state => state.metamask.unapprovedPersonalMsgCount
+const unapprovedTypedMessagesCountSelector = state => state.metamask.unapprovedTypedMessagesCount
+
+export const unconfirmedTransactionsCountSelector = createSelector(
+ unapprovedTxsSelector,
+ unapprovedMsgCountSelector,
+ unapprovedPersonalMsgCountSelector,
+ unapprovedTypedMessagesCountSelector,
+ networkSelector,
+ (
+ unapprovedTxs = {},
+ unapprovedMsgCount = 0,
+ unapprovedPersonalMsgCount = 0,
+ unapprovedTypedMessagesCount = 0,
+ network
+ ) => {
+ const filteredUnapprovedTxIds = Object.keys(unapprovedTxs).filter(txId => {
+ const { metamaskNetworkId } = unapprovedTxs[txId]
+ return metamaskNetworkId === network
+ })
+
+ return filteredUnapprovedTxIds.length + unapprovedTypedMessagesCount + unapprovedMsgCount +
+ unapprovedPersonalMsgCount
+ }
+)
+
+
+export const currentCurrencySelector = state => state.metamask.currentCurrency
+export const conversionRateSelector = state => state.metamask.conversionRate
+
+const txDataSelector = state => state.confirmTransaction.txData
+const tokenDataSelector = state => state.confirmTransaction.tokenData
+const tokenPropsSelector = state => state.confirmTransaction.tokenProps
+
+const contractExchangeRatesSelector = state => state.metamask.contractExchangeRates
+
+const tokenDecimalsSelector = createSelector(
+ tokenPropsSelector,
+ tokenProps => tokenProps && tokenProps.tokenDecimals
+)
+
+const tokenDataParamsSelector = createSelector(
+ tokenDataSelector,
+ tokenData => tokenData && tokenData.params || []
+)
+
+const txParamsSelector = createSelector(
+ txDataSelector,
+ txData => txData && txData.txParams || {}
+)
+
+export const tokenAddressSelector = createSelector(
+ txParamsSelector,
+ txParams => txParams && txParams.to
+)
+
+const TOKEN_PARAM_SPENDER = '_spender'
+const TOKEN_PARAM_TO = '_to'
+const TOKEN_PARAM_VALUE = '_value'
+
+export const tokenAmountAndToAddressSelector = createSelector(
+ tokenDataParamsSelector,
+ params => {
+ let toAddress = ''
+ let tokenAmount = 0
+
+ if (params && params.length) {
+ const toParam = params.find(param => param.name === TOKEN_PARAM_TO)
+ const valueParam = params.find(param => param.name === TOKEN_PARAM_VALUE)
+ toAddress = toParam ? toParam.value : params[0].value
+ tokenAmount = valueParam ? Number(valueParam.value) : Number(params[1].value)
+ }
+
+ return {
+ toAddress,
+ tokenAmount,
+ }
+ }
+)
+
+export const approveTokenAmountAndToAddressSelector = createSelector(
+ tokenDataParamsSelector,
+ params => {
+ let toAddress = ''
+ let tokenAmount = 0
+
+ if (params && params.length) {
+ toAddress = params.find(param => param.name === TOKEN_PARAM_SPENDER).value
+ tokenAmount = Number(params.find(param => param.name === TOKEN_PARAM_VALUE).value)
+ }
+
+ return {
+ toAddress,
+ tokenAmount,
+ }
+ }
+)
+
+export const sendTokenTokenAmountAndToAddressSelector = createSelector(
+ tokenDataParamsSelector,
+ tokenDecimalsSelector,
+ (params, tokenDecimals) => {
+ let toAddress = ''
+ let tokenAmount = 0
+
+ if (params && params.length) {
+ toAddress = params.find(param => param.name === TOKEN_PARAM_TO).value
+ tokenAmount = Number(params.find(param => param.name === TOKEN_PARAM_VALUE).value)
+
+ if (tokenDecimals) {
+ tokenAmount = calcTokenAmount(tokenAmount, tokenDecimals)
+ }
+ }
+
+ return {
+ toAddress,
+ tokenAmount,
+ }
+ }
+)
+
+export const contractExchangeRateSelector = createSelector(
+ contractExchangeRatesSelector,
+ tokenAddressSelector,
+ (contractExchangeRates, tokenAddress) => contractExchangeRates[tokenAddress]
+)
diff --git a/ui/app/token-util.js b/ui/app/token-util.js
index cd6a47dbc..0d4233766 100644
--- a/ui/app/token-util.js
+++ b/ui/app/token-util.js
@@ -1,5 +1,6 @@
const log = require('loglevel')
const util = require('./util')
+const BigNumber = require('bignumber.js')
function tokenInfoGetter () {
const tokens = {}
@@ -43,9 +44,7 @@ async function getSymbolAndDecimals (tokenAddress, existingTokens = []) {
function calcTokenAmount (value, decimals) {
const multiplier = Math.pow(10, Number(decimals || 0))
- const amount = Number(value / multiplier)
-
- return amount
+ return new BigNumber(value).div(multiplier).toNumber()
}
diff --git a/ui/app/util.js b/ui/app/util.js
index 8c85c5926..8b194e0c7 100644
--- a/ui/app/util.js
+++ b/ui/app/util.js
@@ -59,6 +59,7 @@ module.exports = {
allNull,
getTokenAddressFromTokenObject,
checksumAddress,
+ addressSlicer,
}
function valuesFor (obj) {
@@ -303,3 +304,11 @@ function getTokenAddressFromTokenObject (token) {
function checksumAddress (address) {
return address ? ethUtil.toChecksumAddress(address) : ''
}
+
+function addressSlicer (address = '') {
+ if (address.length < 11) {
+ return address
+ }
+
+ return `${address.slice(0, 6)}...${address.slice(-4)}`
+}
diff --git a/ui/i18n-helper.js b/ui/i18n-helper.js
index 79aa93116..bc927ee65 100644
--- a/ui/i18n-helper.js
+++ b/ui/i18n-helper.js
@@ -1,20 +1,22 @@
// cross-browser connection to extension i18n API
const log = require('loglevel')
+/**
+ * Returns a localized message for the given key
+ * @param {object} locale The locale
+ * @param {string} key The message key
+ * @param {string[]} substitutions A list of message substitution replacements
+ * @return {null|string} The localized message
+ */
const getMessage = (locale, key, substitutions) => {
- // check locale is loaded
if (!locale) {
- // throw new Error('Translator - has not loaded a locale yet.')
- return ''
+ return null
}
- // check entry is present
- const { current, en } = locale
- const entry = current[key] || en[key]
- if (!entry) {
- // throw new Error(`Translator - Unable to find value for "${key}"`)
- log.error(`Translator - Unable to find value for "${key}"`)
- return `[${key}]`
+ if (!locale[key]) {
+ log.error(`Translator - Unable to find value for key "${key}"`)
+ return null
}
+ const entry = locale[key]
let phrase = entry.message
// perform substitutions
if (substitutions && substitutions.length) {
@@ -29,8 +31,7 @@ const getMessage = (locale, key, substitutions) => {
async function fetchLocale (localeName) {
try {
const response = await fetch(`./_locales/${localeName}/messages.json`)
- const locale = await response.json()
- return locale
+ return await response.json()
} catch (error) {
log.error(`failed to fetch ${localeName} locale because of ${error}`)
return {}