aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--app/_locales/en/messages.json11
-rw-r--r--app/images/search.svg14
-rw-r--r--app/images/tokensearch.svg15
-rw-r--r--old-ui/app/app.js6
-rw-r--r--old-ui/app/components/ens-input.js101
-rw-r--r--package-lock.json161
-rw-r--r--package.json5
-rw-r--r--test/base.conf.js3
-rw-r--r--test/e2e/metamask.spec.js34
-rw-r--r--test/integration/lib/add-token.js71
-rw-r--r--test/screens/new-ui.js225
-rw-r--r--test/stub/blacklist.json1374
-rw-r--r--test/unit/actions/tx_test.js2
-rw-r--r--test/unit/app/ComposableObservableStore.js (renamed from test/unit/ComposableObservableStore.js)2
-rw-r--r--test/unit/app/account-import-strategies.spec.js31
-rw-r--r--test/unit/app/buy-eth-url.spec.js48
-rw-r--r--test/unit/app/controllers/address-book-controller.js (renamed from test/unit/address-book-controller.js)2
-rw-r--r--test/unit/app/controllers/blacklist-controller-test.js (renamed from test/unit/blacklist-controller-test.js)2
-rw-r--r--test/unit/app/controllers/currency-controller-test.js (renamed from test/unit/currency-controller-test.js)3
-rw-r--r--test/unit/app/controllers/infura-controller-test.js (renamed from test/unit/infura-controller-test.js)4
-rw-r--r--test/unit/app/controllers/metamask-controller-test.js550
-rw-r--r--test/unit/app/controllers/network-contoller-test.js (renamed from test/unit/network-contoller-test.js)6
-rw-r--r--test/unit/app/controllers/notice-controller-test.js (renamed from test/unit/notice-controller-test.js)4
-rw-r--r--test/unit/app/controllers/preferences-controller-test.js (renamed from test/unit/preferences-controller-test.js)2
-rw-r--r--test/unit/app/controllers/token-rates-controller.js (renamed from test/unit/token-rates-controller.js)2
-rw-r--r--test/unit/app/controllers/transactions/nonce-tracker-test.js (renamed from test/unit/nonce-tracker-test.js)4
-rw-r--r--test/unit/app/controllers/transactions/pending-tx-test.js (renamed from test/unit/pending-tx-test.js)8
-rw-r--r--test/unit/app/controllers/transactions/tx-controller-test.js (renamed from test/unit/tx-controller-test.js)6
-rw-r--r--test/unit/app/controllers/transactions/tx-gas-util-test.js (renamed from test/unit/tx-gas-util-test.js)4
-rw-r--r--test/unit/app/controllers/transactions/tx-helper-test.js (renamed from test/unit/tx-helper-test.js)2
-rw-r--r--test/unit/app/controllers/transactions/tx-state-history-helper-test.js (renamed from test/unit/tx-state-history-helper-test.js)4
-rw-r--r--test/unit/app/controllers/transactions/tx-state-manager-test.js (renamed from test/unit/tx-state-manager-test.js)4
-rw-r--r--test/unit/app/controllers/transactions/tx-utils-test.js (renamed from test/unit/tx-utils-test.js)2
-rw-r--r--test/unit/app/edge-encryptor-test.js (renamed from test/unit/edge-encryptor-test.js)2
-rw-r--r--test/unit/app/message-manager-test.js (renamed from test/unit/message-manager-test.js)2
-rw-r--r--test/unit/app/nodeify-test.js (renamed from test/unit/nodeify-test.js)2
-rw-r--r--test/unit/app/pending-balance-test.js (renamed from test/unit/pending-balance-test.js)4
-rw-r--r--test/unit/app/personal-message-manager-test.js (renamed from test/unit/personal-message-manager-test.js)2
-rw-r--r--test/unit/app/seed-phrase-verifier-test.js (renamed from test/unit/seed-phrase-verifier-test.js)6
-rw-r--r--test/unit/app/util-test.js (renamed from test/unit/util-test.js)2
-rw-r--r--test/unit/metamask-controller-test.js120
-rw-r--r--test/unit/migrations/migrations-test.js (renamed from test/unit/migrations-test.js)28
-rw-r--r--test/unit/migrations/migrator-test.js (renamed from test/unit/migrator-test.js)6
-rw-r--r--test/unit/nameForAccount_test.js2
-rw-r--r--test/unit/reducers/unlock_vault_test.js2
-rw-r--r--test/unit/util_test.js2
-rw-r--r--ui/app/actions.js23
-rw-r--r--ui/app/app.js3
-rw-r--r--ui/app/components/button/button.component.js19
-rw-r--r--ui/app/components/button/index.js2
-rw-r--r--ui/app/components/export-text-container/index.scss (renamed from ui/app/components/export-text-container/export-text-container.scss)0
-rw-r--r--ui/app/components/index.scss5
-rw-r--r--ui/app/components/info-box/index.js2
-rw-r--r--ui/app/components/info-box/index.scss24
-rw-r--r--ui/app/components/info-box/info-box.component.js49
-rw-r--r--ui/app/components/pages/add-token.js431
-rw-r--r--ui/app/components/pages/add-token/add-token.component.js351
-rw-r--r--ui/app/components/pages/add-token/add-token.container.js22
-rw-r--r--ui/app/components/pages/add-token/index.js2
-rw-r--r--ui/app/components/pages/add-token/index.scss25
-rw-r--r--ui/app/components/pages/add-token/token-list/index.js2
-rw-r--r--ui/app/components/pages/add-token/token-list/index.scss65
-rw-r--r--ui/app/components/pages/add-token/token-list/token-list-placeholder/index.js2
-rw-r--r--ui/app/components/pages/add-token/token-list/token-list-placeholder/index.scss19
-rw-r--r--ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js27
-rw-r--r--ui/app/components/pages/add-token/token-list/token-list.component.js60
-rw-r--r--ui/app/components/pages/add-token/token-list/token-list.container.js11
-rw-r--r--ui/app/components/pages/add-token/token-search/index.js2
-rw-r--r--ui/app/components/pages/add-token/token-search/token-search.component.js85
-rw-r--r--ui/app/components/pages/add-token/util.js13
-rw-r--r--ui/app/components/pages/confirm-add-token/confirm-add-token.component.js115
-rw-r--r--ui/app/components/pages/confirm-add-token/confirm-add-token.container.js20
-rw-r--r--ui/app/components/pages/confirm-add-token/index.js2
-rw-r--r--ui/app/components/pages/confirm-add-token/index.scss69
-rw-r--r--ui/app/components/pages/confirm-add-token/token-balance/index.js2
-rw-r--r--ui/app/components/pages/confirm-add-token/token-balance/token-balance.component.js16
-rw-r--r--ui/app/components/pages/confirm-add-token/token-balance/token-balance.container.js16
-rw-r--r--ui/app/components/pages/index.scss5
-rw-r--r--ui/app/components/pages/unlock-page/index.scss (renamed from ui/app/components/pages/unlock-page/unlock-page.scss)0
-rw-r--r--ui/app/components/pages/unlock-page/unlock-page.component.js3
-rw-r--r--ui/app/components/signature-request.js2
-rw-r--r--ui/app/components/text-field/text-field.component.js119
-rw-r--r--ui/app/css/itcss/components/add-token.scss461
-rw-r--r--ui/app/css/itcss/components/buttons.scss5
-rw-r--r--ui/app/css/itcss/components/index.scss4
-rw-r--r--ui/app/css/itcss/components/newui-sections.scss2
-rw-r--r--ui/app/css/itcss/components/pages/index.scss2
-rw-r--r--ui/app/css/itcss/generic/index.scss45
-rw-r--r--ui/app/helpers/with-token-tracker.js105
-rw-r--r--ui/app/reducers/metamask.js12
-rw-r--r--ui/app/routes.js2
91 files changed, 2353 insertions, 2790 deletions
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 90beda418..fa01fea24 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -23,6 +23,9 @@
"addTokens": {
"message": "Add Tokens"
},
+ "addAcquiredTokens": {
+ "message": "Add the tokens you've acquired using MetaMask"
+ },
"amount": {
"message": "Amount"
},
@@ -53,7 +56,7 @@
"message": "Back"
},
"balance": {
- "message": "Balance:"
+ "message": "Balance"
},
"balances": {
"message": "Token balance(s)"
@@ -717,6 +720,9 @@
"search": {
"message": "Search"
},
+ "searchResults": {
+ "message": "Search Results"
+ },
"secretPhrase": {
"message": "Enter your secret twelve word phrase here to restore your vault."
},
@@ -832,6 +838,9 @@
"message": "$1 to ETH via ShapeShift",
"description": "system will fill in deposit type in start of message"
},
+ "token": {
+ "message": "Token"
+ },
"tokenAddress": {
"message": "Token Address"
},
diff --git a/app/images/search.svg b/app/images/search.svg
new file mode 100644
index 000000000..44fea12aa
--- /dev/null
+++ b/app/images/search.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="17px" height="17px" viewBox="0 0 17 17" 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>search</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Add-Tokens" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="Metamascara---add-from-token-list-Copy-3" transform="translate(-345.000000, -350.000000)" fill="#9B9B9B" fill-rule="nonzero">
+ <g id="search" transform="translate(345.000000, 350.000000)">
+ <path d="M2.01875,6.90625 C2.01875,4.25 4.25,2.01875 6.90625,2.01875 C9.5625,2.01875 11.6875,4.14375 11.6875,6.90625 C11.6875,9.5625 9.5625,11.6875 6.90625,11.6875 C4.14375,11.6875 2.01875,9.5625 2.01875,6.90625 Z M16.575,15.0875 L12.325,10.8375 C13.175,9.66875 13.6,8.2875 13.6,6.8 C13.70625,3.08125 10.625,0 6.90625,0 C3.08125,0 0,3.08125 0,6.90625 C0,10.73125 3.08125,13.8125 6.90625,13.8125 C8.18125,13.8125 9.45625,13.3875 10.4125,12.75 L14.6625,17 L16.575,15.0875 Z" id="Page-1"></path>
+ </g>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/images/tokensearch.svg b/app/images/tokensearch.svg
new file mode 100644
index 000000000..cd0b03bf2
--- /dev/null
+++ b/app/images/tokensearch.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="65px" height="58px" viewBox="0 0 65 58" 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>7FDB75AD-BD4D-497C-B391-69EEB31A0561</title>
+ <desc>Created with sketchtool.</desc>
+ <defs></defs>
+ <g id="Add-Tokens" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="Add-tokens" transform="translate(-267.000000, -284.000000)" fill="#B8BAC1">
+ <g id="tokensearch" transform="translate(267.000000, 284.000000)">
+ <path d="M28.5322581,2.80645161 C42.4391613,2.80645161 54.1925806,9.22854839 54.2552581,16.8433871 C54.1925806,24.4591613 42.4391613,30.8821935 28.5322581,30.8821935 C14.6253548,30.8821935 2.87193548,24.4600968 2.80925806,16.8443226 C2.87193548,9.22854839 14.6253548,2.80645161 28.5322581,2.80645161 M28.5322581,36.7289677 C15.7432581,36.7289677 4.78125806,31.2975484 3.05154839,24.5012581 C7.70932258,29.9981613 17.2559355,33.6886452 28.5322581,33.6886452 C39.8085806,33.6886452 49.3551935,29.9981613 54.0129677,24.5012581 C52.2832581,31.2975484 41.3212581,36.7289677 28.5322581,36.7289677 M28.5322581,54.2692903 C15.7432581,54.2692903 4.78125806,48.837871 3.05154839,42.0415806 C7.70932258,47.5384839 17.2559355,51.2289677 28.5322581,51.2289677 C33.2237097,51.2289677 37.6083226,50.5844194 41.471871,49.4403226 C42.1379355,49.243871 42.5270968,48.5675161 42.4110968,47.8818065 C42.4045484,47.8453226 42.398,47.8079032 42.3923871,47.7704839 C42.2642258,46.9603548 41.439129,46.458 40.6533226,46.6946774 C37.0180323,47.792 32.8822581,48.4225161 28.5322581,48.4225161 C15.7432581,48.4225161 4.78125806,42.9910968 3.05154839,36.1948065 C7.70932258,41.6917097 17.2559355,45.3821935 28.5322581,45.3821935 C33.3649677,45.3821935 37.8730645,44.6983548 41.8217419,43.4906452 C42.2763871,43.3503226 42.6066129,42.976129 42.7422581,42.5196129 L42.7534839,42.4812581 C43.0752903,41.4082581 42.0733871,40.4063548 41.004129,40.7403226 C37.2846452,41.9040645 33.0225806,42.5757419 28.5322581,42.5757419 C15.7432581,42.5757419 4.78125806,37.1443226 3.05154839,30.3480323 C7.70932258,35.8449355 17.2559355,39.5354194 28.5322581,39.5354194 C39.8085806,39.5354194 49.3551935,35.8449355 54.0129677,30.3480323 L54.0129677,33.5492581 C54.0129677,34.3846452 54.6902581,35.0619355 55.5256452,35.0619355 C56.3610323,35.0619355 57.0383226,34.3902581 57.0392581,33.5558065 C57.0467419,26.4900968 57.0645161,16.9257097 57.0645161,16.905129 C57.0645161,16.8845484 57.0617097,16.8649032 57.0617097,16.8443226 C57.0617097,16.8237419 57.0645161,16.8031613 57.0645161,16.7825806 L57.0598387,16.7825806 C56.9513226,7.36225806 44.4616774,0 28.5322581,0 C12.6028387,0 0.113193548,7.36225806 0.00467741935,16.7825806 L0,16.7825806 C0,16.8031613 0.00280645161,16.8237419 0.00280645161,16.8443226 C0.00280645161,16.8649032 0,16.8845484 0,16.905129 C0,16.9322581 0.00467741935,19.3420645 0.0102903226,22.6293548 L0,22.6293548 C0,22.7154194 0.00841935484,22.7996129 0.0102903226,22.8838065 C0.0140322581,24.5957419 0.0177741935,26.5247097 0.0196451613,28.476129 L0,28.476129 C0,28.650129 0.0130967742,28.8222581 0.0205806452,28.9953226 C0.0243225806,30.828871 0.0280645161,32.6586774 0.0308709677,34.3229032 L0,34.3229032 C0,34.5857742 0.0140322581,34.8467742 0.0318064516,35.1059032 C0.036483871,37.3108387 0.0392903226,39.1406452 0.0411612903,40.1696774 L0,40.1696774 C0,40.4905484 0.0177741935,40.8086129 0.0458387097,41.123871 L0.0495806452,41.2033871 C0.0645483871,41.3699032 0.0935483871,41.5345484 0.116935484,41.700129 C0.130032258,41.7861935 0.137516129,41.8731935 0.152483871,41.9583226 C0.183354839,42.1416774 0.225451613,42.3240968 0.266612903,42.5055806 C0.29,42.6103548 0.308709677,42.7160645 0.334903226,42.8199032 C0.358290323,42.9078387 0.387290323,42.9939032 0.411612903,43.0818387 C2.00006452,48.7134516 8.12841935,53.3160323 16.5777097,55.5705484 C16.6010968,55.5770968 16.6254194,55.5836452 16.6488065,55.5892581 C16.9350645,55.6650323 17.2213226,55.739871 17.5122581,55.8109677 C20.9099355,56.6538387 24.6322258,57.1215806 28.5322581,57.1215806 C32.4322903,57.1215806 36.1545806,56.6538387 39.5522581,55.8109677 C39.8431935,55.739871 40.1294516,55.6650323 40.4157097,55.5892581 C40.4390968,55.5836452 40.4634194,55.5770968 40.4868065,55.5705484 C41.5766452,55.2796129 42.6253226,54.9475161 43.6319032,54.579871 C44.4682258,54.2739677 44.7675806,53.2627097 44.2652258,52.5274194 C44.2437097,52.4956129 44.2212581,52.462871 44.1997419,52.430129 C43.8423871,51.8950323 43.1688387,51.6873548 42.5645161,51.9090645 C38.4998387,53.3955484 33.6624516,54.2692903 28.5322581,54.2692903" id="Fill-1"></path>
+ <path d="M64.3227484,54.3991355 L60.4535871,50.5299742 C61.4526839,49.1566839 61.9522323,47.5345548 61.9522323,45.7880065 C62.1009742,40.5661355 56.8996839,36.4144581 51.4654581,38.2367806 C48.6131677,39.1928452 46.4821355,41.7401677 46.0611677,44.7187484 C45.3530065,49.7460387 49.205329,54.0249419 54.0894903,54.0249419 C55.5872,54.0249419 57.0849097,53.5244581 58.2074903,52.7770065 L62.0766516,56.6452323 C62.6968774,57.2654581 63.7025226,57.2654581 64.3227484,56.6452323 C64.9429742,56.0250065 64.9429742,55.0193613 64.3227484,54.3991355 M48.3484258,45.9124258 C48.3484258,42.7925871 50.9696516,40.1713613 54.0894903,40.1713613 C57.209329,40.1713613 59.7052,42.6681677 59.7052,45.9124258 C59.7052,49.0332 57.209329,51.529071 54.0894903,51.529071 C50.8452323,51.529071 48.3484258,49.0332 48.3484258,45.9124258" id="Fill-3"></path>
+ </g>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/old-ui/app/app.js b/old-ui/app/app.js
index 2378a1a0a..abeb4f3f9 100644
--- a/old-ui/app/app.js
+++ b/old-ui/app/app.js
@@ -62,7 +62,7 @@ function mapStateToProps (state) {
isInitialized: state.metamask.isInitialized,
isUnlocked: state.metamask.isUnlocked,
currentView: state.appState.currentView,
- activeAddress: state.appState.activeAddress,
+ selectedAddress: state.metamask.selectedAddress,
transForward: state.appState.transForward,
isMascara: state.metamask.isMascara,
isOnboarding: Boolean(!noActiveNotices || seedWords || !isInitialized),
@@ -197,7 +197,7 @@ App.prototype.renderAppBar = function () {
style: {},
enableAccountsSelector: true,
identities: this.props.identities,
- selected: this.props.currentView.context,
+ selected: this.props.selectedAddress,
network: this.props.network,
keyrings: this.props.keyrings,
}, []),
@@ -588,7 +588,7 @@ App.prototype.renderPrimary = function () {
},
}, [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', {
- onClick: () => props.dispatch(actions.backToAccountDetail(props.activeAddress)),
+ onClick: () => props.dispatch(actions.backToAccountDetail(props.selectedAddress)),
style: {
marginLeft: '10px',
marginTop: '50px',
diff --git a/old-ui/app/components/ens-input.js b/old-ui/app/components/ens-input.js
index d09c30644..7e06fa9f1 100644
--- a/old-ui/app/components/ens-input.js
+++ b/old-ui/app/components/ens-input.js
@@ -1,7 +1,6 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
-const extend = require('xtend')
const debounce = require('debounce')
const copyToClipboard = require('copy-to-clipboard')
const ENS = require('ethjs-ens')
@@ -20,55 +19,61 @@ function EnsInput () {
EnsInput.prototype.render = function () {
const props = this.props
- const opts = extend(props, {
- list: 'addresses',
- onChange: () => {
- const network = this.props.network
- const networkHasEnsSupport = getNetworkEnsSupport(network)
- if (!networkHasEnsSupport) return
-
- const recipient = document.querySelector('input[name="address"]').value
- if (recipient.match(ensRE) === null) {
- return this.setState({
- loadingEns: false,
- ensResolution: null,
- ensFailure: null,
- })
- }
- this.setState({
- loadingEns: true,
+ function onInputChange() {
+ const network = this.props.network
+ const networkHasEnsSupport = getNetworkEnsSupport(network)
+ if (!networkHasEnsSupport) return
+
+ const recipient = document.querySelector('input[name="address"]').value
+ if (recipient.match(ensRE) === null) {
+ return this.setState({
+ loadingEns: false,
+ ensResolution: null,
+ ensFailure: null,
})
- this.checkName()
- },
- })
- return h('div', {
- style: { width: '100%' },
- }, [
- h('input.large-input', opts),
- // The address book functionality.
- h('datalist#addresses',
- [
- // Corresponds to the addresses owned.
- Object.keys(props.identities).map((key) => {
- const identity = props.identities[key]
- return h('option', {
- value: identity.address,
- label: identity.name,
- key: identity.address,
- })
- }),
- // Corresponds to previously sent-to addresses.
- props.addressBook.map((identity) => {
- return h('option', {
- value: identity.address,
- label: identity.name,
- key: identity.address,
- })
- }),
- ]),
- this.ensIcon(),
- ])
+ }
+
+ this.setState({
+ loadingEns: true,
+ })
+ this.checkName()
+ }
+
+ return (
+ h('div', {
+ style: { width: '100%' },
+ }, [
+ h('input.large-input', {
+ name: props.name,
+ placeholder: props.placeholder,
+ list: 'addresses',
+ onChange: onInputChange.bind(this),
+ }),
+ // The address book functionality.
+ h('datalist#addresses',
+ [
+ // Corresponds to the addresses owned.
+ Object.keys(props.identities).map((key) => {
+ const identity = props.identities[key]
+ return h('option', {
+ value: identity.address,
+ label: identity.name,
+ key: identity.address,
+ })
+ }),
+ // Corresponds to previously sent-to addresses.
+ props.addressBook.map((identity) => {
+ return h('option', {
+ value: identity.address,
+ label: identity.name,
+ key: identity.address,
+ })
+ }),
+ ]),
+ this.ensIcon(),
+ ])
+ )
}
EnsInput.prototype.componentDidMount = function () {
diff --git a/package-lock.json b/package-lock.json
index 263454b5a..a1fdf0059 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -67,6 +67,15 @@
"@babel/types": "7.0.0-beta.31"
}
},
+ "@babel/runtime": {
+ "version": "7.0.0-beta.47",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0-beta.47.tgz",
+ "integrity": "sha512-3IaakAC5B4bHJ0aCUKVw0pt+GruavdgWDFbf7TfKh7ZJ8yQuUp7af7MNwf3e+jH8776cjqYmMO1JNDDAE9WfrA==",
+ "requires": {
+ "core-js": "2.5.3",
+ "regenerator-runtime": "0.11.1"
+ }
+ },
"@babel/template": {
"version": "7.0.0-beta.31",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.31.tgz",
@@ -182,6 +191,74 @@
"through2": "2.0.3"
}
},
+ "@material-ui/core": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-1.0.0.tgz",
+ "integrity": "sha512-BTLp4goHFKGqCVSjSWNSUZp3/fvN36L0B73Z68i4Hs6TRZaApW5M2JyKmWTsCf/hk4PNKTnZMh141qNQFhxzAw==",
+ "requires": {
+ "@babel/runtime": "7.0.0-beta.47",
+ "@types/jss": "9.5.3",
+ "@types/react-transition-group": "2.0.9",
+ "brcast": "3.0.1",
+ "classnames": "2.2.5",
+ "deepmerge": "2.1.0",
+ "dom-helpers": "3.3.1",
+ "hoist-non-react-statics": "2.5.0",
+ "jss": "9.8.1",
+ "jss-camel-case": "6.1.0",
+ "jss-default-unit": "8.0.2",
+ "jss-global": "3.0.0",
+ "jss-nested": "6.0.1",
+ "jss-props-sort": "6.0.0",
+ "jss-vendor-prefixer": "7.0.0",
+ "keycode": "2.2.0",
+ "lodash": "4.17.10",
+ "normalize-scroll-left": "0.1.2",
+ "prop-types": "15.6.1",
+ "react-event-listener": "0.5.3",
+ "react-jss": "8.4.0",
+ "react-popper": "0.10.4",
+ "react-scrollbar-size": "2.1.0",
+ "react-transition-group": "2.2.1",
+ "recompose": "0.27.0",
+ "scroll": "2.0.3",
+ "warning": "3.0.0"
+ },
+ "dependencies": {
+ "@types/jss": {
+ "version": "9.5.3",
+ "resolved": "https://registry.npmjs.org/@types/jss/-/jss-9.5.3.tgz",
+ "integrity": "sha512-RQWhcpOVyIhGryKpnUyZARwsgmp+tB82O7c75lC4Tjbmr3hPiCnM1wc+pJipVEOsikYXW0IHgeiQzmxQXbnAIA==",
+ "requires": {
+ "csstype": "2.4.2",
+ "indefinite-observable": "1.0.1"
+ }
+ },
+ "deepmerge": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.1.0.tgz",
+ "integrity": "sha512-Q89Z26KAfA3lpPGhbF6XMfYAm3jIV3avViy6KOJ2JLzFbeWHOvPQUu5aSJIWXap3gDZC2y1eF5HXEPI2wGqgvw=="
+ },
+ "hoist-non-react-statics": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz",
+ "integrity": "sha512-6Bl6XsDT1ntE0lHbIhr4Kp2PGcleGZ66qu5Jqk8lc0Xc/IeG6gVLmwUGs/K0Us+L8VWoKgj0uWdPMataOsm31w=="
+ },
+ "recompose": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/recompose/-/recompose-0.27.0.tgz",
+ "integrity": "sha512-hivr1EopLhzjchhv2Y7VcLA2H5NGztwV/qfYqmIAhTkNowNQ9PyXdfq9Q8QCa0TMrPM1NtStlUyi5I/p8XfUNQ==",
+ "requires": {
+ "babel-runtime": "6.26.0",
+ "change-emitter": "0.1.6",
+ "fbjs": "0.8.16",
+ "hoist-non-react-statics": "2.5.0",
+ "react-lifecycles-compat": "3.0.2",
+ "symbol-observable": "1.1.0"
+ }
+ }
+ }
+ },
"@sentry/cli": {
"version": "1.30.3",
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-1.30.3.tgz",
@@ -1365,15 +1442,6 @@
}
}
},
- "@types/jss": {
- "version": "9.5.2",
- "resolved": "https://registry.npmjs.org/@types/jss/-/jss-9.5.2.tgz",
- "integrity": "sha512-EX87yNYcisXO5BU9tT7stB7OGuDJyV3JwtMwhfUprrmHwYKWh9a3vchAy6DYzUSbmTA7bD46h8qata5jP1V7Zw==",
- "requires": {
- "csstype": "2.4.2",
- "indefinite-observable": "1.0.1"
- }
- },
"@types/node": {
"version": "8.5.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.5.5.tgz",
@@ -17752,78 +17820,6 @@
"integrity": "sha1-UpJZPmdUyxvMK5gDDk4Najr8nqE=",
"dev": true
},
- "material-ui": {
- "version": "1.0.0-beta.44",
- "resolved": "https://registry.npmjs.org/material-ui/-/material-ui-1.0.0-beta.44.tgz",
- "integrity": "sha512-m5SJxvDz77bVKcjyZG/AyG6RBR+UUwkPgvHHLJa2jyAHBNtJMCQ5GVouTXOxaUKlvD5cbO/mcH0YtzugyQTAVg==",
- "requires": {
- "@types/jss": "9.5.2",
- "@types/react-transition-group": "2.0.9",
- "babel-runtime": "6.26.0",
- "brcast": "3.0.1",
- "classnames": "2.2.5",
- "deepmerge": "2.1.0",
- "dom-helpers": "3.3.1",
- "hoist-non-react-statics": "2.5.0",
- "jss": "9.8.1",
- "jss-camel-case": "6.1.0",
- "jss-default-unit": "8.0.2",
- "jss-global": "3.0.0",
- "jss-nested": "6.0.1",
- "jss-props-sort": "6.0.0",
- "jss-vendor-prefixer": "7.0.0",
- "keycode": "2.2.0",
- "lodash": "4.17.10",
- "normalize-scroll-left": "0.1.2",
- "prop-types": "15.6.1",
- "react-event-listener": "0.5.3",
- "react-jss": "8.4.0",
- "react-lifecycles-compat": "2.0.2",
- "react-popper": "0.10.4",
- "react-scrollbar-size": "2.1.0",
- "react-transition-group": "2.2.1",
- "recompose": "0.27.0",
- "scroll": "2.0.3",
- "warning": "3.0.0"
- },
- "dependencies": {
- "deepmerge": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.1.0.tgz",
- "integrity": "sha512-Q89Z26KAfA3lpPGhbF6XMfYAm3jIV3avViy6KOJ2JLzFbeWHOvPQUu5aSJIWXap3gDZC2y1eF5HXEPI2wGqgvw=="
- },
- "hoist-non-react-statics": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz",
- "integrity": "sha512-6Bl6XsDT1ntE0lHbIhr4Kp2PGcleGZ66qu5Jqk8lc0Xc/IeG6gVLmwUGs/K0Us+L8VWoKgj0uWdPMataOsm31w=="
- },
- "react-lifecycles-compat": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-2.0.2.tgz",
- "integrity": "sha512-BPksUj7VMAAFhcCw79sZA0Ow/LTAEjs3Sio1AQcuwLeOP+ua0f/08Su2wyiW+JjDDH6fRqNy3h5CLXh21u1mVg=="
- },
- "recompose": {
- "version": "0.27.0",
- "resolved": "https://registry.npmjs.org/recompose/-/recompose-0.27.0.tgz",
- "integrity": "sha512-hivr1EopLhzjchhv2Y7VcLA2H5NGztwV/qfYqmIAhTkNowNQ9PyXdfq9Q8QCa0TMrPM1NtStlUyi5I/p8XfUNQ==",
- "requires": {
- "babel-runtime": "6.26.0",
- "change-emitter": "0.1.6",
- "fbjs": "0.8.16",
- "hoist-non-react-statics": "2.5.0",
- "react-lifecycles-compat": "3.0.3",
- "symbol-observable": "1.1.0"
- },
- "dependencies": {
- "react-lifecycles-compat": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.3.tgz",
- "integrity": "sha512-bOr65SSYgxDgDNqLnDqt+gropXGPNB1Wbyys4tOYiNuP/qYWC4qFM9XH1ruzq+tT6EjE29pJsCr19rclKtpUEg=="
- }
- }
- }
- }
- },
"math-expression-evaluator": {
"version": "1.2.17",
"resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz",
@@ -25056,8 +25052,7 @@
"react-lifecycles-compat": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.2.tgz",
- "integrity": "sha512-pbZOSMVVkvppW7XRn9fcHK5OgEDnYLwMva7P6TgS44/SN9uGGjfh3Z1c8tomO+y4IsHQ6Fsz2EGwmE7sMeNZgQ==",
- "dev": true
+ "integrity": "sha512-pbZOSMVVkvppW7XRn9fcHK5OgEDnYLwMva7P6TgS44/SN9uGGjfh3Z1c8tomO+y4IsHQ6Fsz2EGwmE7sMeNZgQ=="
},
"react-markdown": {
"version": "3.1.4",
diff --git a/package.json b/package.json
index 9e4950734..b5d8b545f 100644
--- a/package.json
+++ b/package.json
@@ -63,6 +63,7 @@
]
},
"dependencies": {
+ "@material-ui/core": "^1.0.0",
"abi-decoder": "^1.0.9",
"asmcrypto.js": "0.22.0",
"async": "^2.5.0",
@@ -136,7 +137,6 @@
"lodash.shuffle": "^4.2.0",
"lodash.uniqby": "^4.7.0",
"loglevel": "^1.4.1",
- "material-ui": "1.0.0-beta.44",
"metamascara": "^2.0.0",
"metamask-logo": "^2.1.4",
"mkdirp": "^0.5.1",
@@ -186,7 +186,6 @@
"sw-controller": "^1.0.3",
"sw-stream": "^2.0.2",
"textarea-caret": "^3.0.1",
- "through2": "^2.0.3",
"valid-url": "^1.0.9",
"vreme": "^3.0.2",
"web3": "^0.20.1",
@@ -213,6 +212,7 @@
"browserify": "^16.1.1",
"chai": "^4.1.0",
"chromedriver": "2.36.0",
+ "clipboardy": "^1.2.3",
"compression": "^1.7.1",
"coveralls": "^3.0.0",
"cross-env": "^5.1.4",
@@ -291,6 +291,7 @@
"stylelint-config-standard": "^18.2.0",
"tape": "^4.5.1",
"testem": "^2.0.0",
+ "through2": "^2.0.3",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",
"watchify": "^3.9.0"
diff --git a/test/base.conf.js b/test/base.conf.js
index e2e9d44ba..956dce011 100644
--- a/test/base.conf.js
+++ b/test/base.conf.js
@@ -6,6 +6,9 @@ module.exports = function(config) {
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: process.cwd(),
+ // Uncomment to allow for longer timeouts
+ // browserNoActivityTimeout: 100000000,
+
browserConsoleLogOptions: {
terminal: false,
},
diff --git a/test/e2e/metamask.spec.js b/test/e2e/metamask.spec.js
index 707ca2560..8ec7de16c 100644
--- a/test/e2e/metamask.spec.js
+++ b/test/e2e/metamask.spec.js
@@ -30,6 +30,18 @@ describe('Metamask popup page', function () {
})
afterEach(async function () {
+ // logs command not supported in firefox
+ // https://github.com/SeleniumHQ/selenium/issues/2910
+ if (process.env.SELENIUM_BROWSER === 'chrome') {
+ // check for console errors
+ const errors = await checkBrowserForConsoleErrors()
+ if (errors.length) {
+ const errorReports = errors.map(err => err.message)
+ const errorMessage = `Errors found in browser console:\n${errorReports.join('\n')}`
+ this.test.error(new Error(errorMessage))
+ }
+ }
+ // gather extra data if test failed
if (this.currentTest.state === 'failed') {
await verboseReportOnFailure(this.currentTest)
}
@@ -300,13 +312,33 @@ describe('Metamask popup page', function () {
await driver.executeScript('window.metamask.setProviderType(arguments[0])', type)
}
+ async function checkBrowserForConsoleErrors() {
+ const ignoredLogTypes = ['WARNING']
+ const ignoredErrorMessages = [
+ // React throws error warnings on "dataset", but still sets the data-* properties correctly
+ 'Warning: Unknown prop `dataset` on ',
+ // Third-party Favicon 404s show up as errors
+ 'favicon.ico - Failed to load resource: the server responded with a status of 404 (Not Found)',
+ // React Development build - known issue blocked by test build sys
+ 'Warning: It looks like you\'re using a minified copy of the development build of React.',
+ // Redux Development build - known issue blocked by test build sys
+ 'This means that you are running a slower development build of Redux.',
+ ]
+ const browserLogs = await driver.manage().logs().get('browser')
+ const errorEntries = browserLogs.filter(entry => !ignoredLogTypes.includes(entry.level.toString()))
+ const errorObjects = errorEntries.map(entry => entry.toJSON())
+ // ignore all errors that contain a message in `ignoredErrorMessages`
+ const matchedErrorObjects = errorObjects.filter(entry => !ignoredErrorMessages.some(message => entry.message.includes(message)))
+ return matchedErrorObjects
+ }
+
async function verboseReportOnFailure (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)
// capture screenshot
diff --git a/test/integration/lib/add-token.js b/test/integration/lib/add-token.js
index 1840bdd39..e51c854d2 100644
--- a/test/integration/lib/add-token.js
+++ b/test/integration/lib/add-token.js
@@ -22,6 +22,11 @@ async function runAddTokenFlowTest (assert, done) {
selectState.val('add token')
reactTriggerChange(selectState[0])
+ // Used to set values on TextField input component
+ const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
+ window.HTMLInputElement.prototype, 'value'
+ ).set
+
// Check that no tokens have been added
assert.ok($('.token-list-item').length === 0, 'no tokens added')
@@ -31,14 +36,14 @@ async function runAddTokenFlowTest (assert, done) {
addTokenButton[0].click()
// Verify Add Token screen
- let addTokenWrapper = await queryAsync($, '.add-token__wrapper')
+ let addTokenWrapper = await queryAsync($, '.page-container')
assert.ok(addTokenWrapper[0], 'add token wrapper renders')
- let addTokenTitle = await queryAsync($, '.add-token__header__title')
+ let addTokenTitle = await queryAsync($, '.page-container__title')
assert.equal(addTokenTitle[0].textContent, 'Add Tokens', 'add token title is correct')
// Cancel Add Token
- const cancelAddTokenButton = await queryAsync($, 'button.btn-secondary--lg.add-token__cancel-button')
+ const cancelAddTokenButton = await queryAsync($, 'button.btn-secondary--lg.page-container__footer-button')
assert.ok(cancelAddTokenButton[0], 'cancel add token button present')
cancelAddTokenButton.click()
@@ -50,20 +55,22 @@ async function runAddTokenFlowTest (assert, done) {
addTokenButton[0].click()
// Verify Add Token Screen
- addTokenWrapper = await queryAsync($, '.add-token__wrapper')
- addTokenTitle = await queryAsync($, '.add-token__header__title')
+ addTokenWrapper = await queryAsync($, '.page-container')
+ addTokenTitle = await queryAsync($, '.page-container__title')
assert.ok(addTokenWrapper[0], 'add token wrapper renders')
assert.equal(addTokenTitle[0].textContent, 'Add Tokens', 'add token title is correct')
// Search for token
- const searchInput = await queryAsync($, 'input.add-token__input')
- searchInput.val('a')
- reactTriggerChange(searchInput[0])
+ const searchInput = (await findAsync(addTokenWrapper, '#search-tokens'))[0]
+ searchInput.focus()
+ await timeout(1000)
+ nativeInputValueSetter.call(searchInput, 'a')
+ searchInput.dispatchEvent(new Event('input', { bubbles: true}))
// Click token to add
- const tokenWrapper = await queryAsync($, 'div.add-token__token-wrapper')
+ const tokenWrapper = await queryAsync($, 'div.token-list__token')
assert.ok(tokenWrapper[0], 'token found')
- const tokenImageProp = tokenWrapper.find('.add-token__token-icon').css('background-image')
+ const tokenImageProp = tokenWrapper.find('.token-list__token-icon').css('background-image')
const tokenImageUrl = tokenImageProp.slice(5, -2)
tokenWrapper[0].click()
@@ -73,11 +80,8 @@ async function runAddTokenFlowTest (assert, done) {
nextButton[0].click()
// Confirm Add token
- assert.equal(
- $('.add-token__description')[0].textContent,
- 'Token balance(s)',
- 'confirm add token rendered'
- )
+ const confirmAddToken = await queryAsync($, '.confirm-add-token')
+ assert.ok(confirmAddToken[0], 'confirm add token rendered')
assert.ok($('button.btn-primary--lg')[0], 'confirm add token button found')
$('button.btn-primary--lg')[0].click()
@@ -91,39 +95,46 @@ async function runAddTokenFlowTest (assert, done) {
assert.ok(addTokenButton[0], 'add token button present')
addTokenButton[0].click()
- const addTokenTabs = await queryAsync($, '.add-token__header__tabs__tab')
+ addTokenWrapper = await queryAsync($, '.page-container')
+ const addTokenTabs = await queryAsync($, '.page-container__tab')
assert.equal(addTokenTabs.length, 2, 'expected number of tabs')
assert.equal(addTokenTabs[1].textContent, 'Custom Token', 'Custom Token tab present')
assert.ok(addTokenTabs[1], 'add custom token tab present')
addTokenTabs[1].click()
+ await timeout(1000)
// Input token contract address
- const customInput = await queryAsync($, 'input.add-token__add-custom-input')
- customInput.val('0x177af043D3A1Aed7cc5f2397C70248Fc6cDC056c')
- reactTriggerChange(customInput[0])
+ const customInput = (await findAsync(addTokenWrapper, '#custom-address'))[0]
+ customInput.focus()
+ await timeout(1000)
+ nativeInputValueSetter.call(customInput, '0x177af043D3A1Aed7cc5f2397C70248Fc6cDC056c')
+ customInput.dispatchEvent(new Event('input', { bubbles: true}))
+
// Click Next button
- nextButton = await queryAsync($, 'button.btn-primary--lg')
- assert.equal(nextButton[0].textContent, 'Next', 'next button rendered')
- nextButton[0].click()
+ // nextButton = await queryAsync($, 'button.btn-primary--lg')
+ // assert.equal(nextButton[0].textContent, 'Next', 'next button rendered')
+ // nextButton[0].click()
- // Verify symbol length error since contract address won't return symbol
- const errorMessage = await queryAsync($, '.add-token__add-custom-error-message')
+ // // Verify symbol length error since contract address won't return symbol
+ const errorMessage = await queryAsync($, '#custom-symbol-helper-text')
assert.ok(errorMessage[0], 'error rendered')
$('button.btn-secondary--lg')[0].click()
- // // Confirm Add token
+ // await timeout(100000)
+
+ // Confirm Add token
// assert.equal(
- // $('.add-token__description')[0].textContent,
+ // $('.page-container__subtitle')[0].textContent,
// 'Would you like to add these tokens?',
// 'confirm add token rendered'
// )
// assert.ok($('button.btn-primary--lg')[0], 'confirm add token button found')
// $('button.btn-primary--lg')[0].click()
- // // Verify added token image
- // heroBalance = await queryAsync($, '.hero-balance')
- // assert.ok(heroBalance, 'rendered hero balance')
- // assert.ok(heroBalance.find('.identicon')[0], 'token added')
+ // Verify added token image
+ heroBalance = await queryAsync($, '.hero-balance')
+ assert.ok(heroBalance, 'rendered hero balance')
+ assert.ok(heroBalance.find('.identicon')[0], 'token added')
}
diff --git a/test/screens/new-ui.js b/test/screens/new-ui.js
index 6a8822eb3..6b873ac85 100644
--- a/test/screens/new-ui.js
+++ b/test/screens/new-ui.js
@@ -5,29 +5,44 @@ const mkdirp = require('mkdirp')
const rimraf = require('rimraf')
const webdriver = require('selenium-webdriver')
const endOfStream = require('end-of-stream')
+const clipboardy = require('clipboardy')
+const Ethjs = require('ethjs')
const GIFEncoder = require('gifencoder')
const pngFileStream = require('png-file-stream')
const sizeOfPng = require('image-size/lib/types/png')
const By = webdriver.By
const { delay, buildWebDriver } = require('./func')
const localesIndex = require('../../app/_locales/index.json')
+// const localesIndex = []
+
+const eth = new Ethjs(new Ethjs.HttpProvider('http://localhost:8545'))
let driver
+let screenshotCount = 0
+
+captureAllScreens()
+.then(async () => {
+ // build screenshots into gif
+ console.log('building gif...')
+ await generateGif()
-captureAllScreens().catch((err) => {
+ await driver.quit()
+ process.exit()
+})
+.catch(async (err) => {
try {
console.error(err)
- verboseReportOnFailure()
- driver.quit()
+ verboseReportOnFailure({ title: 'something broke' })
} catch (err) {
console.error(err)
}
+
+ await driver.quit()
process.exit(1)
})
-async function captureAllScreens() {
- let screenshotCount = 0
+async function captureAllScreens() {
// common names
let button
let tabs
@@ -74,10 +89,11 @@ async function captureAllScreens() {
await driver.findElement(By.css('button')).click()
await captureLanguageScreenShots('create password')
+ const password = '123456789'
const passwordBox = await driver.findElement(By.css('input#create-password'))
const passwordBoxConfirm = await driver.findElement(By.css('input#confirm-password'))
- passwordBox.sendKeys('123456789')
- passwordBoxConfirm.sendKeys('123456789')
+ passwordBox.sendKeys(password)
+ passwordBoxConfirm.sendKeys(password)
await delay(500)
await captureLanguageScreenShots('choose-password-filled')
@@ -111,109 +127,123 @@ async function captureAllScreens() {
await delay(300)
await captureLanguageScreenShots('secret backup phrase - reveal')
+ const seedPhrase = await driver.findElement(By.css('.backup-phrase__secret-words')).getText()
+ const seedPhraseWords = seedPhrase.split(' ')
await driver.findElement(By.css('button')).click()
await delay(300)
await captureLanguageScreenShots('confirm secret backup phrase')
- // finish up
- console.log('building gif...')
- await generateGif()
- await driver.quit()
- return
-
- //
- // await button.click()
- // await delay(700)
- // this.seedPhase = await driver.findElement(By.css('.twelve-word-phrase')).getText()
- // await captureScreenShot('seed phrase')
- //
- // const continueAfterSeedPhrase = await driver.findElement(By.css('button'))
- // await continueAfterSeedPhrase.click()
- // await delay(300)
- // await captureScreenShot('main screen')
- //
- // await driver.findElement(By.css('.sandwich-expando')).click()
- // await delay(500)
- // await captureScreenShot('menu')
-
- // await driver.findElement(By.css('#app-content > div > div:nth-child(3) > span > div > li:nth-child(3)')).click()
- // await captureScreenShot('main screen')
- // it('should accept account password after lock', async () => {
- // await delay(500)
- // await driver.findElement(By.id('password-box')).sendKeys('123456789')
- // await driver.findElement(By.css('button')).click()
- // await delay(500)
- // })
- //
- // it('should show QR code option', async () => {
- // await delay(300)
- // await driver.findElement(By.css('.fa-ellipsis-h')).click()
- // await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div > div:nth-child(1) > flex-column > div.name-label > div > span > i > div > div > li:nth-child(3)')).click()
- // await delay(300)
- // })
- //
- // it('should show the account address', async () => {
- // this.accountAddress = await driver.findElement(By.css('.ellip-address')).getText()
- // await driver.findElement(By.css('.fa-arrow-left')).click()
- // await delay(500)
- // })
-
- async function captureLanguageScreenShots(label) {
- const nonEnglishLocales = localesIndex.filter(localeMeta => localeMeta.code !== 'en')
- // take english shot
- await captureScreenShot(`${label} (en)`)
- for (let localeMeta of nonEnglishLocales) {
- // set locale and take shot
- await setLocale(localeMeta.code)
- await delay(300)
- await captureScreenShot(`${label} (${localeMeta.code})`)
- }
- // return locale to english
- await setLocale('en')
- await delay(300)
+ // enter seed phrase
+ const seedPhraseButtons = await driver.findElements(By.css('.backup-phrase__confirm-seed-options > button'))
+ const seedPhraseButtonWords = await Promise.all(seedPhraseButtons.map(button => button.getText()))
+ for (let targetWord of seedPhraseWords) {
+ const wordIndex = seedPhraseButtonWords.indexOf(targetWord)
+ if (wordIndex === -1) throw new Error(`Captured seed phrase word "${targetWord}" not in found seed phrase button options ${seedPhraseButtonWords.join(' ')}`)
+ await driver.findElement(By.css(`.backup-phrase__confirm-seed-options > button:nth-child(${wordIndex+1})`)).click()
+ await delay(100)
}
+ await captureLanguageScreenShots('confirm secret backup phrase - words selected correctly')
- async function setLocale(code) {
- await driver.executeScript('window.metamask.updateCurrentLocale(arguments[0])', code)
- }
+ await driver.findElement(By.css('.backup-phrase__content-wrapper .first-time-flow__button')).click()
+ await delay(300)
+ await captureLanguageScreenShots('metamask post-initialize greeter screen deposit ether')
- async function setProviderType(type) {
- await driver.executeScript('window.metamask.setProviderType(arguments[0])', type)
- }
+ await driver.findElement(By.css('.page-container__header-close')).click()
+ await delay(300)
+ await captureLanguageScreenShots('metamask account main screen')
- // cleanup
- await driver.quit()
+ // account details + export private key
+ await driver.findElement(By.css('.wallet-view__name-container > .wallet-view__details-button')).click()
+ await delay(300)
+ await captureLanguageScreenShots('metamask account detail screen')
- async function cleanScreenShotDir() {
- await pify(rimraf)(`./test-artifacts/screens/`)
- }
+ await driver.findElement(By.css('.account-modal__button:nth-of-type(2)')).click()
+ await delay(300)
+ await captureLanguageScreenShots('metamask account detail export private key screen - initial')
- async function captureScreenShot(label) {
- const shotIndex = screenshotCount.toString().padStart(4, '0')
- screenshotCount++
- const artifactDir = `./test-artifacts/screens/`
- await pify(mkdirp)(artifactDir)
- // capture screenshot
- const screenshot = await driver.takeScreenshot()
- await pify(fs.writeFile)(`${artifactDir}/${shotIndex} - ${label}.png`, screenshot, { encoding: 'base64' })
- }
+ await driver.findElement(By.css('.private-key-password > input')).sendKeys(password)
+ await delay(300)
+ await captureLanguageScreenShots('metamask account detail export private key screen - password entered')
- async function generateGif(){
- // calculate screenshot size
- const screenshot = await driver.takeScreenshot()
- const pngBuffer = Buffer.from(screenshot, 'base64')
- const size = sizeOfPng.calculate(pngBuffer)
+ await driver.findElement(By.css('.btn-primary--lg.export-private-key__button')).click()
+ await delay(300)
+ await captureLanguageScreenShots('metamask account detail export private key screen - reveal key')
- // read only the english pngs into gif
- const encoder = new GIFEncoder(size.width, size.height)
- const stream = pngFileStream('./test-artifacts/screens/* (en).png')
- .pipe(encoder.createWriteStream({ repeat: 0, delay: 1000, quality: 10 }))
- .pipe(fs.createWriteStream('./test-artifacts/screens/walkthrough (en).gif'))
+ await driver.findElement(By.css('.export-private-key__button')).click()
+ await delay(300)
+ await captureLanguageScreenShots('metamask account detail export private key screen - done')
- // wait for end
- await pify(endOfStream)(stream)
+ // get eth from Ganache
+ // const viewAddressButton = await driver.findElement(By.css('.wallet-view__address'))
+ // await driver.actions({ bridge: true }).move({ origin: viewAddressButton }).perform()
+ // console.log('driver.actions', driver.actions({ bridge: true }))
+ // await delay(300)
+ // await captureLanguageScreenShots('metamask home - hover copy address')
+
+ await driver.findElement(By.css('.wallet-view__address')).click()
+ await delay(100)
+ await captureLanguageScreenShots('metamask home - hover copy address')
+
+ const primaryAddress = clipboardy.readSync()
+ await requestEther(primaryAddress)
+ // wait for block polling
+ await delay(10000)
+ await captureLanguageScreenShots('metamask home - has ether')
+
+}
+
+
+async function captureLanguageScreenShots(label) {
+ const nonEnglishLocales = localesIndex.filter(localeMeta => localeMeta.code !== 'en')
+ // take english shot
+ await captureScreenShot(`${label} (en)`)
+ for (let localeMeta of nonEnglishLocales) {
+ // set locale and take shot
+ await setLocale(localeMeta.code)
+ await delay(300)
+ await captureScreenShot(`${label} (${localeMeta.code})`)
}
+ // return locale to english
+ await setLocale('en')
+ await delay(300)
+}
+
+async function setLocale(code) {
+ await driver.executeScript('window.metamask.updateCurrentLocale(arguments[0])', code)
+}
+
+async function setProviderType(type) {
+ await driver.executeScript('window.metamask.setProviderType(arguments[0])', type)
+}
+async function cleanScreenShotDir() {
+ await pify(rimraf)(`./test-artifacts/screens/`)
+}
+
+async function captureScreenShot(label) {
+ const shotIndex = screenshotCount.toString().padStart(4, '0')
+ screenshotCount++
+ const artifactDir = `./test-artifacts/screens/`
+ await pify(mkdirp)(artifactDir)
+ // capture screenshot
+ const screenshot = await driver.takeScreenshot()
+ await pify(fs.writeFile)(`${artifactDir}/${shotIndex} - ${label}.png`, screenshot, { encoding: 'base64' })
+}
+
+async function generateGif(){
+ // calculate screenshot size
+ const screenshot = await driver.takeScreenshot()
+ const pngBuffer = Buffer.from(screenshot, 'base64')
+ const size = sizeOfPng.calculate(pngBuffer)
+
+ // read only the english pngs into gif
+ const encoder = new GIFEncoder(size.width, size.height)
+ const stream = pngFileStream('./test-artifacts/screens/* (en).png')
+ .pipe(encoder.createWriteStream({ repeat: 0, delay: 1000, quality: 10 }))
+ .pipe(fs.createWriteStream('./test-artifacts/screens/walkthrough (en).gif'))
+
+ // wait for end
+ await pify(endOfStream)(stream)
}
async function verboseReportOnFailure(test) {
@@ -227,3 +257,8 @@ async function verboseReportOnFailure(test) {
const htmlSource = await driver.getPageSource()
await pify(fs.writeFile)(`${filepathBase}-dom.html`, htmlSource)
}
+
+async function requestEther(address) {
+ const accounts = await eth.accounts()
+ await eth.sendTransaction({ from: accounts[0], to: address, value: 1 * 1e18, data: '0x0' })
+}
diff --git a/test/stub/blacklist.json b/test/stub/blacklist.json
deleted file mode 100644
index 6a3230b2f..000000000
--- a/test/stub/blacklist.json
+++ /dev/null
@@ -1,1374 +0,0 @@
-{
- "version": 2,
- "tolerance": 2,
- "fuzzylist": [
- "metamask.io",
- "myetherwallet.com",
- "cryptokitties.co",
- "mycrypto.com"
- ],
- "whitelist": [
- "crypto.pro",
- "ocrypto.org",
- "wecrypto.net",
- "iccrypto.io",
- "crypto.kred",
- "ohmycrypto.io",
- "spcrypto.net",
- "melcrypto.com",
- "zzcrypto.org",
- "zzcrypto.net",
- "crypto.bg",
- "mycrypto24.online",
- "acrypto.io",
- "mycrypto.ca",
- "scrypto.io",
- "mycrypto.dk",
- "mvzcrypto.com",
- "ambcrypto.com",
- "crypto.bi",
- "crypto.jobs",
- "crypto.help",
- "my.crypt.observer",
- "crypt.observer",
- "ucrypto.com",
- "cryptojobslist.com",
- "crypto.review",
- "crypto.me",
- "b3crypto.com",
- "mycrypto.ninja",
- "jkcrypto.com",
- "crypto.cr",
- "mycrypto.live",
- "yocrypto.io",
- "crypto.ba",
- "zacrypto.info",
- "mycrypto.com",
- "remix.ethereum.org",
- "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",
- "remme.io",
- "jibrel.network"
- ],
- "blacklist": [
- "xn--myethrwalle-jb9e19a.com",
- "xn--myetheralle-7b9ezl.com",
- "iconfoundation.co",
- "fundrequest.info",
- "xn--myetherwale-os8e7x.com",
- "remme-ico.eu",
- "gonetwork.live",
- "token.gonetwork.pro",
- "gonetwork.pro",
- "gonetwork.eu",
- "nucleus-vision.cc",
- "jibreltoken.in",
- "dock.so",
- "dock.promo",
- "xn--mycrypt-r0a.com",
- "xn--mycrypt-g1a.com",
- "xn--mycrpto-y2a.com",
- "ethexploit.org",
- "remme.in",
- "remme.ws",
- "remme.com.ng",
- "nyeitthervvallet.com",
- "xn--myeerhwailet-ooc.com",
- "myeterhwaliot.com",
- "remme.live",
- "xn--yethewalle-to2exkhi.com",
- "myetherwallet.custom-token.com",
- "custom-token.com",
- "sale-earn.com",
- "bankera.live",
- "originprotocol.io",
- "trx.foundation",
- "tokensale.adhive.net",
- "adhive.net",
- "decentral.market",
- "cryptoexploite.com",
- "blockclain.net",
- "xn--blckchin-5za9o.info",
- "xn--blkhain-m0a4pb.info",
- "xn--blocchal-gmb8m.info",
- "xn--blocchaln-orb.info",
- "xn--blocchan-gmb7c.info",
- "xn--blockaden-lsen-5pb.com",
- "xn--blockchai-3vb.info",
- "xn--blockchai-jvb.info",
- "xn--blockchal-3vb.info",
- "xn--blockcham-ipb.info",
- "xn--blockchan-2pb.com",
- "xn--blockchan-75a.com",
- "xn--blockchan-7sb.info",
- "xn--blockchan-d5a.net",
- "xn--blockchan-dob.info",
- "xn--blockchan-ipb.com",
- "xn--blockchan-ipb.info",
- "xn--blockchan-nk7d.com",
- "xn--blockchan-xub.info",
- "xn--blockchann-4ub.com",
- "xn--blockchi-n7a50e.info",
- "xn--blockchi-o8a54d.info",
- "xn--blockchi-p99co8a.com",
- "xn--blockchim-hdb.info",
- "xn--blockchin-1xb.info",
- "xn--blockchin-61a.info",
- "xn--blockchin-61a.net",
- "xn--blockchin-6ib.info",
- "xn--blockchin-ccb.info",
- "xn--blockchin-h4a.com",
- "xn--blockchin-h4a.info",
- "xn--blockchin-hdb.info",
- "xn--blockchin-hhb.info",
- "xn--blockchin-mib.net",
- "xn--blockchin-wcb.com",
- "xn--blockchn-fza4j.com",
- "xn--blockchn-fza4j.info",
- "xn--blockchn-n7a43b.info",
- "xn--blockchn-p0a.info",
- "xn--blockchn-tx0d4p.com",
- "xn--blockclai-3vb.info",
- "xn--blockclin-hdb.com",
- "xn--blockclin-hdb.info",
- "xn--blockclin-hdb.org",
- "xn--blockflte-kirchrode-w6b.de",
- "xn--blockfltenquartett-windspiel-81c.de",
- "xn--blockhai-obb78c.info",
- "xn--blockhain-4eb.com",
- "xn--blockhain-pfb.com",
- "xn--blockhain-pfb.info",
- "xn--blockhain-zdb.info",
- "xn--blockhan-obb65a.info",
- "xn--blockhas-d6a.com",
- "xn--blockwallt-j7a.com",
- "xn--blokchai-fqb.info",
- "xn--blokchain-nfb.info",
- "xn--blokhain-28ab.info",
- "xn--bockclnain-eyb.info",
- "xn--mymoeo-zt7bzf.com",
- "xn--mymoer-nqc1368c.com",
- "xn--mymoero-c13c.com",
- "xn--mymoero-s13c.com",
- "xn--mymoneo-f63c.com",
- "xn--mymoneo-v63c.com",
- "xn--mymoneo-y53c.com",
- "xn--mymoner-j0a.com",
- "xn--mymoner-j5b.com",
- "xn--mymoner-r0a.com",
- "xn--mymoner-z0a.com",
- "xn--mymoner-z2c.com",
- "xn--mymonro-fya.com",
- "xn--mymonro-x8a.com",
- "xn--myetheallet-l58emu.com",
- "xn--myetheraet-9k2ea77h.com",
- "xn--myetheralet-ms8e21b.com",
- "xn--myetheralle-7b9exm.com",
- "xn--myetherallet-5s5f.com",
- "xn--myetherallet-fs5f.com",
- "xn--myetherewalle-1t1g.com",
- "xn--myetherllet-pl9e6k.com",
- "xn--myethervvalle-8vc.com",
- "xn--myetherwaet-61ea.com",
- "xn--myetherwaet-8eda.com",
- "xn--myetherwaet-ns8ea.com",
- "xn--myetherwale-ns8e8x.com",
- "xn--myetherwalet-0fb.com",
- "xn--myetherwalet-0z4f.com",
- "xn--myetherwalet-814f.com",
- "xn--myetherwalet-d9b.com",
- "xn--myetherwalet-h14f.com",
- "xn--myetherwalle-9me.com",
- "xn--myetherwalle-ek5f.com",
- "xn--myetherwalle-fqc.com",
- "xn--myetherwalle-opc.com",
- "xn--myetherwalle-q05f.com",
- "xn--myetherwllet-wob.com",
- "xn--myetherwllt-r7a0i.com",
- "xn--myethewaliet-9d5f.com",
- "xn--myethewalle-3ic0947g.com",
- "xn--myethewallet-0e5f.com",
- "xn--myethewallet-1kc.com",
- "xn--myethewallet-bkc.com",
- "xn--myethewallet-vof.com",
- "xn--myethewalliet-nm1g.com",
- "xn--myethewallt-kbb3019g.com",
- "xn--myethewallt-w48ew7b.com",
- "xn--myethrwalet-6qb6408g.com",
- "xn--myethrwalet-ms8e83d.com",
- "xn--myethrwallet-1db.com",
- "xn--myethrwallt-29af.com",
- "xn--myethrwallt-29as.com",
- "xn--myethrwllet-q7a31e.com",
- "xn--myethrwllet-r8a3c.com",
- "fintrux.eu",
- "refereum-ico.eu",
- "arcblock-ico.org",
- "xn--fuson-1sa.org",
- "refereum-token.com",
- "fintrux.co",
- "ico-ton.org",
- "xn--mytherwallt-cbbv.com",
- "xmoneta.co",
- "data-wallet.co",
- "tokensale.data-wallet.co",
- "xn--myeerhwallot-ooc.com",
- "xn--myeterwalet-cm8epi.com",
- "xn--myeterwalle-cm8ev6a.com",
- "rnyetherumwallet.com",
- "republic-protocol.net",
- "nyeihitervvallatt.com",
- "arcblock.eu",
- "republicprotocol.eu",
- "tokensale-fusion.com",
- "myetherwalletjoin.com",
- "medicalchian.com",
- "myeahteirwaliet.com",
- "myenhtersvvailct.com",
- "trinity-token.com",
- "xn--eo-yzs.com",
- "zilliqa.in",
- "sparc.pro",
- "myetherwallet.import-tokens.com",
- "token-gram.org",
- "xn--shapshift-e4a.com",
- "xn--shapshift-y4a.com",
- "xn--shpeshift-c2a.com",
- "xn--shpeshift-r1a.com",
- "xn--shapshift-o4a.com",
- "xn--shpeshift-w2a.com",
- "xn--shapeshft-w5a.com",
- "tokensale-fusion.org",
- "fusion-ico.com",
- "beetolen.com",
- "tokencrowdsale.online",
- "fusion.tokencrowdsale.online",
- "beetokem.com",
- "block.chaiins.in",
- "origintrail.in",
- "bit-z.ru",
- "xn--myetherallet-nu5f.com",
- "xn--mytherwalet-3qb08c.com",
- "xn--myeterwllet-cm8et1d.com",
- "xn--mytherwllet-q7a01e.com",
- "xn--biance-xt7b.com",
- "xn--bnance-wic.com",
- "xn--biance-jeb.com",
- "xn--bttrx-9za8334c.com",
- "wwwkodakcoin.com",
- "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"
- ]
-}
diff --git a/test/unit/actions/tx_test.js b/test/unit/actions/tx_test.js
index b6a691860..c110f71fc 100644
--- a/test/unit/actions/tx_test.js
+++ b/test/unit/actions/tx_test.js
@@ -9,7 +9,7 @@ var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'redu
describe('tx confirmation screen', function () {
beforeEach(function () {
- this.sinon = sinon.sandbox.create()
+ this.sinon = sinon.createSandbox()
})
afterEach(function () {
diff --git a/test/unit/ComposableObservableStore.js b/test/unit/app/ComposableObservableStore.js
index 3fba200c1..aa8abd463 100644
--- a/test/unit/ComposableObservableStore.js
+++ b/test/unit/app/ComposableObservableStore.js
@@ -1,5 +1,5 @@
const assert = require('assert')
-const ComposableObservableStore = require('../../app/scripts/lib/ComposableObservableStore')
+const ComposableObservableStore = require('../../../app/scripts/lib/ComposableObservableStore')
const ObservableStore = require('obs-store')
describe('ComposableObservableStore', () => {
diff --git a/test/unit/app/account-import-strategies.spec.js b/test/unit/app/account-import-strategies.spec.js
new file mode 100644
index 000000000..83cfaeb3e
--- /dev/null
+++ b/test/unit/app/account-import-strategies.spec.js
@@ -0,0 +1,31 @@
+const assert = require('assert')
+const path = require('path')
+const accountImporter = require('../../../app/scripts/account-import-strategies/index')
+const ethUtil = require('ethereumjs-util')
+
+describe('Account Import Strategies', function () {
+ const privkey = '0x4cfd3e90fc78b0f86bf7524722150bb8da9c60cd532564d7ff43f5716514f553'
+ const json = '{"version":3,"id":"dbb54385-0a99-437f-83c0-647de9f244c3","address":"a7f92ce3fba24196cf6f4bd2e1eb3db282ba998c","Crypto":{"ciphertext":"bde13d9ade5c82df80281ca363320ce254a8a3a06535bbf6ffdeaf0726b1312c","cipherparams":{"iv":"fbf93718a57f26051b292f072f2e5b41"},"cipher":"aes-128-ctr","kdf":"scrypt","kdfparams":{"dklen":32,"salt":"7ffe00488319dec48e4c49a120ca49c6afbde9272854c64d9541c83fc6acdffe","n":8192,"r":8,"p":1},"mac":"2adfd9c4bc1cdac4c85bddfb31d9e21a684e0e050247a70c5698facf6b7d4681"}}'
+
+ it('imports a private key and strips 0x prefix', async function () {
+ const importPrivKey = await accountImporter.importAccount('Private Key', [ privkey ])
+ assert.equal(importPrivKey, ethUtil.stripHexPrefix(privkey))
+ })
+
+ it('fails when password is incorrect for keystore', async function () {
+ const wrongPassword = 'password2'
+
+ try {
+ await accountImporter.importAccount('JSON File', [ json, wrongPassword])
+ } catch (error) {
+ assert.equal(error.message, 'Key derivation failed - possibly wrong passphrase')
+ }
+ })
+
+ it('imports json string and password to return a private key', async function () {
+ const fileContentsPassword = 'password1'
+ const importJson = await accountImporter.importAccount('JSON File', [ json, fileContentsPassword])
+ assert.equal(importJson, '0x5733876abe94146069ce8bcbabbde2677f2e35fa33e875e92041ed2ac87e5bc7')
+ })
+
+})
diff --git a/test/unit/app/buy-eth-url.spec.js b/test/unit/app/buy-eth-url.spec.js
new file mode 100644
index 000000000..36646fa68
--- /dev/null
+++ b/test/unit/app/buy-eth-url.spec.js
@@ -0,0 +1,48 @@
+const assert = require('assert')
+const getBuyEthUrl = require('../../../app/scripts/lib/buy-eth-url')
+
+describe('', function () {
+ const mainnet = {
+ network: '1',
+ amount: 5,
+ address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
+ }
+ const ropsten = {
+ network: '3',
+ }
+ const rinkeby = {
+ network: '4',
+ }
+ const kovan = {
+ network: '42',
+ }
+
+ it('returns coinbase url with amount and address for network 1', function () {
+ const coinbaseUrl = getBuyEthUrl(mainnet)
+ const coinbase = coinbaseUrl.match(/(https:\/\/buy.coinbase.com)/)
+ const amount = coinbaseUrl.match(/(amount)\D\d/)
+ const address = coinbaseUrl.match(/(address)(.*)(?=&)/)
+
+ assert.equal(coinbase[0], 'https://buy.coinbase.com')
+ assert.equal(amount[0], 'amount=5')
+ assert.equal(address[0], 'address=0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc')
+
+ })
+
+ it('returns metamask ropsten faucet for network 3', function () {
+ const ropstenUrl = getBuyEthUrl(ropsten)
+ assert.equal(ropstenUrl, 'https://faucet.metamask.io/')
+ })
+
+ it('returns rinkeby dapp for network 4', function () {
+ const rinkebyUrl = getBuyEthUrl(rinkeby)
+ assert.equal(rinkebyUrl, 'https://www.rinkeby.io/')
+ })
+
+ it('returns kovan github test faucet for network 42', function () {
+ const kovanUrl = getBuyEthUrl(kovan)
+ assert.equal(kovanUrl, 'https://github.com/kovan-testnet/faucet')
+ })
+
+})
+
diff --git a/test/unit/address-book-controller.js b/test/unit/app/controllers/address-book-controller.js
index e48e69d98..dc4b8e3ff 100644
--- a/test/unit/address-book-controller.js
+++ b/test/unit/app/controllers/address-book-controller.js
@@ -1,5 +1,5 @@
const assert = require('assert')
-const AddressBookController = require('../../app/scripts/controllers/address-book')
+const AddressBookController = require('../../../../app/scripts/controllers/address-book')
const stubPreferencesStore = {
getState: function () {
diff --git a/test/unit/blacklist-controller-test.js b/test/unit/app/controllers/blacklist-controller-test.js
index cbf73d3e5..085641777 100644
--- a/test/unit/blacklist-controller-test.js
+++ b/test/unit/app/controllers/blacklist-controller-test.js
@@ -1,5 +1,5 @@
const assert = require('assert')
-const BlacklistController = require('../../app/scripts/controllers/blacklist')
+const BlacklistController = require('../../../../app/scripts/controllers/blacklist')
describe('blacklist controller', function () {
let blacklistController
diff --git a/test/unit/currency-controller-test.js b/test/unit/app/controllers/currency-controller-test.js
index 63ab60f9e..1941d1c43 100644
--- a/test/unit/currency-controller-test.js
+++ b/test/unit/app/controllers/currency-controller-test.js
@@ -3,7 +3,7 @@ global.fetch = global.fetch || require('isomorphic-fetch')
const assert = require('assert')
const nock = require('nock')
-const CurrencyController = require('../../app/scripts/controllers/currency')
+const CurrencyController = require('../../../../app/scripts/controllers/currency')
describe('currency-controller', function () {
var currencyController
@@ -45,7 +45,6 @@ describe('currency-controller', function () {
currencyController.updateConversionRate()
.then(function () {
var result = currencyController.getConversionRate()
- console.log('currencyController.getConversionRate:', result)
assert.equal(typeof result, 'number')
done()
}).catch(function (err) {
diff --git a/test/unit/infura-controller-test.js b/test/unit/app/controllers/infura-controller-test.js
index 605305efa..7bd95dd4b 100644
--- a/test/unit/infura-controller-test.js
+++ b/test/unit/app/controllers/infura-controller-test.js
@@ -1,6 +1,6 @@
const assert = require('assert')
const sinon = require('sinon')
-const InfuraController = require('../../app/scripts/controllers/infura')
+const InfuraController = require('../../../../app/scripts/controllers/infura')
describe('infura-controller', function () {
let infuraController, sandbox, networkStatus
@@ -8,7 +8,7 @@ describe('infura-controller', function () {
before(async function () {
infuraController = new InfuraController()
- sandbox = sinon.sandbox.create()
+ sandbox = sinon.createSandbox()
sinon.stub(infuraController, 'checkInfuraNetworkStatus').resolves(response)
networkStatus = await infuraController.checkInfuraNetworkStatus()
})
diff --git a/test/unit/app/controllers/metamask-controller-test.js b/test/unit/app/controllers/metamask-controller-test.js
new file mode 100644
index 000000000..4bc16e65e
--- /dev/null
+++ b/test/unit/app/controllers/metamask-controller-test.js
@@ -0,0 +1,550 @@
+const assert = require('assert')
+const sinon = require('sinon')
+const clone = require('clone')
+const nock = require('nock')
+const createThoughStream = require('through2').obj
+const MetaMaskController = require('../../../../app/scripts/metamask-controller')
+const blacklistJSON = require('eth-phishing-detect/src/config')
+const firstTimeState = require('../../../../app/scripts/first-time-state')
+
+const currentNetworkId = 42
+const DEFAULT_LABEL = 'Account 1'
+const TEST_SEED = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium'
+const TEST_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'
+const TEST_SEED_ALT = 'setup olympic issue mobile velvet surge alcohol burger horse view reopen gentle'
+const TEST_ADDRESS_ALT = '0xc42edfcc21ed14dda456aa0756c153f7985d8813'
+
+describe('MetaMaskController', function () {
+ let metamaskController
+ const sandbox = sinon.createSandbox()
+ const noop = () => {}
+
+ beforeEach(function () {
+
+ nock('https://api.infura.io')
+ .persist()
+ .get('/v2/blacklist')
+ .reply(200, blacklistJSON)
+
+ nock('https://api.infura.io')
+ .get('/v1/ticker/ethusd')
+ .reply(200, '{"base": "ETH", "quote": "USD", "bid": 288.45, "ask": 288.46, "volume": 112888.17569277, "exchange": "bitfinex", "total_volume": 272175.00106721005, "num_exchanges": 8, "timestamp": 1506444677}')
+
+ nock('https://api.infura.io')
+ .get('/v1/ticker/ethjpy')
+ .reply(200, '{"base": "ETH", "quote": "JPY", "bid": 32300.0, "ask": 32400.0, "volume": 247.4616071, "exchange": "kraken", "total_volume": 247.4616071, "num_exchanges": 1, "timestamp": 1506444676}')
+
+ nock('https://api.infura.io')
+ .persist()
+ .get(/.*/)
+ .reply(200)
+
+ metamaskController = new MetaMaskController({
+ showUnapprovedTx: noop,
+ showUnconfirmedMessage: noop,
+ encryptor: {
+ encrypt: function (password, object) {
+ this.object = object
+ return Promise.resolve()
+ },
+ decrypt: function () {
+ return Promise.resolve(this.object)
+ },
+ },
+ initState: clone(firstTimeState),
+ })
+ sandbox.spy(metamaskController.keyringController, 'createNewVaultAndKeychain')
+ sandbox.spy(metamaskController.keyringController, 'createNewVaultAndRestore')
+ })
+
+ afterEach(function () {
+ nock.cleanAll()
+ sandbox.restore()
+ })
+
+ describe('#getGasPrice', function () {
+
+ it('gives the 50th percentile lowest accepted gas price from recentBlocksController', async function () {
+ const realRecentBlocksController = metamaskController.recentBlocksController
+ metamaskController.recentBlocksController = {
+ store: {
+ getState: () => {
+ return {
+ recentBlocks: [
+ { gasPrices: [ '0x3b9aca00', '0x174876e800'] },
+ { gasPrices: [ '0x3b9aca00', '0x174876e800'] },
+ { gasPrices: [ '0x174876e800', '0x174876e800' ]},
+ { gasPrices: [ '0x174876e800', '0x174876e800' ]},
+ ],
+ }
+ },
+ },
+ }
+
+ const gasPrice = metamaskController.getGasPrice()
+ assert.equal(gasPrice, '0x3b9aca00', 'accurately estimates 50th percentile accepted gas price')
+
+ metamaskController.recentBlocksController = realRecentBlocksController
+ })
+ })
+
+ describe('#createNewVaultAndKeychain', function () {
+ it('can only create new vault on keyringController once', async function () {
+ const selectStub = sandbox.stub(metamaskController, 'selectFirstIdentity')
+
+ const password = 'a-fake-password'
+
+ await metamaskController.createNewVaultAndKeychain(password)
+ await metamaskController.createNewVaultAndKeychain(password)
+
+ assert(metamaskController.keyringController.createNewVaultAndKeychain.calledOnce)
+
+ selectStub.reset()
+ })
+ })
+
+ describe('#createNewVaultAndRestore', function () {
+ it('should be able to call newVaultAndRestore despite a mistake.', async function () {
+ const password = 'what-what-what'
+ await metamaskController.createNewVaultAndRestore(password, TEST_SEED.slice(0, -1)).catch((e) => null)
+ await metamaskController.createNewVaultAndRestore(password, TEST_SEED)
+
+ assert(metamaskController.keyringController.createNewVaultAndRestore.calledTwice)
+ })
+
+ it('should clear previous identities after vault restoration', async () => {
+ await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED)
+ assert.deepEqual(metamaskController.getState().identities, {
+ [TEST_ADDRESS]: { address: TEST_ADDRESS, name: DEFAULT_LABEL },
+ })
+
+ await metamaskController.preferencesController.setAccountLabel(TEST_ADDRESS, 'Account Foo')
+ assert.deepEqual(metamaskController.getState().identities, {
+ [TEST_ADDRESS]: { address: TEST_ADDRESS, name: 'Account Foo' },
+ })
+
+ await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED_ALT)
+ assert.deepEqual(metamaskController.getState().identities, {
+ [TEST_ADDRESS_ALT]: { address: TEST_ADDRESS_ALT, name: DEFAULT_LABEL },
+ })
+ })
+ })
+
+ describe('#getApi', function () {
+ let getApi, state
+
+ beforeEach(function () {
+ getApi = metamaskController.getApi()
+ })
+
+ it('getState', function (done) {
+ getApi.getState((err, res) => {
+ if (err) {
+ done(err)
+ } else {
+ state = res
+ }
+ })
+ assert.deepEqual(state, metamaskController.getState())
+ done()
+ })
+
+ })
+
+ describe('preferencesController', function () {
+
+ it('defaults useBlockie to false', function () {
+ assert.equal(metamaskController.preferencesController.store.getState().useBlockie, false)
+ })
+
+ it('setUseBlockie to true', function () {
+ metamaskController.setUseBlockie(true, noop)
+ assert.equal(metamaskController.preferencesController.store.getState().useBlockie, true)
+ })
+
+ })
+
+ describe('#selectFirstIdentity', function () {
+ let identities, address
+
+ beforeEach(function () {
+ address = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'
+ identities = {
+ '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': {
+ 'address': address,
+ 'name': 'Account 1',
+ },
+ '0xc42edfcc21ed14dda456aa0756c153f7985d8813': {
+ 'address': '0xc42edfcc21ed14dda456aa0756c153f7985d8813',
+ 'name': 'Account 2',
+ },
+ }
+ metamaskController.preferencesController.store.updateState({ identities })
+ metamaskController.selectFirstIdentity()
+ })
+
+ it('changes preferences controller select address', function () {
+ const preferenceControllerState = metamaskController.preferencesController.store.getState()
+ assert.equal(preferenceControllerState.selectedAddress, address)
+ })
+
+ it('changes metamask controller selected address', function () {
+ const metamaskState = metamaskController.getState()
+ assert.equal(metamaskState.selectedAddress, address)
+ })
+ })
+
+ describe('#setCustomRpc', function () {
+ const customRPC = 'https://custom.rpc/'
+ let rpcTarget
+
+ beforeEach(function () {
+
+ nock('https://custom.rpc')
+ .post('/')
+ .reply(200)
+
+ rpcTarget = metamaskController.setCustomRpc(customRPC)
+ })
+
+ afterEach(function () {
+ nock.cleanAll()
+ })
+
+ it('returns custom RPC that when called', async function () {
+ assert.equal(await rpcTarget, customRPC)
+ })
+
+ it('changes the network controller rpc', function () {
+ const networkControllerState = metamaskController.networkController.store.getState()
+ assert.equal(networkControllerState.provider.rpcTarget, customRPC)
+ })
+ })
+
+ describe('#setCurrentCurrency', function () {
+ let defaultMetaMaskCurrency
+
+ beforeEach(function () {
+ defaultMetaMaskCurrency = metamaskController.currencyController.getCurrentCurrency()
+ })
+
+ it('defaults to usd', function () {
+ assert.equal(defaultMetaMaskCurrency, 'usd')
+ })
+
+ it('sets currency to JPY', function () {
+ metamaskController.setCurrentCurrency('JPY', noop)
+ assert.equal(metamaskController.currencyController.getCurrentCurrency(), 'JPY')
+ })
+ })
+
+ describe('#createShapeshifttx', function () {
+ let depositAddress, depositType, shapeShiftTxList
+
+ beforeEach(function () {
+ nock('https://shapeshift.io')
+ .get('/txStat/3EevLFfB4H4XMWQwYCgjLie1qCAGpd2WBc')
+ .reply(200, '{"status": "no_deposits", "address": "3EevLFfB4H4XMWQwYCgjLie1qCAGpd2WBc"}')
+
+ depositAddress = '3EevLFfB4H4XMWQwYCgjLie1qCAGpd2WBc'
+ depositType = 'ETH'
+ shapeShiftTxList = metamaskController.shapeshiftController.store.getState().shapeShiftTxList
+ })
+
+ it('creates a shapeshift tx', async function () {
+ metamaskController.createShapeShiftTx(depositAddress, depositType)
+ assert.equal(shapeShiftTxList[0].depositAddress, depositAddress)
+ })
+
+ })
+
+ describe('#addNewAccount', function () {
+ let addNewAccount
+
+ beforeEach(function () {
+ addNewAccount = metamaskController.addNewAccount()
+ })
+
+ it('errors when an primary keyring is does not exist', async function () {
+ try {
+ await addNewAccount
+ assert.equal(1 === 0)
+ } catch (e) {
+ assert.equal(e.message, 'MetamaskController - No HD Key Tree found')
+ }
+ })
+ })
+
+ describe('#verifyseedPhrase', function () {
+ let seedPhrase, getConfigSeed
+
+ it('errors when no keying is provided', async function () {
+ try {
+ await metamaskController.verifySeedPhrase()
+ } catch (error) {
+ assert.equal(error.message, 'MetamaskController - No HD Key Tree found')
+ }
+ })
+
+ beforeEach(async function () {
+ await metamaskController.createNewVaultAndKeychain('password')
+ seedPhrase = await metamaskController.verifySeedPhrase()
+ })
+
+ it('#placeSeedWords should match the initially created vault seed', function () {
+
+ metamaskController.placeSeedWords((err, result) => {
+ if (err) {
+ console.log(err)
+ } else {
+ getConfigSeed = metamaskController.configManager.getSeedWords()
+ assert.equal(result, seedPhrase)
+ assert.equal(result, getConfigSeed)
+ }
+ })
+ assert.equal(getConfigSeed, undefined)
+ })
+
+ it('#addNewAccount', async function () {
+ await metamaskController.addNewAccount()
+ const getAccounts = await metamaskController.keyringController.getAccounts()
+ assert.equal(getAccounts.length, 2)
+ })
+ })
+
+ describe('#resetAccount', function () {
+
+ beforeEach(function () {
+ const selectedAddressStub = sinon.stub(metamaskController.preferencesController, 'getSelectedAddress')
+ const getNetworkstub = sinon.stub(metamaskController.txController.txStateManager, 'getNetwork')
+
+ selectedAddressStub.returns('0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc')
+ getNetworkstub.returns(42)
+
+ metamaskController.txController.txStateManager._saveTxList([
+ { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {from: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'} },
+ { id: 2, status: 'rejected', metamaskNetworkId: 32, txParams: {} },
+ { id: 3, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {from: '0xB09d8505E1F4EF1CeA089D47094f5DD3464083d4'} },
+ ])
+ })
+
+ it('wipes transactions from only the correct network id and with the selected address', async function () {
+ await metamaskController.resetAccount()
+ assert.equal(metamaskController.txController.txStateManager.getTx(1), undefined)
+ })
+ })
+
+ describe('#clearSeedWordCache', function () {
+
+ it('should have set seed words', function () {
+ metamaskController.configManager.setSeedWords('test words')
+ const getConfigSeed = metamaskController.configManager.getSeedWords()
+ assert.equal(getConfigSeed, 'test words')
+ })
+
+ it('should clear config seed phrase', function () {
+ metamaskController.configManager.setSeedWords('test words')
+ metamaskController.clearSeedWordCache((err, result) => {
+ if (err) console.log(err)
+ })
+ const getConfigSeed = metamaskController.configManager.getSeedWords()
+ assert.equal(getConfigSeed, null)
+ })
+
+ })
+
+ describe('#setCurrentLocale', function () {
+
+ it('checks the default currentLocale', function () {
+ const preferenceCurrentLocale = metamaskController.preferencesController.store.getState().currentLocale
+ assert.equal(preferenceCurrentLocale, undefined)
+ })
+
+ it('sets current locale in preferences controller', function () {
+ metamaskController.setCurrentLocale('ja', noop)
+ const preferenceCurrentLocale = metamaskController.preferencesController.store.getState().currentLocale
+ assert.equal(preferenceCurrentLocale, 'ja')
+ })
+
+ })
+
+ describe('#newUnsignedMessage', function () {
+
+ let msgParams, metamaskMsgs, messages, msgId
+
+ const address = '0xc42edfcc21ed14dda456aa0756c153f7985d8813'
+ const data = '0x43727970746f6b697474696573'
+
+ beforeEach(async function () {
+
+ await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED_ALT)
+
+ msgParams = {
+ 'from': address,
+ 'data': data,
+ }
+
+ metamaskController.newUnsignedMessage(msgParams, noop)
+ metamaskMsgs = metamaskController.messageManager.getUnapprovedMsgs()
+ messages = metamaskController.messageManager.messages
+ msgId = Object.keys(metamaskMsgs)[0]
+ messages[0].msgParams.metamaskId = parseInt(msgId)
+ })
+
+ it('persists address from msg params', function () {
+ assert.equal(metamaskMsgs[msgId].msgParams.from, address)
+ })
+
+ it('persists data from msg params', function () {
+ assert.equal(metamaskMsgs[msgId].msgParams.data, data)
+ })
+
+ it('sets the status to unapproved', function () {
+ assert.equal(metamaskMsgs[msgId].status, 'unapproved')
+ })
+
+ it('sets the type to eth_sign', function () {
+ assert.equal(metamaskMsgs[msgId].type, 'eth_sign')
+ })
+
+ it('rejects the message', function () {
+ const msgIdInt = parseInt(msgId)
+ metamaskController.cancelMessage(msgIdInt, noop)
+ assert.equal(messages[0].status, 'rejected')
+ })
+
+ it('errors when signing a message', async function () {
+ try {
+ await metamaskController.signMessage(messages[0].msgParams)
+ } catch (error) {
+ assert.equal(error.message, 'message length is invalid')
+ }
+ })
+ })
+
+ describe('#newUnsignedPersonalMessage', function () {
+
+ it('errors with no from in msgParams', function () {
+ const msgParams = {
+ 'data': data,
+ }
+ metamaskController.newUnsignedPersonalMessage(msgParams, function (error) {
+ assert.equal(error.message, 'MetaMask Message Signature: from field is required.')
+ })
+ })
+
+ let msgParams, metamaskPersonalMsgs, personalMessages, msgId
+
+ const address = '0xc42edfcc21ed14dda456aa0756c153f7985d8813'
+ const data = '0x43727970746f6b697474696573'
+
+ beforeEach(async function () {
+
+ await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED_ALT)
+
+ msgParams = {
+ 'from': address,
+ 'data': data,
+ }
+
+ metamaskController.newUnsignedPersonalMessage(msgParams, noop)
+ metamaskPersonalMsgs = metamaskController.personalMessageManager.getUnapprovedMsgs()
+ personalMessages = metamaskController.personalMessageManager.messages
+ msgId = Object.keys(metamaskPersonalMsgs)[0]
+ personalMessages[0].msgParams.metamaskId = parseInt(msgId)
+ })
+
+ it('persists address from msg params', function () {
+ assert.equal(metamaskPersonalMsgs[msgId].msgParams.from, address)
+ })
+
+ it('persists data from msg params', function () {
+ assert.equal(metamaskPersonalMsgs[msgId].msgParams.data, data)
+ })
+
+ it('sets the status to unapproved', function () {
+ assert.equal(metamaskPersonalMsgs[msgId].status, 'unapproved')
+ })
+
+ it('sets the type to personal_sign', function () {
+ assert.equal(metamaskPersonalMsgs[msgId].type, 'personal_sign')
+ })
+
+ it('rejects the message', function () {
+ const msgIdInt = parseInt(msgId)
+ metamaskController.cancelPersonalMessage(msgIdInt, noop)
+ assert.equal(personalMessages[0].status, 'rejected')
+ })
+
+ it('errors when signing a message', async function () {
+ await metamaskController.signPersonalMessage(personalMessages[0].msgParams)
+ assert.equal(metamaskPersonalMsgs[msgId].status, 'signed')
+ assert.equal(metamaskPersonalMsgs[msgId].rawSig, '0x6a1b65e2b8ed53cf398a769fad24738f9fbe29841fe6854e226953542c4b6a173473cb152b6b1ae5f06d601d45dd699a129b0a8ca84e78b423031db5baa734741b')
+ })
+ })
+
+ describe('#setupUntrustedCommunication', function () {
+ let streamTest
+
+ const phishingUrl = 'decentral.market'
+
+ afterEach(function () {
+ streamTest.end()
+ })
+
+ it('sets up phishing stream for untrusted communication ', async function () {
+ await metamaskController.blacklistController.updatePhishingList()
+
+ streamTest = createThoughStream((chunk, enc, cb) => {
+ assert.equal(chunk.name, 'phishing')
+ assert.equal(chunk.data.hostname, phishingUrl)
+ cb()
+ })
+ // console.log(streamTest)
+ metamaskController.setupUntrustedCommunication(streamTest, phishingUrl)
+ })
+ })
+
+ describe('#setupTrustedCommunication', function () {
+ let streamTest
+
+ afterEach(function () {
+ streamTest.end()
+ })
+
+ it('sets up controller dnode api for trusted communication', function (done) {
+ streamTest = createThoughStream((chunk, enc, cb) => {
+ assert.equal(chunk.name, 'controller')
+ cb()
+ done()
+ })
+
+ metamaskController.setupTrustedCommunication(streamTest, 'mycrypto.com')
+ })
+ })
+
+ describe('#markAccountsFound', function () {
+ it('adds lost accounts to config manager data', function () {
+ metamaskController.markAccountsFound(noop)
+ const configManagerData = metamaskController.configManager.getData()
+ assert.deepEqual(configManagerData.lostAccounts, [])
+ })
+ })
+
+ describe('#markPasswordForgotten', function () {
+ it('adds and sets forgottenPassword to config data to true', function () {
+ metamaskController.markPasswordForgotten(noop)
+ const configManagerData = metamaskController.configManager.getData()
+ assert.equal(configManagerData.forgottenPassword, true)
+ })
+ })
+
+ describe('#unMarkPasswordForgotten', function () {
+ it('adds and sets forgottenPassword to config data to false', function () {
+ metamaskController.unMarkPasswordForgotten(noop)
+ const configManagerData = metamaskController.configManager.getData()
+ assert.equal(configManagerData.forgottenPassword, false)
+ })
+ })
+
+})
diff --git a/test/unit/network-contoller-test.js b/test/unit/app/controllers/network-contoller-test.js
index 2d590a3f6..789850ef3 100644
--- a/test/unit/network-contoller-test.js
+++ b/test/unit/app/controllers/network-contoller-test.js
@@ -1,11 +1,11 @@
const assert = require('assert')
const nock = require('nock')
-const NetworkController = require('../../app/scripts/controllers/network')
+const NetworkController = require('../../../../app/scripts/controllers/network')
const {
getNetworkDisplayName,
-} = require('../../app/scripts/controllers/network/util')
+} = require('../../../../app/scripts/controllers/network/util')
-const { createTestProviderTools } = require('../stub/provider')
+const { createTestProviderTools } = require('../../../stub/provider')
const providerResultStub = {}
describe('# Network Controller', function () {
diff --git a/test/unit/notice-controller-test.js b/test/unit/app/controllers/notice-controller-test.js
index 09eeda15c..e78b69623 100644
--- a/test/unit/notice-controller-test.js
+++ b/test/unit/app/controllers/notice-controller-test.js
@@ -1,6 +1,6 @@
const assert = require('assert')
-const configManagerGen = require('../lib/mock-config-manager')
-const NoticeController = require('../../app/scripts/notice-controller')
+const configManagerGen = require('../../../lib/mock-config-manager')
+const NoticeController = require('../../../../app/scripts/notice-controller')
describe('notice-controller', function () {
var noticeController
diff --git a/test/unit/preferences-controller-test.js b/test/unit/app/controllers/preferences-controller-test.js
index c613c68f9..e5e751b57 100644
--- a/test/unit/preferences-controller-test.js
+++ b/test/unit/app/controllers/preferences-controller-test.js
@@ -1,5 +1,5 @@
const assert = require('assert')
-const PreferencesController = require('../../app/scripts/controllers/preferences')
+const PreferencesController = require('../../../../app/scripts/controllers/preferences')
describe('preferences controller', function () {
let preferencesController
diff --git a/test/unit/token-rates-controller.js b/test/unit/app/controllers/token-rates-controller.js
index a49547313..28e583d8d 100644
--- a/test/unit/token-rates-controller.js
+++ b/test/unit/app/controllers/token-rates-controller.js
@@ -1,6 +1,6 @@
const assert = require('assert')
const sinon = require('sinon')
-const TokenRatesController = require('../../app/scripts/controllers/token-rates')
+const TokenRatesController = require('../../../../app/scripts/controllers/token-rates')
const ObservableStore = require('obs-store')
describe('TokenRatesController', () => {
diff --git a/test/unit/nonce-tracker-test.js b/test/unit/app/controllers/transactions/nonce-tracker-test.js
index cf26945d3..fc852458c 100644
--- a/test/unit/nonce-tracker-test.js
+++ b/test/unit/app/controllers/transactions/nonce-tracker-test.js
@@ -1,6 +1,6 @@
const assert = require('assert')
-const NonceTracker = require('../../app/scripts/controllers/transactions/nonce-tracker')
-const MockTxGen = require('../lib/mock-tx-gen')
+const NonceTracker = require('../../../../../app/scripts/controllers/transactions/nonce-tracker')
+const MockTxGen = require('../../../../lib/mock-tx-gen')
let providerResultStub = {}
describe('Nonce Tracker', function () {
diff --git a/test/unit/pending-tx-test.js b/test/unit/app/controllers/transactions/pending-tx-test.js
index 001b86dd1..e7705c594 100644
--- a/test/unit/pending-tx-test.js
+++ b/test/unit/app/controllers/transactions/pending-tx-test.js
@@ -3,9 +3,9 @@ const ethUtil = require('ethereumjs-util')
const EthTx = require('ethereumjs-tx')
const ObservableStore = require('obs-store')
const clone = require('clone')
-const { createTestProviderTools } = require('../stub/provider')
-const PendingTransactionTracker = require('../../app/scripts/controllers/transactions/pending-tx-tracker')
-const MockTxGen = require('../lib/mock-tx-gen')
+const { createTestProviderTools } = require('../../../../stub/provider')
+const PendingTransactionTracker = require('../../../../../app/scripts/controllers/transactions/pending-tx-tracker')
+const MockTxGen = require('../../../../lib/mock-tx-gen')
const sinon = require('sinon')
const noop = () => true
const currentNetworkId = 42
@@ -294,7 +294,7 @@ describe('PendingTransactionTracker', function () {
})
afterEach(() => {
- pendingTxTracker.publishTransaction.reset()
+ pendingTxTracker.publishTransaction.restore()
})
it('should publish the transaction', function (done) {
diff --git a/test/unit/tx-controller-test.js b/test/unit/app/controllers/transactions/tx-controller-test.js
index 0b5c7226a..1f32a0f37 100644
--- a/test/unit/tx-controller-test.js
+++ b/test/unit/app/controllers/transactions/tx-controller-test.js
@@ -4,9 +4,9 @@ const EthTx = require('ethereumjs-tx')
const EthjsQuery = require('ethjs-query')
const ObservableStore = require('obs-store')
const sinon = require('sinon')
-const TransactionController = require('../../app/scripts/controllers/transactions')
-const TxGasUtils = require('../../app/scripts/controllers/transactions/tx-gas-utils')
-const { createTestProviderTools, getTestAccounts } = require('../stub/provider')
+const TransactionController = require('../../../../../app/scripts/controllers/transactions')
+const TxGasUtils = require('../../../../../app/scripts/controllers/transactions/tx-gas-utils')
+const { createTestProviderTools, getTestAccounts } = require('../../../../stub/provider')
const noop = () => true
const currentNetworkId = 42
diff --git a/test/unit/tx-gas-util-test.js b/test/unit/app/controllers/transactions/tx-gas-util-test.js
index c1d5966da..d1ee86033 100644
--- a/test/unit/tx-gas-util-test.js
+++ b/test/unit/app/controllers/transactions/tx-gas-util-test.js
@@ -3,8 +3,8 @@ const Transaction = require('ethereumjs-tx')
const BN = require('bn.js')
-const { hexToBn, bnToHex } = require('../../app/scripts/lib/util')
-const TxUtils = require('../../app/scripts/controllers/transactions/tx-gas-utils')
+const { hexToBn, bnToHex } = require('../../../../../app/scripts/lib/util')
+const TxUtils = require('../../../../../app/scripts/controllers/transactions/tx-gas-utils')
describe('txUtils', function () {
diff --git a/test/unit/tx-helper-test.js b/test/unit/app/controllers/transactions/tx-helper-test.js
index cc6543c30..ce54ef483 100644
--- a/test/unit/tx-helper-test.js
+++ b/test/unit/app/controllers/transactions/tx-helper-test.js
@@ -1,5 +1,5 @@
const assert = require('assert')
-const txHelper = require('../../ui/lib/tx-helper')
+const txHelper = require('../../../../../ui/lib/tx-helper')
describe('txHelper', function () {
it('always shows the oldest tx first', function () {
diff --git a/test/unit/tx-state-history-helper-test.js b/test/unit/app/controllers/transactions/tx-state-history-helper-test.js
index 5ad014dbb..f4c3a6be1 100644
--- a/test/unit/tx-state-history-helper-test.js
+++ b/test/unit/app/controllers/transactions/tx-state-history-helper-test.js
@@ -1,6 +1,6 @@
const assert = require('assert')
-const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper')
-const testVault = require('../data/v17-long-history.json')
+const txStateHistoryHelper = require('../../../../../app/scripts/controllers/transactions/lib/tx-state-history-helper')
+const testVault = require('../../../../data/v17-long-history.json')
describe ('Transaction state history helper', function () {
diff --git a/test/unit/tx-state-manager-test.js b/test/unit/app/controllers/transactions/tx-state-manager-test.js
index 179542f90..20bc08b94 100644
--- a/test/unit/tx-state-manager-test.js
+++ b/test/unit/app/controllers/transactions/tx-state-manager-test.js
@@ -1,8 +1,8 @@
const assert = require('assert')
const clone = require('clone')
const ObservableStore = require('obs-store')
-const TxStateManager = require('../../app/scripts/controllers/transactions/tx-state-manager')
-const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper')
+const TxStateManager = require('../../../../../app/scripts/controllers/transactions/tx-state-manager')
+const txStateHistoryHelper = require('../../../../../app/scripts/controllers/transactions/lib/tx-state-history-helper')
const noop = () => true
describe('TransactionStateManager', function () {
diff --git a/test/unit/tx-utils-test.js b/test/unit/app/controllers/transactions/tx-utils-test.js
index be16225ba..115127f85 100644
--- a/test/unit/tx-utils-test.js
+++ b/test/unit/app/controllers/transactions/tx-utils-test.js
@@ -1,5 +1,5 @@
const assert = require('assert')
-const txUtils = require('../../app/scripts/controllers/transactions/lib/util')
+const txUtils = require('../../../../../app/scripts/controllers/transactions/lib/util')
describe('txUtils', function () {
diff --git a/test/unit/edge-encryptor-test.js b/test/unit/app/edge-encryptor-test.js
index d3f014d74..cc9777389 100644
--- a/test/unit/edge-encryptor-test.js
+++ b/test/unit/app/edge-encryptor-test.js
@@ -1,6 +1,6 @@
const assert = require('assert')
-const EdgeEncryptor = require('../../app/scripts/edge-encryptor')
+const EdgeEncryptor = require('../../../app/scripts/edge-encryptor')
var password = 'passw0rd1'
var data = 'some random data'
diff --git a/test/unit/message-manager-test.js b/test/unit/app/message-manager-test.js
index 5e7039841..36ef6c29f 100644
--- a/test/unit/message-manager-test.js
+++ b/test/unit/app/message-manager-test.js
@@ -1,5 +1,5 @@
const assert = require('assert')
-const MessageManager = require('../../app/scripts/lib/message-manager')
+const MessageManager = require('../../../app/scripts/lib/message-manager')
describe('Message Manager', function () {
let messageManager
diff --git a/test/unit/nodeify-test.js b/test/unit/app/nodeify-test.js
index c7b127889..901603c8b 100644
--- a/test/unit/nodeify-test.js
+++ b/test/unit/app/nodeify-test.js
@@ -1,5 +1,5 @@
const assert = require('assert')
-const nodeify = require('../../app/scripts/lib/nodeify')
+const nodeify = require('../../../app/scripts/lib/nodeify')
describe('nodeify', function () {
var obj = {
diff --git a/test/unit/pending-balance-test.js b/test/unit/app/pending-balance-test.js
index dc4c1c3e4..1418e4a4e 100644
--- a/test/unit/pending-balance-test.js
+++ b/test/unit/app/pending-balance-test.js
@@ -1,6 +1,6 @@
const assert = require('assert')
-const PendingBalanceCalculator = require('../../app/scripts/lib/pending-balance-calculator')
-const MockTxGen = require('../lib/mock-tx-gen')
+const PendingBalanceCalculator = require('../../../app/scripts/lib/pending-balance-calculator')
+const MockTxGen = require('../../lib/mock-tx-gen')
const BN = require('ethereumjs-util').BN
let providerResultStub = {}
diff --git a/test/unit/personal-message-manager-test.js b/test/unit/app/personal-message-manager-test.js
index ec2f9a4d1..b07167bff 100644
--- a/test/unit/personal-message-manager-test.js
+++ b/test/unit/app/personal-message-manager-test.js
@@ -1,6 +1,6 @@
const assert = require('assert')
-const PersonalMessageManager = require('../../app/scripts/lib/personal-message-manager')
+const PersonalMessageManager = require('../../../app/scripts/lib/personal-message-manager')
describe('Personal Message Manager', function () {
let messageManager
diff --git a/test/unit/seed-phrase-verifier-test.js b/test/unit/app/seed-phrase-verifier-test.js
index 4e314806b..b0da534da 100644
--- a/test/unit/seed-phrase-verifier-test.js
+++ b/test/unit/app/seed-phrase-verifier-test.js
@@ -1,9 +1,9 @@
const assert = require('assert')
const clone = require('clone')
const KeyringController = require('eth-keyring-controller')
-const firstTimeState = require('../../app/scripts/first-time-state')
-const seedPhraseVerifier = require('../../app/scripts/lib/seed-phrase-verifier')
-const mockEncryptor = require('../lib/mock-encryptor')
+const firstTimeState = require('../../../app/scripts/first-time-state')
+const seedPhraseVerifier = require('../../../app/scripts/lib/seed-phrase-verifier')
+const mockEncryptor = require('../../lib/mock-encryptor')
describe('SeedPhraseVerifier', function () {
diff --git a/test/unit/util-test.js b/test/unit/app/util-test.js
index 6da185b2c..670bc4d22 100644
--- a/test/unit/util-test.js
+++ b/test/unit/app/util-test.js
@@ -1,5 +1,5 @@
const assert = require('assert')
-const { sufficientBalance } = require('../../app/scripts/lib/util')
+const { sufficientBalance } = require('../../../app/scripts/lib/util')
describe('SufficientBalance', function () {
diff --git a/test/unit/metamask-controller-test.js b/test/unit/metamask-controller-test.js
deleted file mode 100644
index 649b26e1e..000000000
--- a/test/unit/metamask-controller-test.js
+++ /dev/null
@@ -1,120 +0,0 @@
-const assert = require('assert')
-const sinon = require('sinon')
-const clone = require('clone')
-const nock = require('nock')
-const MetaMaskController = require('../../app/scripts/metamask-controller')
-const blacklistJSON = require('../stub/blacklist')
-const firstTimeState = require('../../app/scripts/first-time-state')
-
-const DEFAULT_LABEL = 'Account 1'
-const TEST_SEED = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium'
-const TEST_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'
-const TEST_SEED_ALT = 'setup olympic issue mobile velvet surge alcohol burger horse view reopen gentle'
-const TEST_ADDRESS_ALT = '0xc42edfcc21ed14dda456aa0756c153f7985d8813'
-
-describe('MetaMaskController', function () {
- let metamaskController
- const sandbox = sinon.sandbox.create()
- const noop = () => { }
-
- beforeEach(function () {
-
- nock('https://api.infura.io')
- .persist()
- .get('/v2/blacklist')
- .reply(200, blacklistJSON)
-
- nock('https://api.infura.io')
- .persist()
- .get(/.*/)
- .reply(200)
-
- metamaskController = new MetaMaskController({
- showUnapprovedTx: noop,
- encryptor: {
- encrypt: function (password, object) {
- this.object = object
- return Promise.resolve()
- },
- decrypt: function () {
- return Promise.resolve(this.object)
- },
- },
- initState: clone(firstTimeState),
- })
- sandbox.spy(metamaskController.keyringController, 'createNewVaultAndKeychain')
- sandbox.spy(metamaskController.keyringController, 'createNewVaultAndRestore')
- })
-
- afterEach(function () {
- nock.cleanAll()
- sandbox.restore()
- })
-
- describe('#getGasPrice', function () {
- it('gives the 50th percentile lowest accepted gas price from recentBlocksController', async function () {
- const realRecentBlocksController = metamaskController.recentBlocksController
- metamaskController.recentBlocksController = {
- store: {
- getState: () => {
- return {
- recentBlocks: [
- { gasPrices: [ '0x3b9aca00', '0x174876e800'] },
- { gasPrices: [ '0x3b9aca00', '0x174876e800'] },
- { gasPrices: [ '0x174876e800', '0x174876e800' ]},
- { gasPrices: [ '0x174876e800', '0x174876e800' ]},
- ],
- }
- },
- },
- }
-
- const gasPrice = metamaskController.getGasPrice()
- assert.equal(gasPrice, '0x3b9aca00', 'accurately estimates 50th percentile accepted gas price')
-
- metamaskController.recentBlocksController = realRecentBlocksController
- })
- })
-
- describe('#createNewVaultAndKeychain', function () {
- it('can only create new vault on keyringController once', async function () {
- const selectStub = sandbox.stub(metamaskController, 'selectFirstIdentity')
-
- const password = 'a-fake-password'
-
- await metamaskController.createNewVaultAndKeychain(password)
- await metamaskController.createNewVaultAndKeychain(password)
-
- assert(metamaskController.keyringController.createNewVaultAndKeychain.calledOnce)
-
- selectStub.reset()
- })
- })
-
- describe('#createNewVaultAndRestore', function () {
- it('should be able to call newVaultAndRestore despite a mistake.', async function () {
- const password = 'what-what-what'
- await metamaskController.createNewVaultAndRestore(password, TEST_SEED.slice(0, -1)).catch((e) => null)
- await metamaskController.createNewVaultAndRestore(password, TEST_SEED)
-
- assert(metamaskController.keyringController.createNewVaultAndRestore.calledTwice)
- })
-
- it('should clear previous identities after vault restoration', async () => {
- await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED)
- assert.deepEqual(metamaskController.getState().identities, {
- [TEST_ADDRESS]: { address: TEST_ADDRESS, name: DEFAULT_LABEL },
- })
-
- await metamaskController.preferencesController.setAccountLabel(TEST_ADDRESS, 'Account Foo')
- assert.deepEqual(metamaskController.getState().identities, {
- [TEST_ADDRESS]: { address: TEST_ADDRESS, name: 'Account Foo' },
- })
-
- await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED_ALT)
- assert.deepEqual(metamaskController.getState().identities, {
- [TEST_ADDRESS_ALT]: { address: TEST_ADDRESS_ALT, name: DEFAULT_LABEL },
- })
- })
- })
-})
diff --git a/test/unit/migrations-test.js b/test/unit/migrations/migrations-test.js
index 5bad25a45..50afd9c2e 100644
--- a/test/unit/migrations-test.js
+++ b/test/unit/migrations/migrations-test.js
@@ -1,22 +1,22 @@
const assert = require('assert')
const path = require('path')
-const wallet1 = require(path.join('..', 'lib', 'migrations', '001.json'))
-const vault4 = require(path.join('..', 'lib', 'migrations', '004.json'))
+const wallet1 = require(path.join('..', '..', 'lib', 'migrations', '001.json'))
+const vault4 = require(path.join('..', '..', 'lib', 'migrations', '004.json'))
let vault5, vault6, vault7, vault8, vault9 // vault10, vault11
-const migration2 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '002'))
-const migration3 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '003'))
-const migration4 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '004'))
-const migration5 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '005'))
-const migration6 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '006'))
-const migration7 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '007'))
-const migration8 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '008'))
-const migration9 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '009'))
-const migration10 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '010'))
-const migration11 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '011'))
-const migration12 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '012'))
-const migration13 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '013'))
+const migration2 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '002'))
+const migration3 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '003'))
+const migration4 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '004'))
+const migration5 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '005'))
+const migration6 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '006'))
+const migration7 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '007'))
+const migration8 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '008'))
+const migration9 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '009'))
+const migration10 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '010'))
+const migration11 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '011'))
+const migration12 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '012'))
+const migration13 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '013'))
const oldTestRpc = 'https://rawtestrpc.metamask.io/'
diff --git a/test/unit/migrator-test.js b/test/unit/migrations/migrator-test.js
index 4404e1dc4..a9374dff1 100644
--- a/test/unit/migrator-test.js
+++ b/test/unit/migrations/migrator-test.js
@@ -1,7 +1,7 @@
const assert = require('assert')
const clone = require('clone')
-const Migrator = require('../../app/scripts/lib/migrator/')
-const liveMigrations = require('../../app/scripts/migrations/')
+const Migrator = require('../../../app/scripts/lib/migrator/')
+const liveMigrations = require('../../../app/scripts/migrations/')
const stubMigrations = [
{
version: 1,
@@ -33,7 +33,7 @@ const versionedData = {meta: {version: 0}, data: {hello: 'world'}}
const firstTimeState = {
meta: { version: 0 },
- data: require('../../app/scripts/first-time-state'),
+ data: require('../../../app/scripts/first-time-state'),
}
describe('Migrator', () => {
diff --git a/test/unit/nameForAccount_test.js b/test/unit/nameForAccount_test.js
index 32af49e9d..9bb02c6bc 100644
--- a/test/unit/nameForAccount_test.js
+++ b/test/unit/nameForAccount_test.js
@@ -6,7 +6,7 @@ var contractNamer = require(path.join(__dirname, '..', '..', 'old-ui', 'lib', 'c
describe('contractNamer', function () {
beforeEach(function () {
- this.sinon = sinon.sandbox.create()
+ this.sinon = sinon.createSandbox()
})
afterEach(function () {
diff --git a/test/unit/reducers/unlock_vault_test.js b/test/unit/reducers/unlock_vault_test.js
index 2b7d70b2c..d66e8edbb 100644
--- a/test/unit/reducers/unlock_vault_test.js
+++ b/test/unit/reducers/unlock_vault_test.js
@@ -10,7 +10,7 @@ var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'redu
describe('#unlockMetamask(selectedAccount)', function () {
beforeEach(function () {
// sinon allows stubbing methods that are easily verified
- this.sinon = sinon.sandbox.create()
+ this.sinon = sinon.createSandbox()
})
afterEach(function () {
diff --git a/test/unit/util_test.js b/test/unit/util_test.js
index 59048975a..39473854f 100644
--- a/test/unit/util_test.js
+++ b/test/unit/util_test.js
@@ -10,7 +10,7 @@ describe('util', function () {
for (var i = 0; i < 18; i++) { ethInWei += '0' }
beforeEach(function () {
- this.sinon = sinon.sandbox.create()
+ this.sinon = sinon.createSandbox()
})
afterEach(function () {
diff --git a/ui/app/actions.js b/ui/app/actions.js
index 57bffb046..894e31fde 100644
--- a/ui/app/actions.js
+++ b/ui/app/actions.js
@@ -275,6 +275,10 @@ var actions = {
UPDATE_NETWORK_ENDPOINT_TYPE: 'UPDATE_NETWORK_ENDPOINT_TYPE',
retryTransaction,
+ SET_PENDING_TOKENS: 'SET_PENDING_TOKENS',
+ CLEAR_PENDING_TOKENS: 'CLEAR_PENDING_TOKENS',
+ setPendingTokens,
+ clearPendingTokens,
}
module.exports = actions
@@ -1929,3 +1933,22 @@ function updateNetworkEndpointType (networkEndpointType) {
value: networkEndpointType,
}
}
+
+function setPendingTokens (pendingTokens) {
+ const { customToken = {}, selectedTokens = {} } = pendingTokens
+ const { address, symbol, decimals } = customToken
+ const tokens = address && symbol && decimals
+ ? { ...selectedTokens, [address]: { ...customToken, isCustom: true } }
+ : selectedTokens
+
+ return {
+ type: actions.SET_PENDING_TOKENS,
+ payload: tokens,
+ }
+}
+
+function clearPendingTokens () {
+ return {
+ type: actions.CLEAR_PENDING_TOKENS,
+ }
+}
diff --git a/ui/app/app.js b/ui/app/app.js
index f840cc34e..3d2961340 100644
--- a/ui/app/app.js
+++ b/ui/app/app.js
@@ -26,6 +26,7 @@ const UnlockPage = require('./components/pages/unlock-page')
const RestoreVaultPage = require('./components/pages/keychains/restore-vault')
const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed')
const AddTokenPage = require('./components/pages/add-token')
+const ConfirmAddTokenPage = require('./components/pages/confirm-add-token')
const CreateAccountPage = require('./components/pages/create-account')
const NoticeScreen = require('./components/pages/notice')
@@ -47,6 +48,7 @@ const {
REVEAL_SEED_ROUTE,
RESTORE_VAULT_ROUTE,
ADD_TOKEN_ROUTE,
+ CONFIRM_ADD_TOKEN_ROUTE,
NEW_ACCOUNT_ROUTE,
SEND_ROUTE,
CONFIRM_TRANSACTION_ROUTE,
@@ -77,6 +79,7 @@ class App extends Component {
h(Authenticated, { path: CONFIRM_TRANSACTION_ROUTE, component: ConfirmTxScreen }),
h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen2 }),
h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }),
+ h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }),
h(Authenticated, { path: NEW_ACCOUNT_ROUTE, component: CreateAccountPage }),
h(Authenticated, { path: DEFAULT_ROUTE, exact, component: Home }),
])
diff --git a/ui/app/components/button/button.component.js b/ui/app/components/button/button.component.js
index 7769e4875..fe3bf363c 100644
--- a/ui/app/components/button/button.component.js
+++ b/ui/app/components/button/button.component.js
@@ -1,7 +1,6 @@
-const { Component } = require('react')
-const h = require('react-hyperscript')
-const PropTypes = require('prop-types')
-const classnames = require('classnames')
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
const SECONDARY = 'secondary'
const CLASSNAME_PRIMARY = 'btn-primary'
@@ -24,10 +23,12 @@ class Button extends Component {
const { type, large, className, ...buttonProps } = this.props
return (
- h('button', {
- className: classnames(getClassName(type, large), className),
- ...buttonProps,
- }, this.props.children)
+ <button
+ className={classnames(getClassName(type, large), className)}
+ { ...buttonProps }
+ >
+ { this.props.children }
+ </button>
)
}
}
@@ -39,5 +40,5 @@ Button.propTypes = {
children: PropTypes.string,
}
-module.exports = Button
+export default Button
diff --git a/ui/app/components/button/index.js b/ui/app/components/button/index.js
index 3dc7d1eea..33ae95ae2 100644
--- a/ui/app/components/button/index.js
+++ b/ui/app/components/button/index.js
@@ -1,2 +1,2 @@
-const Button = require('./button.component')
+import Button from './button.component'
module.exports = Button
diff --git a/ui/app/components/export-text-container/export-text-container.scss b/ui/app/components/export-text-container/index.scss
index 975d62f70..975d62f70 100644
--- a/ui/app/components/export-text-container/export-text-container.scss
+++ b/ui/app/components/export-text-container/index.scss
diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss
new file mode 100644
index 000000000..f3fe823f8
--- /dev/null
+++ b/ui/app/components/index.scss
@@ -0,0 +1,5 @@
+@import './export-text-container/index';
+
+@import './info-box/index';
+
+@import './pages/index';
diff --git a/ui/app/components/info-box/index.js b/ui/app/components/info-box/index.js
new file mode 100644
index 000000000..6110422ed
--- /dev/null
+++ b/ui/app/components/info-box/index.js
@@ -0,0 +1,2 @@
+import InfoBox from './info-box.component'
+module.exports = InfoBox
diff --git a/ui/app/components/info-box/index.scss b/ui/app/components/info-box/index.scss
new file mode 100644
index 000000000..8b5626d79
--- /dev/null
+++ b/ui/app/components/info-box/index.scss
@@ -0,0 +1,24 @@
+.info-box {
+ border-radius: 4px;
+ background-color: $alabaster;
+ position: relative;
+ padding: 16px;
+ display: flex;
+ flex-flow: column;
+ color: $mid-gray;
+
+ &__close::after {
+ content: '\00D7';
+ font-size: 29px;
+ font-weight: 200;
+ color: $dusty-gray;
+ position: absolute;
+ right: 12px;
+ top: 0;
+ cursor: pointer;
+ }
+
+ &__description {
+ font-size: .75rem;
+ }
+}
diff --git a/ui/app/components/info-box/info-box.component.js b/ui/app/components/info-box/info-box.component.js
new file mode 100644
index 000000000..8688b8e8f
--- /dev/null
+++ b/ui/app/components/info-box/info-box.component.js
@@ -0,0 +1,49 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+
+export default class InfoBox extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ onClose: PropTypes.func,
+ title: PropTypes.string,
+ description: PropTypes.string,
+ }
+
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ isShowing: true,
+ }
+ }
+
+ handleClose () {
+ const { onClose } = this.props
+
+ if (onClose) {
+ onClose()
+ } else {
+ this.setState({ isShowing: false })
+ }
+ }
+
+ render () {
+ const { title, description } = this.props
+
+ return !this.state.isShowing
+ ? null
+ : (
+ <div className="info-box">
+ <div
+ className="info-box__close"
+ onClick={() => this.handleClose()}
+ />
+ <div className="info-box__title">{ title }</div>
+ <div className="info-box__description">{ description }</div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/pages/add-token.js b/ui/app/components/pages/add-token.js
deleted file mode 100644
index 8d52571d0..000000000
--- a/ui/app/components/pages/add-token.js
+++ /dev/null
@@ -1,431 +0,0 @@
-const inherits = require('util').inherits
-const Component = require('react').Component
-const classnames = require('classnames')
-const h = require('react-hyperscript')
-const PropTypes = require('prop-types')
-const connect = require('react-redux').connect
-const R = require('ramda')
-const Fuse = require('fuse.js')
-const contractMap = require('eth-contract-metadata')
-const TokenBalance = require('../../components/token-balance')
-const Identicon = require('../../components/identicon')
-const contractList = Object.entries(contractMap)
- .map(([ _, tokenData]) => tokenData)
- .filter(tokenData => Boolean(tokenData.erc20))
-const fuse = new Fuse(contractList, {
- shouldSort: true,
- threshold: 0.45,
- location: 0,
- distance: 100,
- maxPatternLength: 32,
- minMatchCharLength: 1,
- keys: [
- { name: 'name', weight: 0.5 },
- { name: 'symbol', weight: 0.5 },
- ],
-})
-const actions = require('../../actions')
-const ethUtil = require('ethereumjs-util')
-const { tokenInfoGetter } = require('../../token-util')
-const { DEFAULT_ROUTE } = require('../../routes')
-
-const emptyAddr = '0x0000000000000000000000000000000000000000'
-
-AddTokenScreen.contextTypes = {
- t: PropTypes.func,
-}
-
-module.exports = connect(mapStateToProps, mapDispatchToProps)(AddTokenScreen)
-
-
-function mapStateToProps (state) {
- const { identities, tokens } = state.metamask
- return {
- identities,
- tokens,
- }
-}
-
-function mapDispatchToProps (dispatch) {
- return {
- addTokens: tokens => dispatch(actions.addTokens(tokens)),
- }
-}
-
-inherits(AddTokenScreen, Component)
-function AddTokenScreen () {
- this.state = {
- isShowingConfirmation: false,
- isShowingInfoBox: true,
- customAddress: '',
- customSymbol: '',
- customDecimals: '',
- searchQuery: '',
- selectedTokens: {},
- errors: {},
- autoFilled: false,
- displayedTab: 'SEARCH',
- }
- this.tokenAddressDidChange = this.tokenAddressDidChange.bind(this)
- this.tokenSymbolDidChange = this.tokenSymbolDidChange.bind(this)
- this.tokenDecimalsDidChange = this.tokenDecimalsDidChange.bind(this)
- this.onNext = this.onNext.bind(this)
- Component.call(this)
-}
-
-AddTokenScreen.prototype.componentWillMount = function () {
- this.tokenInfoGetter = tokenInfoGetter()
-}
-
-AddTokenScreen.prototype.toggleToken = function (address, token) {
- const { selectedTokens = {}, errors } = this.state
- const selectedTokensCopy = { ...selectedTokens }
-
- if (address in selectedTokensCopy) {
- delete selectedTokensCopy[address]
- } else {
- selectedTokensCopy[address] = token
- }
-
- this.setState({
- selectedTokens: selectedTokensCopy,
- errors: {
- ...errors,
- tokenSelector: null,
- },
- })
-}
-
-AddTokenScreen.prototype.onNext = function () {
- const { isValid, errors } = this.validate()
-
- return !isValid
- ? this.setState({ errors })
- : this.setState({ isShowingConfirmation: true })
-}
-
-AddTokenScreen.prototype.tokenAddressDidChange = function (e) {
- const customAddress = e.target.value.trim()
- this.setState({ customAddress })
- if (ethUtil.isValidAddress(customAddress) && customAddress !== emptyAddr) {
- this.attemptToAutoFillTokenParams(customAddress)
- } else {
- this.setState({
- customSymbol: '',
- customDecimals: 0,
- })
- }
-}
-
-AddTokenScreen.prototype.tokenSymbolDidChange = function (e) {
- const customSymbol = e.target.value.trim()
- this.setState({ customSymbol })
-}
-
-AddTokenScreen.prototype.tokenDecimalsDidChange = function (e) {
- const customDecimals = e.target.value.trim()
- this.setState({ customDecimals })
-}
-
-AddTokenScreen.prototype.checkExistingAddresses = function (address) {
- if (!address) return false
- const tokensList = this.props.tokens
- const matchesAddress = existingToken => {
- return existingToken.address.toLowerCase() === address.toLowerCase()
- }
-
- return R.any(matchesAddress)(tokensList)
-}
-
-AddTokenScreen.prototype.validate = function () {
- const errors = {}
- const identitiesList = Object.keys(this.props.identities)
- const { customAddress, customSymbol, customDecimals, selectedTokens } = this.state
- const standardAddress = ethUtil.addHexPrefix(customAddress).toLowerCase()
-
- if (customAddress) {
- const validAddress = ethUtil.isValidAddress(customAddress)
- if (!validAddress) {
- errors.customAddress = this.context.t('invalidAddress')
- }
-
- const validDecimals = customDecimals !== null
- && customDecimals !== ''
- && customDecimals >= 0
- && customDecimals < 36
- if (!validDecimals) {
- errors.customDecimals = this.context.t('decimalsMustZerotoTen')
- }
-
- const symbolLen = customSymbol.trim().length
- const validSymbol = symbolLen > 0 && symbolLen < 10
- if (!validSymbol) {
- errors.customSymbol = this.context.t('symbolBetweenZeroTen')
- }
-
- const ownAddress = identitiesList.includes(standardAddress)
- if (ownAddress) {
- errors.customAddress = this.context.t('personalAddressDetected')
- }
-
- const tokenAlreadyAdded = this.checkExistingAddresses(customAddress)
- if (tokenAlreadyAdded) {
- errors.customAddress = this.context.t('tokenAlreadyAdded')
- }
- } else if (
- Object.entries(selectedTokens)
- .reduce((isEmpty, [ symbol, isSelected ]) => (
- isEmpty && !isSelected
- ), true)
- ) {
- errors.tokenSelector = this.context.t('mustSelectOne')
- }
-
- return {
- isValid: !Object.keys(errors).length,
- errors,
- }
-}
-
-AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) {
- const { symbol, decimals } = await this.tokenInfoGetter(address)
- if (symbol && decimals) {
- this.setState({
- customSymbol: symbol,
- customDecimals: decimals,
- autoFilled: true,
- })
- }
-}
-
-AddTokenScreen.prototype.renderCustomForm = function () {
- const { autoFilled, customAddress, customSymbol, customDecimals, errors } = this.state
-
- return (
- h('div.add-token__add-custom-form', [
- h('div', {
- className: classnames('add-token__add-custom-field', {
- 'add-token__add-custom-field--error': errors.customAddress,
- }),
- }, [
- h('div.add-token__add-custom-label', this.context.t('tokenAddress')),
- h('input.add-token__add-custom-input', {
- type: 'text',
- onChange: this.tokenAddressDidChange,
- value: customAddress,
- }),
- h('div.add-token__add-custom-error-message', errors.customAddress),
- ]),
- h('div', {
- className: classnames('add-token__add-custom-field', {
- 'add-token__add-custom-field--error': errors.customSymbol,
- }),
- }, [
- h('div.add-token__add-custom-label', this.context.t('tokenSymbol')),
- h('input.add-token__add-custom-input', {
- type: 'text',
- onChange: this.tokenSymbolDidChange,
- value: customSymbol,
- disabled: autoFilled,
- }),
- h('div.add-token__add-custom-error-message', errors.customSymbol),
- ]),
- h('div', {
- className: classnames('add-token__add-custom-field', {
- 'add-token__add-custom-field--error': errors.customDecimals,
- }),
- }, [
- h('div.add-token__add-custom-label', this.context.t('decimal')),
- h('input.add-token__add-custom-input', {
- type: 'number',
- onChange: this.tokenDecimalsDidChange,
- value: customDecimals,
- disabled: autoFilled,
- }),
- h('div.add-token__add-custom-error-message', errors.customDecimals),
- ]),
- ])
- )
-}
-
-AddTokenScreen.prototype.renderTokenList = function () {
- const { searchQuery = '', selectedTokens } = this.state
- const fuseSearchResult = fuse.search(searchQuery)
- const addressSearchResult = contractList.filter(token => {
- return token.address.toLowerCase() === searchQuery.toLowerCase()
- })
- const results = [...addressSearchResult, ...fuseSearchResult]
-
- return h('div', [
- results.length > 0 && h('div.add-token__token-icons-title', this.context.t('popularTokens')),
- h('div.add-token__token-icons-container', Array(6).fill(undefined)
- .map((_, i) => {
- const { logo, symbol, name, address } = results[i] || {}
- const tokenAlreadyAdded = this.checkExistingAddresses(address)
- return Boolean(logo || symbol || name) && (
- h('div.add-token__token-wrapper', {
- className: classnames({
- 'add-token__token-wrapper--selected': selectedTokens[address],
- 'add-token__token-wrapper--disabled': tokenAlreadyAdded,
- }),
- onClick: () => !tokenAlreadyAdded && this.toggleToken(address, results[i]),
- }, [
- h('div.add-token__token-icon', {
- style: {
- backgroundImage: logo && `url(images/contract/${logo})`,
- },
- }),
- h('div.add-token__token-data', [
- h('div.add-token__token-symbol', symbol),
- h('div.add-token__token-name', name),
- ]),
- // tokenAlreadyAdded && (
- // h('div.add-token__token-message', 'Already added')
- // ),
- ])
- )
- })),
- ])
-}
-
-AddTokenScreen.prototype.renderConfirmation = function () {
- const {
- customAddress: address,
- customSymbol: symbol,
- customDecimals: decimals,
- selectedTokens,
- } = this.state
-
- const { addTokens, history } = this.props
-
- const customToken = {
- address,
- symbol,
- decimals,
- }
-
- const tokens = address && symbol && decimals
- ? { ...selectedTokens, [address]: customToken }
- : selectedTokens
-
- return (
- h('div.add-token', [
- h('div.add-token__wrapper', [
- h('div.add-token__content-container.add-token__confirmation-content', [
- h('div.add-token__description.add-token__confirmation-description', this.context.t('balances')),
- h('div.add-token__confirmation-token-list',
- Object.entries(tokens)
- .map(([ address, token ]) => (
- h('span.add-token__confirmation-token-list-item', [
- h(Identicon, {
- className: 'add-token__confirmation-token-icon',
- diameter: 75,
- address,
- }),
- h(TokenBalance, { token }),
- ])
- ))
- ),
- ]),
- ]),
- h('div.add-token__buttons', [
- h('button.btn-secondary--lg.add-token__cancel-button', {
- onClick: () => this.setState({ isShowingConfirmation: false }),
- }, this.context.t('back')),
- h('button.btn-primary--lg', {
- onClick: () => addTokens(tokens).then(() => history.push(DEFAULT_ROUTE)),
- }, this.context.t('addTokens')),
- ]),
- ])
- )
-}
-
-AddTokenScreen.prototype.displayTab = function (selectedTab) {
- this.setState({ displayedTab: selectedTab })
-}
-
-AddTokenScreen.prototype.renderTabs = function () {
- const { isShowingInfoBox, displayedTab, errors } = this.state
-
- return displayedTab === 'CUSTOM_TOKEN'
- ? this.renderCustomForm()
- : h('div', [
- h('div.add-token__wrapper', [
- h('div.add-token__content-container', [
- isShowingInfoBox && h('div.add-token__info-box', [
- h('div.add-token__info-box__close', {
- onClick: () => this.setState({ isShowingInfoBox: false }),
- }),
- h('div.add-token__info-box__title', this.context.t('whatsThis')),
- h('div.add-token__info-box__copy', this.context.t('keepTrackTokens')),
- h('a.add-token__info-box__copy--blue', {
- href: 'http://metamask.helpscoutdocs.com/article/16-managing-erc20-tokens',
- target: '_blank',
- }, this.context.t('learnMore')),
- ]),
- h('div.add-token__input-container', [
- h('input.add-token__input', {
- type: 'text',
- placeholder: this.context.t('searchTokens'),
- onChange: e => this.setState({ searchQuery: e.target.value }),
- }),
- h('div.add-token__search-input-error-message', errors.tokenSelector),
- ]),
- this.renderTokenList(),
- ]),
- ]),
- ])
-}
-
-AddTokenScreen.prototype.render = function () {
- const {
- isShowingConfirmation,
- displayedTab,
- } = this.state
- const { history } = this.props
-
- return h('div.add-token', [
- h('div.add-token__header', [
- h('div.add-token__header__cancel', {
- onClick: () => history.push(DEFAULT_ROUTE),
- }, [
- h('i.fa.fa-angle-left.fa-lg'),
- h('span', this.context.t('cancel')),
- ]),
- h('div.add-token__header__title', this.context.t('addTokens')),
- isShowingConfirmation && h('div.add-token__header__subtitle', this.context.t('likeToAddTokens')),
- !isShowingConfirmation && h('div.add-token__header__tabs', [
-
- h('div.add-token__header__tabs__tab', {
- className: classnames('add-token__header__tabs__tab', {
- 'add-token__header__tabs__selected': displayedTab === 'SEARCH',
- 'add-token__header__tabs__unselected': displayedTab !== 'SEARCH',
- }),
- onClick: () => this.displayTab('SEARCH'),
- }, this.context.t('search')),
-
- h('div.add-token__header__tabs__tab', {
- className: classnames('add-token__header__tabs__tab', {
- 'add-token__header__tabs__selected': displayedTab === 'CUSTOM_TOKEN',
- 'add-token__header__tabs__unselected': displayedTab !== 'CUSTOM_TOKEN',
- }),
- onClick: () => this.displayTab('CUSTOM_TOKEN'),
- }, this.context.t('customToken')),
-
- ]),
- ]),
-
- isShowingConfirmation
- ? this.renderConfirmation()
- : this.renderTabs(),
-
- !isShowingConfirmation && h('div.add-token__buttons', [
- h('button.btn-secondary--lg.add-token__cancel-button', {
- onClick: () => history.push(DEFAULT_ROUTE),
- }, this.context.t('cancel')),
- h('button.btn-primary--lg.add-token__confirm-button', {
- onClick: this.onNext,
- }, this.context.t('next')),
- ]),
- ])
-}
diff --git a/ui/app/components/pages/add-token/add-token.component.js b/ui/app/components/pages/add-token/add-token.component.js
new file mode 100644
index 000000000..0677b4317
--- /dev/null
+++ b/ui/app/components/pages/add-token/add-token.component.js
@@ -0,0 +1,351 @@
+import React, { Component } from 'react'
+import classnames from 'classnames'
+import PropTypes from 'prop-types'
+import ethUtil from 'ethereumjs-util'
+import { checkExistingAddresses } from './util'
+import { tokenInfoGetter } from '../../../token-util'
+import { DEFAULT_ROUTE, CONFIRM_ADD_TOKEN_ROUTE } from '../../../routes'
+import Button from '../../button'
+import TextField from '../../text-field'
+import TokenList from './token-list'
+import TokenSearch from './token-search'
+
+const emptyAddr = '0x0000000000000000000000000000000000000000'
+const SEARCH_TAB = 'SEARCH'
+const CUSTOM_TOKEN_TAB = 'CUSTOM_TOKEN'
+
+class AddToken extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ history: PropTypes.object,
+ setPendingTokens: PropTypes.func,
+ pendingTokens: PropTypes.object,
+ clearPendingTokens: PropTypes.func,
+ tokens: PropTypes.array,
+ identities: PropTypes.object,
+ }
+
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ customAddress: '',
+ customSymbol: '',
+ customDecimals: 0,
+ searchResults: [],
+ selectedTokens: {},
+ tokenSelectorError: null,
+ customAddressError: null,
+ customSymbolError: null,
+ customDecimalsError: null,
+ autoFilled: false,
+ displayedTab: SEARCH_TAB,
+ }
+ }
+
+ componentDidMount () {
+ this.tokenInfoGetter = tokenInfoGetter()
+ const { pendingTokens = {} } = this.props
+ const pendingTokenKeys = Object.keys(pendingTokens)
+
+ if (pendingTokenKeys.length > 0) {
+ let selectedTokens = {}
+ let customToken = {}
+
+ pendingTokenKeys.forEach(tokenAddress => {
+ const token = pendingTokens[tokenAddress]
+ const { isCustom } = token
+
+ if (isCustom) {
+ customToken = { ...token }
+ } else {
+ selectedTokens = { ...selectedTokens, [tokenAddress]: { ...token } }
+ }
+ })
+
+ const {
+ address: customAddress = '',
+ symbol: customSymbol = '',
+ decimals: customDecimals = 0,
+ } = customToken
+
+ const displayedTab = Object.keys(selectedTokens).length > 0 ? SEARCH_TAB : CUSTOM_TOKEN_TAB
+ this.setState({ selectedTokens, customAddress, customSymbol, customDecimals, displayedTab })
+ }
+ }
+
+ handleToggleToken (token) {
+ const { address } = token
+ const { selectedTokens = {} } = this.state
+ const selectedTokensCopy = { ...selectedTokens }
+
+ if (address in selectedTokensCopy) {
+ delete selectedTokensCopy[address]
+ } else {
+ selectedTokensCopy[address] = token
+ }
+
+ this.setState({
+ selectedTokens: selectedTokensCopy,
+ tokenSelectorError: null,
+ })
+ }
+
+ hasError () {
+ const {
+ tokenSelectorError,
+ customAddressError,
+ customSymbolError,
+ customDecimalsError,
+ } = this.state
+
+ return tokenSelectorError || customAddressError || customSymbolError || customDecimalsError
+ }
+
+ hasSelected () {
+ const { customAddress = '', selectedTokens = {} } = this.state
+ return customAddress || Object.keys(selectedTokens).length > 0
+ }
+
+ handleNext () {
+ if (this.hasError()) {
+ return
+ }
+
+ if (!this.hasSelected()) {
+ this.setState({ tokenSelectorError: this.context.t('mustSelectOne') })
+ return
+ }
+
+ const { setPendingTokens, history } = this.props
+ const {
+ customAddress: address,
+ customSymbol: symbol,
+ customDecimals: decimals,
+ selectedTokens,
+ } = this.state
+
+ const customToken = {
+ address,
+ symbol,
+ decimals,
+ }
+
+ setPendingTokens({ customToken, selectedTokens })
+ history.push(CONFIRM_ADD_TOKEN_ROUTE)
+ }
+
+ async attemptToAutoFillTokenParams (address) {
+ const { symbol = '', decimals = 0 } = await this.tokenInfoGetter(address)
+
+ const autoFilled = Boolean(symbol && decimals)
+ this.setState({ autoFilled })
+ this.handleCustomSymbolChange(symbol || '')
+ this.handleCustomDecimalsChange(decimals)
+ }
+
+ handleCustomAddressChange (value) {
+ const customAddress = value.trim()
+ this.setState({
+ customAddress,
+ customAddressError: null,
+ tokenSelectorError: null,
+ autoFilled: false,
+ })
+
+ const isValidAddress = ethUtil.isValidAddress(customAddress)
+ const standardAddress = ethUtil.addHexPrefix(customAddress).toLowerCase()
+
+ switch (true) {
+ case !isValidAddress:
+ this.setState({
+ customAddressError: this.context.t('invalidAddress'),
+ customSymbol: '',
+ customDecimals: 0,
+ customSymbolError: null,
+ customDecimalsError: null,
+ })
+
+ break
+ case Boolean(this.props.identities[standardAddress]):
+ this.setState({
+ customAddressError: this.context.t('personalAddressDetected'),
+ })
+
+ break
+ case checkExistingAddresses(customAddress, this.props.tokens):
+ this.setState({
+ customAddressError: this.context.t('tokenAlreadyAdded'),
+ })
+
+ break
+ default:
+ if (customAddress !== emptyAddr) {
+ this.attemptToAutoFillTokenParams(customAddress)
+ }
+ }
+ }
+
+ handleCustomSymbolChange (value) {
+ const customSymbol = value.trim()
+ const symbolLength = customSymbol.length
+ let customSymbolError = null
+
+ if (symbolLength <= 0 || symbolLength >= 10) {
+ customSymbolError = this.context.t('symbolBetweenZeroTen')
+ }
+
+ this.setState({ customSymbol, customSymbolError })
+ }
+
+ handleCustomDecimalsChange (value) {
+ const customDecimals = value.trim()
+ const validDecimals = customDecimals !== null &&
+ customDecimals !== '' &&
+ customDecimals >= 0 &&
+ customDecimals < 36
+ let customDecimalsError = null
+
+ if (!validDecimals) {
+ customDecimalsError = this.context.t('decimalsMustZerotoTen')
+ }
+
+ this.setState({ customDecimals, customDecimalsError })
+ }
+
+ renderCustomTokenForm () {
+ const {
+ customAddress,
+ customSymbol,
+ customDecimals,
+ customAddressError,
+ customSymbolError,
+ customDecimalsError,
+ autoFilled,
+ } = this.state
+
+ return (
+ <div className="add-token__custom-token-form">
+ <TextField
+ id="custom-address"
+ label="Token Address"
+ type="text"
+ value={customAddress}
+ onChange={e => this.handleCustomAddressChange(e.target.value)}
+ error={customAddressError}
+ fullWidth
+ margin="normal"
+ />
+ <TextField
+ id="custom-symbol"
+ label="Token Symbol"
+ type="text"
+ value={customSymbol}
+ onChange={e => this.handleCustomSymbolChange(e.target.value)}
+ error={customSymbolError}
+ fullWidth
+ margin="normal"
+ disabled={autoFilled}
+ />
+ <TextField
+ id="custom-decimals"
+ label="Decimals of Precision"
+ type="number"
+ value={customDecimals}
+ onChange={e => this.handleCustomDecimalsChange(e.target.value)}
+ error={customDecimalsError}
+ fullWidth
+ margin="normal"
+ disabled={autoFilled}
+ />
+ </div>
+ )
+ }
+
+ renderSearchToken () {
+ const { tokenSelectorError, selectedTokens, searchResults } = this.state
+
+ return (
+ <div className="add-token__search-token">
+ <TokenSearch
+ onSearch={({ results = [] }) => this.setState({ searchResults: results })}
+ error={tokenSelectorError}
+ />
+ <div className="add-token__token-list">
+ <TokenList
+ results={searchResults}
+ selectedTokens={selectedTokens}
+ onToggleToken={token => this.handleToggleToken(token)}
+ />
+ </div>
+ </div>
+ )
+ }
+
+ render () {
+ const { displayedTab } = this.state
+ const { history, clearPendingTokens } = this.props
+
+ return (
+ <div className="page-container">
+ <div className="page-container__header page-container__header--no-padding-bottom">
+ <div className="page-container__title">
+ { this.context.t('addTokens') }
+ </div>
+ <div className="page-container__tabs">
+ <div
+ className={classnames('page-container__tab', {
+ 'page-container__tab--selected': displayedTab === SEARCH_TAB,
+ })}
+ onClick={() => this.setState({ displayedTab: SEARCH_TAB })}
+ >
+ { this.context.t('search') }
+ </div>
+ <div
+ className={classnames('page-container__tab', {
+ 'page-container__tab--selected': displayedTab === CUSTOM_TOKEN_TAB,
+ })}
+ onClick={() => this.setState({ displayedTab: CUSTOM_TOKEN_TAB })}
+ >
+ { this.context.t('customToken') }
+ </div>
+ </div>
+ </div>
+ <div className="page-container__content">
+ {
+ displayedTab === CUSTOM_TOKEN_TAB
+ ? this.renderCustomTokenForm()
+ : this.renderSearchToken()
+ }
+ </div>
+ <div className="page-container__footer">
+ <Button
+ type="secondary"
+ large
+ className="page-container__footer-button"
+ onClick={() => {
+ clearPendingTokens()
+ history.push(DEFAULT_ROUTE)
+ }}
+ >
+ { this.context.t('cancel') }
+ </Button>
+ <Button
+ type="primary"
+ large
+ className="page-container__footer-button"
+ onClick={() => this.handleNext()}
+ disabled={this.hasError() || !this.hasSelected()}
+ >
+ { this.context.t('next') }
+ </Button>
+ </div>
+ </div>
+ )
+ }
+}
+
+export default AddToken
diff --git a/ui/app/components/pages/add-token/add-token.container.js b/ui/app/components/pages/add-token/add-token.container.js
new file mode 100644
index 000000000..87671b156
--- /dev/null
+++ b/ui/app/components/pages/add-token/add-token.container.js
@@ -0,0 +1,22 @@
+import { connect } from 'react-redux'
+import AddToken from './add-token.component'
+
+const { setPendingTokens, clearPendingTokens } = require('../../../actions')
+
+const mapStateToProps = ({ metamask }) => {
+ const { identities, tokens, pendingTokens } = metamask
+ return {
+ identities,
+ tokens,
+ pendingTokens,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ setPendingTokens: tokens => dispatch(setPendingTokens(tokens)),
+ clearPendingTokens: () => dispatch(clearPendingTokens()),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(AddToken)
diff --git a/ui/app/components/pages/add-token/index.js b/ui/app/components/pages/add-token/index.js
new file mode 100644
index 000000000..3666cae82
--- /dev/null
+++ b/ui/app/components/pages/add-token/index.js
@@ -0,0 +1,2 @@
+import AddToken from './add-token.container'
+module.exports = AddToken
diff --git a/ui/app/components/pages/add-token/index.scss b/ui/app/components/pages/add-token/index.scss
new file mode 100644
index 000000000..39e86b97b
--- /dev/null
+++ b/ui/app/components/pages/add-token/index.scss
@@ -0,0 +1,25 @@
+@import './token-list/index';
+
+.add-token {
+ &__custom-token-form {
+ padding: 8px 16px 16px;
+
+ input[type="number"]::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ display: none;
+ }
+
+ input[type="number"]:hover::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ display: none;
+ }
+ }
+
+ &__search-token {
+ padding: 16px;
+ }
+
+ &__token-list {
+ margin-top: 16px;
+ }
+}
diff --git a/ui/app/components/pages/add-token/token-list/index.js b/ui/app/components/pages/add-token/token-list/index.js
new file mode 100644
index 000000000..21dd5ac72
--- /dev/null
+++ b/ui/app/components/pages/add-token/token-list/index.js
@@ -0,0 +1,2 @@
+import TokenList from './token-list.container'
+module.exports = TokenList
diff --git a/ui/app/components/pages/add-token/token-list/index.scss b/ui/app/components/pages/add-token/token-list/index.scss
new file mode 100644
index 000000000..e32739d59
--- /dev/null
+++ b/ui/app/components/pages/add-token/token-list/index.scss
@@ -0,0 +1,65 @@
+@import './token-list-placeholder/index';
+
+.token-list {
+ &__title {
+ font-size: .75rem;
+ }
+
+ &__tokens-container {
+ display: flex;
+ flex-direction: column;
+ }
+
+ &__token {
+ transition: 200ms ease-in-out;
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: center;
+ padding: 8px;
+ margin-top: 8px;
+ box-sizing: border-box;
+ border-radius: 10px;
+ cursor: pointer;
+ border: 2px solid transparent;
+ position: relative;
+
+ &:hover {
+ border: 2px solid rgba($malibu-blue, .5);
+ }
+
+ &--selected {
+ border: 2px solid $malibu-blue !important;
+ }
+
+ &--disabled {
+ opacity: .4;
+ pointer-events: none;
+ }
+ }
+
+ &__token-icon {
+ width: 48px;
+ height: 48px;
+ background-repeat: no-repeat;
+ background-size: contain;
+ background-position: center;
+ border-radius: 50%;
+ background-color: $white;
+ box-shadow: 0 2px 4px 0 rgba($black, .24);
+ margin-right: 12px;
+ flex: 0 0 auto;
+ }
+
+ &__token-data {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ min-width: 0;
+ }
+
+ &__token-name {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+}
diff --git a/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.js b/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.js
new file mode 100644
index 000000000..b82f45e93
--- /dev/null
+++ b/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.js
@@ -0,0 +1,2 @@
+import TokenListPlaceholder from './token-list-placeholder.component'
+module.exports = TokenListPlaceholder
diff --git a/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.scss b/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.scss
new file mode 100644
index 000000000..9d0f4be32
--- /dev/null
+++ b/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.scss
@@ -0,0 +1,19 @@
+.token-list-placeholder {
+ display: flex;
+ align-items: center;
+ padding-top: 36px;
+ flex-direction: column;
+ line-height: 22px;
+ opacity: .5;
+
+ &__text {
+ color: $silver-chalice;
+ width: 50%;
+ text-align: center;
+ margin-top: 8px;
+ }
+
+ &__link {
+ color: $curious-blue;
+ }
+}
diff --git a/ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js b/ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js
new file mode 100644
index 000000000..abd599b26
--- /dev/null
+++ b/ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js
@@ -0,0 +1,27 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+
+export default class TokenListPlaceholder extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ render () {
+ return (
+ <div className="token-list-placeholder">
+ <img src="images/tokensearch.svg" />
+ <div className="token-list-placeholder__text">
+ { this.context.t('addAcquiredTokens') }
+ </div>
+ <a
+ className="token-list-placeholder__link"
+ href="http://metamask.helpscoutdocs.com/article/16-managing-erc20-tokens"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ { this.context.t('learnMore') }
+ </a>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/pages/add-token/token-list/token-list.component.js b/ui/app/components/pages/add-token/token-list/token-list.component.js
new file mode 100644
index 000000000..724a68d6e
--- /dev/null
+++ b/ui/app/components/pages/add-token/token-list/token-list.component.js
@@ -0,0 +1,60 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import { checkExistingAddresses } from '../util'
+import TokenListPlaceholder from './token-list-placeholder'
+
+export default class InfoBox extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ tokens: PropTypes.array,
+ results: PropTypes.array,
+ selectedTokens: PropTypes.object,
+ onToggleToken: PropTypes.func,
+ }
+
+ render () {
+ const { results = [], selectedTokens = {}, onToggleToken, tokens = [] } = this.props
+
+ return results.length === 0
+ ? <TokenListPlaceholder />
+ : (
+ <div className="token-list">
+ <div className="token-list__title">
+ { this.context.t('searchResults') }
+ </div>
+ <div className="token-list__tokens-container">
+ {
+ Array(6).fill(undefined)
+ .map((_, i) => {
+ const { logo, symbol, name, address } = results[i] || {}
+ const tokenAlreadyAdded = checkExistingAddresses(address, tokens)
+
+ return Boolean(logo || symbol || name) && (
+ <div
+ className={classnames('token-list__token', {
+ 'token-list__token--selected': selectedTokens[address],
+ 'token-list__token--disabled': tokenAlreadyAdded,
+ })}
+ onClick={() => !tokenAlreadyAdded && onToggleToken(results[i])}
+ key={i}
+ >
+ <div
+ className="token-list__token-icon"
+ style={{ backgroundImage: logo && `url(images/contract/${logo})` }}>
+ </div>
+ <div className="token-list__token-data">
+ <span className="token-list__token-name">{ `${name} (${symbol})` }</span>
+ </div>
+ </div>
+ )
+ })
+ }
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/pages/add-token/token-list/token-list.container.js b/ui/app/components/pages/add-token/token-list/token-list.container.js
new file mode 100644
index 000000000..cd7b07a37
--- /dev/null
+++ b/ui/app/components/pages/add-token/token-list/token-list.container.js
@@ -0,0 +1,11 @@
+import { connect } from 'react-redux'
+import TokenList from './token-list.component'
+
+const mapStateToProps = ({ metamask }) => {
+ const { tokens } = metamask
+ return {
+ tokens,
+ }
+}
+
+export default connect(mapStateToProps)(TokenList)
diff --git a/ui/app/components/pages/add-token/token-search/index.js b/ui/app/components/pages/add-token/token-search/index.js
new file mode 100644
index 000000000..acaa6b084
--- /dev/null
+++ b/ui/app/components/pages/add-token/token-search/index.js
@@ -0,0 +1,2 @@
+import TokenSearch from './token-search.component'
+module.exports = TokenSearch
diff --git a/ui/app/components/pages/add-token/token-search/token-search.component.js b/ui/app/components/pages/add-token/token-search/token-search.component.js
new file mode 100644
index 000000000..036b2db1e
--- /dev/null
+++ b/ui/app/components/pages/add-token/token-search/token-search.component.js
@@ -0,0 +1,85 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import contractMap from 'eth-contract-metadata'
+import Fuse from 'fuse.js'
+import InputAdornment from '@material-ui/core/InputAdornment'
+import TextField from '../../../text-field'
+
+const contractList = Object.entries(contractMap)
+ .map(([ _, tokenData]) => tokenData)
+ .filter(tokenData => Boolean(tokenData.erc20))
+
+const fuse = new Fuse(contractList, {
+ shouldSort: true,
+ threshold: 0.45,
+ location: 0,
+ distance: 100,
+ maxPatternLength: 32,
+ minMatchCharLength: 1,
+ keys: [
+ { name: 'name', weight: 0.5 },
+ { name: 'symbol', weight: 0.5 },
+ ],
+})
+
+export default class TokenSearch extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static defaultProps = {
+ error: null,
+ }
+
+ static propTypes = {
+ onSearch: PropTypes.func,
+ error: PropTypes.string,
+ }
+
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ searchQuery: '',
+ }
+ }
+
+ handleSearch (searchQuery) {
+ this.setState({ searchQuery })
+ const fuseSearchResult = fuse.search(searchQuery)
+ const addressSearchResult = contractList.filter(token => {
+ return token.address.toLowerCase() === searchQuery.toLowerCase()
+ })
+ const results = [...addressSearchResult, ...fuseSearchResult]
+ this.props.onSearch({ searchQuery, results })
+ }
+
+ renderAdornment () {
+ return (
+ <InputAdornment
+ position="start"
+ style={{ marginRight: '12px' }}
+ >
+ <img src="images/search.svg" />
+ </InputAdornment>
+ )
+ }
+
+ render () {
+ const { error } = this.props
+ const { searchQuery } = this.state
+
+ return (
+ <TextField
+ id="search-tokens"
+ placeholder={this.context.t('searchTokens')}
+ type="text"
+ value={searchQuery}
+ onChange={e => this.handleSearch(e.target.value)}
+ error={error}
+ fullWidth
+ startAdornment={this.renderAdornment()}
+ />
+ )
+ }
+}
diff --git a/ui/app/components/pages/add-token/util.js b/ui/app/components/pages/add-token/util.js
new file mode 100644
index 000000000..579c56cc0
--- /dev/null
+++ b/ui/app/components/pages/add-token/util.js
@@ -0,0 +1,13 @@
+import R from 'ramda'
+
+export function checkExistingAddresses (address, tokenList = []) {
+ if (!address) {
+ return false
+ }
+
+ const matchesAddress = existingToken => {
+ return existingToken.address.toLowerCase() === address.toLowerCase()
+ }
+
+ return R.any(matchesAddress)(tokenList)
+}
diff --git a/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js b/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js
new file mode 100644
index 000000000..9db9efc37
--- /dev/null
+++ b/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js
@@ -0,0 +1,115 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { DEFAULT_ROUTE, ADD_TOKEN_ROUTE } from '../../../routes'
+import Button from '../../button'
+import Identicon from '../../../components/identicon'
+import TokenBalance from './token-balance'
+
+export default class ConfirmAddToken extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ history: PropTypes.object,
+ clearPendingTokens: PropTypes.func,
+ addTokens: PropTypes.func,
+ pendingTokens: PropTypes.object,
+ }
+
+ componentDidMount () {
+ const { pendingTokens = {}, history } = this.props
+
+ if (Object.keys(pendingTokens).length === 0) {
+ history.push(DEFAULT_ROUTE)
+ }
+ }
+
+ getTokenName (name, symbol) {
+ return typeof name === 'undefined'
+ ? symbol
+ : `${name} (${symbol})`
+ }
+
+ render () {
+ const { history, addTokens, clearPendingTokens, pendingTokens } = this.props
+
+ return (
+ <div className="page-container">
+ <div className="page-container__header">
+ <div className="page-container__title">
+ { this.context.t('addTokens') }
+ </div>
+ <div className="page-container__subtitle">
+ { this.context.t('likeToAddTokens') }
+ </div>
+ </div>
+ <div className="page-container__content">
+ <div className="confirm-add-token">
+ <div className="confirm-add-token__header">
+ <div className="confirm-add-token__token">
+ { this.context.t('token') }
+ </div>
+ <div className="confirm-add-token__balance">
+ { this.context.t('balance') }
+ </div>
+ </div>
+ <div className="confirm-add-token__token-list">
+ {
+ Object.entries(pendingTokens)
+ .map(([ address, token ]) => {
+ const { name, symbol } = token
+
+ return (
+ <div
+ className="confirm-add-token__token-list-item"
+ key={address}
+ >
+ <div className="confirm-add-token__token confirm-add-token__data">
+ <Identicon
+ className="confirm-add-token__token-icon"
+ diameter={48}
+ address={address}
+ />
+ <div className="confirm-add-token__name">
+ { this.getTokenName(name, symbol) }
+ </div>
+ </div>
+ <div className="confirm-add-token__balance">
+ <TokenBalance token={token} />
+ </div>
+ </div>
+ )
+ })
+ }
+ </div>
+ </div>
+ </div>
+ <div className="page-container__footer">
+ <Button
+ type="secondary"
+ large
+ className="page-container__footer-button"
+ onClick={() => history.push(ADD_TOKEN_ROUTE)}
+ >
+ { this.context.t('back') }
+ </Button>
+ <Button
+ type="primary"
+ large
+ className="page-container__footer-button"
+ onClick={() => {
+ addTokens(pendingTokens)
+ .then(() => {
+ clearPendingTokens()
+ history.push(DEFAULT_ROUTE)
+ })
+ }}
+ >
+ { this.context.t('addTokens') }
+ </Button>
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/pages/confirm-add-token/confirm-add-token.container.js b/ui/app/components/pages/confirm-add-token/confirm-add-token.container.js
new file mode 100644
index 000000000..0190024d9
--- /dev/null
+++ b/ui/app/components/pages/confirm-add-token/confirm-add-token.container.js
@@ -0,0 +1,20 @@
+import { connect } from 'react-redux'
+import ConfirmAddToken from './confirm-add-token.component'
+
+const { addTokens, clearPendingTokens } = require('../../../actions')
+
+const mapStateToProps = ({ metamask }) => {
+ const { pendingTokens } = metamask
+ return {
+ pendingTokens,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ addTokens: tokens => dispatch(addTokens(tokens)),
+ clearPendingTokens: () => dispatch(clearPendingTokens()),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(ConfirmAddToken)
diff --git a/ui/app/components/pages/confirm-add-token/index.js b/ui/app/components/pages/confirm-add-token/index.js
new file mode 100644
index 000000000..b7decabec
--- /dev/null
+++ b/ui/app/components/pages/confirm-add-token/index.js
@@ -0,0 +1,2 @@
+import ConfirmAddToken from './confirm-add-token.container'
+module.exports = ConfirmAddToken
diff --git a/ui/app/components/pages/confirm-add-token/index.scss b/ui/app/components/pages/confirm-add-token/index.scss
new file mode 100644
index 000000000..66146cf78
--- /dev/null
+++ b/ui/app/components/pages/confirm-add-token/index.scss
@@ -0,0 +1,69 @@
+.confirm-add-token {
+ padding: 16px;
+
+ &__header {
+ font-size: .75rem;
+ display: flex;
+ }
+
+ &__token {
+ flex: 1;
+ min-width: 0;
+ }
+
+ &__balance {
+ flex: 0 0 30%;
+ min-width: 0;
+ }
+
+ &__token-list {
+ display: flex;
+ flex-flow: column nowrap;
+
+ .token-balance {
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: flex-start;
+
+ &__amount {
+ color: $scorpion;
+ font-size: 43px;
+ line-height: 43px;
+ margin-right: 8px;
+ }
+
+ &__symbol {
+ color: $scorpion;
+ font-size: 16px;
+ font-weight: 400;
+ line-height: 24px;
+ }
+ }
+ }
+
+ &__token-list-item {
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: center;
+ margin-top: 8px;
+ box-sizing: border-box;
+ }
+
+ &__data {
+ display: flex;
+ align-items: center;
+ padding: 8px;
+ }
+
+ &__name {
+ min-width: 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ &__token-icon {
+ margin-right: 12px;
+ flex: 0 0 auto;
+ }
+}
diff --git a/ui/app/components/pages/confirm-add-token/token-balance/index.js b/ui/app/components/pages/confirm-add-token/token-balance/index.js
new file mode 100644
index 000000000..6fb5c8223
--- /dev/null
+++ b/ui/app/components/pages/confirm-add-token/token-balance/index.js
@@ -0,0 +1,2 @@
+import TokenBalance from './token-balance.container'
+module.exports = TokenBalance
diff --git a/ui/app/components/pages/confirm-add-token/token-balance/token-balance.component.js b/ui/app/components/pages/confirm-add-token/token-balance/token-balance.component.js
new file mode 100644
index 000000000..976788d4c
--- /dev/null
+++ b/ui/app/components/pages/confirm-add-token/token-balance/token-balance.component.js
@@ -0,0 +1,16 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+
+export default class TokenBalance extends Component {
+ static propTypes = {
+ string: PropTypes.string,
+ symbol: PropTypes.string,
+ error: PropTypes.string,
+ }
+
+ render () {
+ return (
+ <div className="hide-text-overflow">{ this.props.string }</div>
+ )
+ }
+}
diff --git a/ui/app/components/pages/confirm-add-token/token-balance/token-balance.container.js b/ui/app/components/pages/confirm-add-token/token-balance/token-balance.container.js
new file mode 100644
index 000000000..bc1289ce1
--- /dev/null
+++ b/ui/app/components/pages/confirm-add-token/token-balance/token-balance.container.js
@@ -0,0 +1,16 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import withTokenTracker from '../../../../helpers/with-token-tracker'
+import TokenBalance from './token-balance.component'
+import selectors from '../../../../selectors'
+
+const mapStateToProps = state => {
+ return {
+ userAddress: selectors.getSelectedAddress(state),
+ }
+}
+
+export default compose(
+ connect(mapStateToProps),
+ withTokenTracker
+)(TokenBalance)
diff --git a/ui/app/components/pages/index.scss b/ui/app/components/pages/index.scss
new file mode 100644
index 000000000..b15c59863
--- /dev/null
+++ b/ui/app/components/pages/index.scss
@@ -0,0 +1,5 @@
+@import './unlock-page/index';
+
+@import './add-token/index';
+
+@import './confirm-add-token/index';
diff --git a/ui/app/components/pages/unlock-page/unlock-page.scss b/ui/app/components/pages/unlock-page/index.scss
index 3d44bd037..3d44bd037 100644
--- a/ui/app/components/pages/unlock-page/unlock-page.scss
+++ b/ui/app/components/pages/unlock-page/index.scss
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 0976d9506..a2f009d8f 100644
--- a/ui/app/components/pages/unlock-page/unlock-page.component.js
+++ b/ui/app/components/pages/unlock-page/unlock-page.component.js
@@ -1,6 +1,6 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
-import Button from 'material-ui/Button'
+import Button from '@material-ui/core/Button'
import TextField from '../../text-field'
const { ENVIRONMENT_TYPE_POPUP } = require('../../../../../app/scripts/lib/enums')
@@ -129,6 +129,7 @@ class UnlockPage extends Component {
error={error}
autoFocus
autoComplete="current-password"
+ material
fullWidth
/>
</form>
diff --git a/ui/app/components/signature-request.js b/ui/app/components/signature-request.js
index b958a2d2d..474fcf439 100644
--- a/ui/app/components/signature-request.js
+++ b/ui/app/components/signature-request.js
@@ -115,7 +115,7 @@ SignatureRequest.prototype.renderBalance = function () {
return h('div.request-signature__balance', [
- h('div.request-signature__balance-text', [this.context.t('balance')]),
+ h('div.request-signature__balance-text', `${this.context.t('balance')}:`),
h('div.request-signature__balance-value', `${balanceInEther} ETH`),
diff --git a/ui/app/components/text-field/text-field.component.js b/ui/app/components/text-field/text-field.component.js
index 6fd3b82b4..b695a449a 100644
--- a/ui/app/components/text-field/text-field.component.js
+++ b/ui/app/components/text-field/text-field.component.js
@@ -1,59 +1,102 @@
-import React from 'react'
+import React, { Component } from 'react'
import PropTypes from 'prop-types'
-import { withStyles } from 'material-ui/styles'
-import { default as MaterialTextField } from 'material-ui/TextField'
+import { withStyles } from '@material-ui/core/styles'
+import { default as MaterialTextField } from '@material-ui/core/TextField'
const styles = {
- cssLabel: {
- '&$cssFocused': {
+ materialLabel: {
+ '&$materialFocused': {
color: '#aeaeae',
},
- '&$cssError': {
+ '&$materialError': {
color: '#aeaeae',
},
fontWeight: '400',
color: '#aeaeae',
},
- cssFocused: {},
- cssUnderline: {
+ materialFocused: {},
+ materialUnderline: {
'&:after': {
- backgroundColor: '#f7861c',
+ borderBottom: '2px solid #f7861c',
},
},
- cssError: {},
+ materialError: {},
+ // Non-material styles
+ formLabel: {
+ '&$formLabelFocused': {
+ color: '#5b5b5b',
+ },
+ '&$materialError': {
+ color: '#5b5b5b',
+ },
+ },
+ formLabelFocused: {},
+ inputFocused: {},
+ inputRoot: {
+ 'label + &': {
+ marginTop: '8px',
+ },
+ border: '1px solid #d2d8dd',
+ height: '48px',
+ borderRadius: '4px',
+ padding: '0 16px',
+ display: 'flex',
+ alignItems: 'center',
+ '&$inputFocused': {
+ border: '1px solid #2f9ae0',
+ },
+ },
+ inputLabel: {
+ fontSize: '.75rem',
+ transform: 'none',
+ transition: 'none',
+ position: 'initial',
+ color: '#5b5b5b',
+ },
}
-const TextField = props => {
- const { error, classes, ...textFieldProps } = props
+class TextField extends Component {
+ static defaultProps = {
+ error: null,
+ }
- return (
- <MaterialTextField
- error={Boolean(error)}
- helperText={error}
- InputLabelProps={{
- FormLabelClasses: {
- root: classes.cssLabel,
- focused: classes.cssFocused,
- error: classes.cssError,
- },
- }}
- InputProps={{
- classes: {
- underline: classes.cssUnderline,
- },
- }}
- {...textFieldProps}
- />
- )
-}
+ static propTypes = {
+ error: PropTypes.string,
+ classes: PropTypes.object,
+ material: PropTypes.bool,
+ startAdornment: PropTypes.element,
+ }
-TextField.defaultProps = {
- error: null,
-}
+ render () {
+ const { error, classes, material, startAdornment, ...textFieldProps } = this.props
-TextField.propTypes = {
- error: PropTypes.string,
- classes: PropTypes.object,
+ return (
+ <MaterialTextField
+ error={Boolean(error)}
+ helperText={error}
+ InputLabelProps={{
+ shrink: material ? undefined : true,
+ className: material ? '' : classes.inputLabel,
+ FormLabelClasses: {
+ root: material ? classes.materialLabel : classes.formLabel,
+ focused: material ? classes.materialFocused : classes.formLabelFocused,
+ error: classes.materialError,
+ },
+ }}
+ InputProps={{
+ startAdornment: startAdornment || undefined,
+ disableUnderline: !material,
+ classes: {
+ root: material ? '' : classes.inputRoot,
+ input: material ? '' : classes.input,
+ underline: material ? classes.materialUnderline : '',
+ focused: material ? '' : classes.inputFocused,
+ },
+ }}
+ {...textFieldProps}
+ />
+ )
+ }
}
export default withStyles(styles)(TextField)
diff --git a/ui/app/css/itcss/components/add-token.scss b/ui/app/css/itcss/components/add-token.scss
deleted file mode 100644
index a3ea0d85b..000000000
--- a/ui/app/css/itcss/components/add-token.scss
+++ /dev/null
@@ -1,461 +0,0 @@
-.add-token {
- width: 498px;
- max-height: 805px;
- display: flex;
- flex-flow: column nowrap;
- position: relative;
- z-index: 12;
- font-family: 'Roboto';
- background: white;
- border-radius: 8px;
- box-shadow: 0 0 7px 0 rgba(0, 0, 0, 0.08);
-
- &__wrapper {
- background-color: $white;
- display: flex;
- flex-flow: column nowrap;
- align-items: center;
- flex: 0 0 auto;
- }
-
- &__header {
- display: flex;
- flex-flow: column nowrap;
- padding: 20px 20px 0px;
- border-bottom: 1px solid $geyser;
- flex: 0 0 auto;
-
- &__cancel {
- color: $dodger-blue;
- display: flex;
- align-items: center;
-
- span {
- font-family: Roboto;
- font-size: 16px;
- font-weight: 400;
- line-height: 21px;
- margin-left: 8px;
- cursor:pointer;
- }
- }
-
- &__title {
- color: $tundora;
- font-size: 32px;
- font-weight: 500;
- margin-top: 4px;
- }
-
- &__subtitle {
- font-weight: 400;
- margin-top: 15px;
- margin-bottom: 21px;
- }
-
- &__tabs {
- display: flex;
-
- &__tab {
- height: 54px;
- padding: 15px 10px;
- color: $dusty-gray;
- font-family: Roboto;
- font-size: 18px;
- font-weight: 400;
- line-height: 24px;
- text-align: center;
- }
-
- &__tab:first-of-type {
- margin-right: 20px;
- }
-
- &__unselected:hover {
- color: $black;
- border-bottom: none;
- cursor: pointer;
- }
-
- &__selected {
- color: $curious-blue;
- border-bottom: 3px solid $curious-blue;
- }
- }
- }
-
- &__info-box {
- height: 96px;
- margin: 20px 20px 0px;
- border-radius: 4px;
- background-color: $alabaster;
- position: relative;
- padding-left: 18px;
- display: flex;
- flex-flow: column;
-
- &__close::after {
- content: '\00D7';
- font-size: 29px;
- font-weight: 200;
- color: $dusty-gray;
- position: absolute;
- right: 17px;
- cursor: pointer;
- }
-
- &__title {
- color: $mid-gray;
- font-family: Roboto;
- font-size: 14px;
- font-weight: 400;
- margin-top: 15px;
- margin-bottom: 9px;
- }
-
- &__copy,
- &__copy--blue {
- color: $mid-gray;
- font-family: Roboto;
- font-size: 12px;
- font-weight: 400;
- line-height: 18px;
- }
-
- &__copy--blue {
- color: $curious-blue;
- }
- }
-
- &__description {
- text-align: center;
- }
-
- &__description + &__description {
- margin-top: 24px;
- }
-
- &__confirmation-description {
- font-weight: 400;
- margin: 20px 0 40px 0;
- }
-
- &__content-container {
- width: 100%;
- }
-
- &__input-container {
- display: flex;
- position: relative;
- }
-
- &__search-input-error-message {
- position: absolute;
- bottom: -10px;
- left: 22px;
- font-size: 12px;
- width: 100%;
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
- color: $red;
- }
-
- &__input,
- &__add-custom-input {
- height: 54px;
- padding: 0px 20px;
- border: 1px solid $geyser;
- border-radius: 4px;
- margin: 22px 24px;
- position: relative;
- flex: 1 0 auto;
- color: $scorpion;
- font-family: Roboto;
- font-size: 16px;
-
- &::placeholder {
- color: $scorpion;
- font-family: Roboto;
- font-size: 16px;
- line-height: 21px;
- }
- }
-
- &__footers {
- width: 100%;
- }
-
- &__add-custom {
- color: $scorpion;
- font-size: 18px;
- line-height: 24px;
- text-align: center;
- padding: 12px 0;
- font-weight: 600;
- cursor: pointer;
- position: relative;
-
- &:hover {
- background-color: rgba(0, 0, 0, .05);
- }
-
- &:active {
- background-color: rgba(0, 0, 0, .1);
- }
-
- .fa {
- position: absolute;
- right: 24px;
- font-size: 24px;
- line-height: 24px;
- }
- }
-
- &__add-custom-form {
- display: flex;
- flex-flow: column nowrap;
- margin: 40px 0 30px;
- }
-
- &__add-custom-field {
- position: relative;
- display: flex;
- flex-flow: column;
- flex: 1 0 auto;
-
- &--error {
- .add-token__add-custom-input {
- border-color: $red;
- }
- }
- }
-
- &__add-custom-error-message {
- position: absolute;
- bottom: 1px;
- left: 22px;
- font-size: 12px;
- width: 100%;
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
- color: $red;
- }
-
- &__add-custom-label {
- font-size: 16px;
- font-weight: 400;
- line-height: 21px;
- margin-left: 22px;
- color: $scorpion;
- }
-
- &__add-custom-input {
- margin-top: 6px;
- font-size: 16px;
-
- &::placeholder {
- color: $silver;
- font-size: 16px;
- }
- }
-
- &__add-custom-field + &__add-custom-field {
- margin-top: 6px;
- }
-
- &__buttons {
- display: flex;
- flex-flow: row nowrap;
- flex: 0 0 auto;
- align-items: center;
- justify-content: center;
- padding-bottom: 30px;
- padding-top: 20px;
- }
-
- &__confirm-button,
- &__cancel-button {
- margin: 0 12px;
- padding: 10px 13px;
- height: 54px;
- width: 133px;
- margin-right: 1.2rem;
- }
-
- &__token-icons-title {
- color: #5B5D67;
- font-family: Roboto;
- font-size: 18px;
- font-weight: 400;
- line-height: 24px;
- margin-left: 24px;
- margin-top: 8px;
- margin-bottom: 20px;
- }
-
- &__token-icons-container {
- display: flex;
- flex-flow: row wrap;
- }
-
- &__token-wrapper {
- transition: 200ms ease-in-out;
- display: flex;
- flex-flow: row nowrap;
- flex: 0 0 42.5%;
- align-items: center;
- padding: 12px;
- margin: 0% 2.5% 1.5%;
- box-sizing: border-box;
- border-radius: 10px;
- cursor: pointer;
- border: 2px solid transparent;
- position: relative;
-
- &:hover {
- border: 2px solid rgba($malibu-blue, .5);
- }
-
- &--selected {
- border: 2px solid $malibu-blue !important;
- }
-
- &--disabled {
- opacity: .4;
- pointer-events: none;
- }
- }
-
- &__token-data {
- align-self: flex-start;
- }
-
- &__token-name {
- font-weight: 400;
- font-size: 14px;
- line-height: 19px;
- }
-
- &__token-symbol {
- font-size: 22px;
- line-height: 29px;
- font-weight: 600;
- }
-
- &__token-icon {
- width: 60px;
- height: 60px;
- background-repeat: no-repeat;
- background-size: contain;
- background-position: center;
- border-radius: 50%;
- background-color: $white;
- box-shadow: 0 2px 4px 0 rgba($black, .24);
- margin-right: 12px;
- flex: 0 0 auto;
- }
-
- &__token-message {
- position: absolute;
- color: $caribbean-green;
- font-size: 11px;
- bottom: 0;
- left: 85px;
- }
-
- &__confirmation-token-list {
- display: flex;
- flex-flow: column nowrap;
-
- .token-balance {
- display: flex;
- flex-flow: row nowrap;
- align-items: flex-start;
-
- &__amount {
- color: $scorpion;
- font-size: 43px;
- line-height: 43px;
- margin-right: 8px;
- }
-
- &__symbol {
- color: $scorpion;
- font-size: 16px;
- font-weight: 400;
- line-height: 24px;
- }
- }
- }
-
- &__confirmation-title {
- padding: 30px 120px 12px;
-
- @media screen and (max-width: $break-small) {
- padding: 20px 0;
- width: 100%;
- }
- }
-
- &__confirmation-content {
- padding-bottom: 60px;
- }
-
- &__confirmation-token-list-item {
- display: flex;
- flex-flow: row nowrap;
- margin: 0 auto;
- align-items: center;
- }
-
- &__confirmation-token-list-item + &__confirmation-token-list-item {
- margin-top: 30px;
- }
-
- &__confirmation-token-icon {
- margin-right: 18px;
- }
-
- @media screen and (max-width: $break-small) {
- top: 0;
- width: 100%;
- overflow: hidden;
- flex: 1 0 auto;
-
- &__wrapper {
- box-shadow: none !important;
- flex: 1 1 auto;
- width: 100%;
- overflow-y: scroll;
- height: 400px;
- }
-
- &__footers {
- border-bottom: 1px solid $gallery;
- }
-
- &__token-icon {
- width: 50px;
- height: 50px;
- }
-
- &__token-symbol {
- font-size: 18px;
- line-height: 24px;
- }
-
- &__token-name {
- font-size: 12px;
- line-height: 16px;
- }
-
- &__buttons {
- padding: 1rem;
- margin: 0;
- border-top: 1px solid $gallery;
- width: 100%;
- }
- }
-}
diff --git a/ui/app/css/itcss/components/buttons.scss b/ui/app/css/itcss/components/buttons.scss
index 86daf60d8..4cbed6093 100644
--- a/ui/app/css/itcss/components/buttons.scss
+++ b/ui/app/css/itcss/components/buttons.scss
@@ -15,8 +15,9 @@
font-size: 14px;
font-weight: 500;
transition: border-color .3s ease;
- padding: 0 20px;
+ padding: 0 16px;
min-width: 140px;
+ width: 100%;
text-transform: uppercase;
outline: none;
}
@@ -110,6 +111,7 @@
font-size: .85rem;
font-weight: 400;
transition: border-color .3s ease;
+ width: 100%;
&:hover {
border-color: $scorpion;
@@ -126,6 +128,7 @@
font-size: .85rem;
font-weight: 400;
transition: border-color .3s ease;
+ width: 100%;
}
// No longer used in flat design, remove when modal buttons done
diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss
index 1c544e162..1d87b8004 100644
--- a/ui/app/css/itcss/components/index.scss
+++ b/ui/app/css/itcss/components/index.scss
@@ -30,8 +30,6 @@
@import './token-list.scss';
-@import './add-token.scss';
-
@import './currency-display.scss';
@import './account-menu.scss';
@@ -62,4 +60,4 @@
@import './sender-to-recipient.scss';
-@import '../../../components/export-text-container/export-text-container.scss';
+@import '../../../components/index';
diff --git a/ui/app/css/itcss/components/newui-sections.scss b/ui/app/css/itcss/components/newui-sections.scss
index 2903e07b4..bbe0ee661 100644
--- a/ui/app/css/itcss/components/newui-sections.scss
+++ b/ui/app/css/itcss/components/newui-sections.scss
@@ -144,8 +144,8 @@ $wallet-view-bg: $alabaster;
flex: 0 0 auto;
margin: 36px auto;
background: none;
- padding: .7rem 2rem;
transition: border-color .3s ease;
+ width: 150px;
&:hover {
border-color: $curious-blue;
diff --git a/ui/app/css/itcss/components/pages/index.scss b/ui/app/css/itcss/components/pages/index.scss
index 195185fff..709f8baf6 100644
--- a/ui/app/css/itcss/components/pages/index.scss
+++ b/ui/app/css/itcss/components/pages/index.scss
@@ -1,3 +1 @@
@import './reveal-seed.scss';
-
-@import '../../../../components/pages/unlock-page/unlock-page.scss';
diff --git a/ui/app/css/itcss/generic/index.scss b/ui/app/css/itcss/generic/index.scss
index 9b2982096..f667eeda8 100644
--- a/ui/app/css/itcss/generic/index.scss
+++ b/ui/app/css/itcss/generic/index.scss
@@ -74,28 +74,32 @@ input.large-input {
}
.page-container {
- width: 400px;
+ 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: 7px;
+ border-radius: 8px;
&__header {
display: flex;
flex-flow: column;
border-bottom: 1px solid $geyser;
- padding: 20px;
+ padding: 16px;
flex: 0 0 auto;
position: relative;
+
+ &--no-padding-bottom {
+ padding-bottom: 0;
+ }
}
&__header-close {
color: $tundora;
position: absolute;
- top: 20px;
- right: 20px;
+ top: 16px;
+ right: 16px;
cursor: pointer;
overflow: hidden;
@@ -117,7 +121,7 @@ input.large-input {
flex-flow: row;
justify-content: center;
border-top: 1px solid $geyser;
- padding: 1.6rem;
+ padding: 16px;
flex: 0 0 auto;
.btn-clear,
@@ -128,11 +132,10 @@ input.large-input {
}
&__footer-button {
- width: 165px;
height: 55px;
font-size: 1rem;
text-transform: uppercase;
- margin-right: 1.2rem;
+ margin-right: 16px;
border-radius: 2px;
&:last-of-type {
@@ -162,25 +165,20 @@ input.large-input {
}
&__tabs {
- padding: 0 1.3rem;
display: flex;
+ margin-top: 16px;
}
&__tab {
min-width: 5rem;
- padding: .2rem .8rem .9rem;
+ padding: 8px;
color: $dusty-gray;
font-family: Roboto;
- font-size: 1.1rem;
- line-height: initial;
+ font-size: 1rem;
text-align: center;
cursor: pointer;
border-bottom: none;
- margin-right: 1rem;
-
- &:hover {
- color: $black;
- }
+ margin-right: 16px;
&:last-of-type {
margin-right: 0;
@@ -189,10 +187,6 @@ input.large-input {
&--selected {
color: $curious-blue;
border-bottom: 3px solid $curious-blue;
-
- &:hover {
- color: $curious-blue;
- }
}
}
@@ -260,7 +254,8 @@ input.large-input {
@media screen and (min-width: 576px) {
.page-container {
- height: 600px;
+ max-height: 82vh;
+ min-height: 570px;
flex: 0 0 auto;
}
}
@@ -303,3 +298,9 @@ input.form-control {
border: 1px solid $monzo;
}
}
+
+.hide-text-overflow {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
diff --git a/ui/app/helpers/with-token-tracker.js b/ui/app/helpers/with-token-tracker.js
new file mode 100644
index 000000000..e24517c18
--- /dev/null
+++ b/ui/app/helpers/with-token-tracker.js
@@ -0,0 +1,105 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import TokenTracker from 'eth-token-tracker'
+
+const withTokenTracker = WrappedComponent => {
+ return class TokenTrackerWrappedComponent extends Component {
+ static propTypes = {
+ userAddress: PropTypes.string.isRequired,
+ token: PropTypes.object.isRequired,
+ }
+
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ string: '',
+ symbol: '',
+ error: null,
+ }
+
+ this.tracker = null
+ this.updateBalance = this.updateBalance.bind(this)
+ this.setError = this.setError.bind(this)
+ }
+
+ componentDidMount () {
+ this.createFreshTokenTracker()
+ }
+
+ componentDidUpdate (prevProps) {
+ const { userAddress: newAddress, token: { address: newTokenAddress } } = this.props
+ const { userAddress: oldAddress, token: { address: oldTokenAddress } } = prevProps
+
+ if ((oldAddress === newAddress) && (oldTokenAddress === newTokenAddress)) {
+ return
+ }
+
+ if ((!oldAddress || !newAddress) && (!oldTokenAddress || !newTokenAddress)) {
+ return
+ }
+
+ this.createFreshTokenTracker()
+ }
+
+ componentWillUnmount () {
+ this.removeListeners()
+ }
+
+ createFreshTokenTracker () {
+ this.removeListeners()
+
+ if (!global.ethereumProvider) {
+ return
+ }
+
+ const { userAddress, token } = this.props
+
+ this.tracker = new TokenTracker({
+ userAddress,
+ provider: global.ethereumProvider,
+ tokens: [token],
+ pollingInterval: 8000,
+ })
+
+ this.tracker.on('update', this.updateBalance)
+ this.tracker.on('error', this.setError)
+
+ this.tracker.updateBalances()
+ .then(() => this.updateBalance(this.tracker.serialize()))
+ .catch(error => this.setState({ error: error.message }))
+ }
+
+ setError (error) {
+ this.setState({ error })
+ }
+
+ updateBalance (tokens = []) {
+ const [{ string, symbol }] = tokens
+ this.setState({ string, symbol, error: null })
+ }
+
+ removeListeners () {
+ if (this.tracker) {
+ this.tracker.stop()
+ this.tracker.removeListener('update', this.updateBalance)
+ this.tracker.removeListener('error', this.setError)
+ }
+ }
+
+ render () {
+ const { string, symbol, error } = this.state
+
+ return (
+ <WrappedComponent
+ { ...this.props }
+ string={string}
+ symbol={symbol}
+ error={error}
+ />
+ )
+ }
+ }
+}
+
+module.exports = withTokenTracker
diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js
index 9afaf6a50..732fa6dea 100644
--- a/ui/app/reducers/metamask.js
+++ b/ui/app/reducers/metamask.js
@@ -28,6 +28,7 @@ function reduceMetamask (state, action) {
contractExchangeRates: {},
tokenExchangeRates: {},
tokens: [],
+ pendingTokens: {},
send: {
gasLimit: null,
gasPrice: null,
@@ -356,6 +357,17 @@ function reduceMetamask (state, action) {
currentLocale: action.value,
})
+ case actions.SET_PENDING_TOKENS:
+ return extend(metamaskState, {
+ pendingTokens: { ...action.payload },
+ })
+
+ case actions.CLEAR_PENDING_TOKENS: {
+ return extend(metamaskState, {
+ pendingTokens: {},
+ })
+ }
+
default:
return metamaskState
diff --git a/ui/app/routes.js b/ui/app/routes.js
index 4b3f8f4d8..0ff3f644d 100644
--- a/ui/app/routes.js
+++ b/ui/app/routes.js
@@ -6,6 +6,7 @@ const REVEAL_SEED_ROUTE = '/seed'
const CONFIRM_SEED_ROUTE = '/confirm-seed'
const RESTORE_VAULT_ROUTE = '/restore-vault'
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 SEND_ROUTE = '/send'
@@ -31,6 +32,7 @@ module.exports = {
CONFIRM_SEED_ROUTE,
RESTORE_VAULT_ROUTE,
ADD_TOKEN_ROUTE,
+ CONFIRM_ADD_TOKEN_ROUTE,
NEW_ACCOUNT_ROUTE,
IMPORT_ACCOUNT_ROUTE,
SEND_ROUTE,