diff --git a/.circleci/config.yml b/.circleci/config.yml index 6d4bf270c959..c9d8cdf6a66d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,7 +5,6 @@ workflows: full_test: jobs: - prep-deps-npm - - prep-deps-firefox - prep-build: requires: - prep-deps-npm @@ -28,7 +27,6 @@ workflows: - test-e2e-firefox: requires: - prep-deps-npm - - prep-deps-firefox - prep-build - test-e2e-beta-chrome: requires: @@ -37,7 +35,6 @@ workflows: - test-e2e-beta-firefox: requires: - prep-deps-npm - - prep-deps-firefox - prep-build - test-unit: requires: @@ -49,7 +46,6 @@ workflows: - test-integration-mascara-firefox: requires: - prep-deps-npm - - prep-deps-firefox - prep-scss - test-integration-flat-chrome: requires: @@ -58,7 +54,6 @@ workflows: - test-integration-flat-firefox: requires: - prep-deps-npm - - prep-deps-firefox - prep-scss - all-tests-pass: requires: @@ -103,46 +98,34 @@ jobs: - restore_cache: keys: - v1.0-dependency-cache-{{ checksum "package-lock.json" }} - # fallback to using the latest cache if no exact match is found - - v1.0-dependency-cache- - run: name: Install npm 6 + deps via npm command: | - sudo npm install -g npm@6.1.0 && npm install --no-save + sudo npm install -g npm@6 && npm install --no-save + - persist_to_workspace: + root: . + paths: + - node_modules - save_cache: key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} paths: - node_modules - prep-deps-firefox: - docker: - - image: circleci/node:8.11.3-browsers - steps: - - checkout - - restore_cache: - key: v1.0-dependency-cache-firefox- - - run: - name: Download Firefox If needed - command: ./.circleci/scripts/firefox-download.sh - - save_cache: - key: v1.0-dependency-cache-firefox- - paths: - - firefox prep-build: docker: - image: circleci/node:8.11.3-browsers steps: - checkout - - restore_cache: - key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} + - attach_workspace: + at: . - run: name: build:dist command: npm run dist - run: name: build:debug command: find dist/ -type f -exec md5sum {} \; | sort -k 2 - - save_cache: - key: build-cache-{{ .Revision }} + - persist_to_workspace: + root: . paths: - dist - builds @@ -152,23 +135,23 @@ jobs: - image: circleci/node:8.11.3-browsers steps: - checkout - - restore_cache: - key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} + - attach_workspace: + at: . - run: name: build:dist command: npm run doc - - save_cache: - key: docs-cache-{{ .Revision }} + - persist_to_workspace: + root: . paths: - - docs/jsdoc + - docs/jsdocs prep-scss: docker: - image: circleci/node:8.11.3-browsers steps: - checkout - - restore_cache: - key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} + - attach_workspace: + at: . - run: name: Get Scss Cache key # this allows us to checksum against a whole directory @@ -176,8 +159,8 @@ jobs: - run: name: Build for integration tests command: npm run test:integration:build - - save_cache: - key: scss-cache-{{ checksum "scss_checksum" }} + - persist_to_workspace: + root: . paths: - ui/app/css/output @@ -186,8 +169,8 @@ jobs: - image: circleci/node:8.11.3-browsers steps: - checkout - - restore_cache: - key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} + - attach_workspace: + at: . - run: name: Test command: npm run lint @@ -197,8 +180,8 @@ jobs: - image: circleci/node:8.11.3-browsers steps: - checkout - - restore_cache: - key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} + - attach_workspace: + at: . - run: name: Test command: npx nsp check @@ -208,10 +191,8 @@ jobs: - image: circleci/node:8.11.3-browsers steps: - checkout - - restore_cache: - key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - - restore_cache: - key: build-cache-{{ .Revision }} + - attach_workspace: + at: . - run: name: test:e2e:chrome command: npm run test:e2e:chrome @@ -224,15 +205,11 @@ jobs: - image: circleci/node:8.11.3-browsers steps: - checkout - - restore_cache: - key: v1.0-dependency-cache-firefox- - run: - name: Install firefox - command: ./.circleci/scripts/firefox-install.sh - - restore_cache: - key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - - restore_cache: - key: build-cache-{{ .Revision }} + name: Install Firefox + command: ./.circleci/scripts/firefox-install + - attach_workspace: + at: . - run: name: test:e2e:firefox command: npm run test:e2e:firefox @@ -245,10 +222,8 @@ jobs: - image: circleci/node:8.11.3-browsers steps: - checkout - - restore_cache: - key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - - restore_cache: - key: build-cache-{{ .Revision }} + - attach_workspace: + at: . - run: name: test:e2e:chrome:beta command: npm run test:e2e:chrome:beta @@ -261,15 +236,11 @@ jobs: - image: circleci/node:8.11.3-browsers steps: - checkout - - restore_cache: - key: v1.0-dependency-cache-firefox- - run: - name: Install firefox - command: ./.circleci/scripts/firefox-install.sh - - restore_cache: - key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - - restore_cache: - key: build-cache-{{ .Revision }} + name: Install Firefox + command: ./.circleci/scripts/firefox-install + - attach_workspace: + at: . - run: name: test:e2e:firefox:beta command: npm run test:e2e:firefox:beta @@ -282,15 +253,13 @@ jobs: - image: circleci/node:8.11.3-browsers steps: - checkout - - restore_cache: - key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - - restore_cache: - key: build-cache-{{ .Revision }} + - attach_workspace: + at: . - run: name: Test command: npm run test:screens - - save_cache: - key: job-screens-{{ .Revision }} + - persist_to_workspace: + root: . paths: - test-artifacts @@ -299,12 +268,8 @@ jobs: - image: circleci/node:8.11.3-browsers steps: - checkout - - restore_cache: - key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - - restore_cache: - key: build-cache-{{ .Revision }} - - restore_cache: - key: job-screens-{{ .Revision }} + - attach_workspace: + at: . - store_artifacts: path: dist/mascara destination: builds/mascara @@ -326,14 +291,8 @@ jobs: - image: circleci/node:8.11.3-browsers steps: - checkout - - restore_cache: - key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - - restore_cache: - key: build-cache-{{ .Revision }} - - restore_cache: - key: docs-cache-{{ .Revision }} - - restore_cache: - key: job-screens-{{ .Revision }} + - attach_workspace: + at: . - run: name: sentry sourcemaps upload command: npm run sentry:publish @@ -349,32 +308,22 @@ jobs: - image: circleci/node:8.11.3-browsers steps: - checkout - - restore_cache: - key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} + - attach_workspace: + at: . - run: name: test:coverage command: npm run test:coverage test-integration-flat-firefox: - environment: - browsers: '["Firefox"]' docker: - image: circleci/node:8.11.3-browsers steps: - checkout - - restore_cache: - key: v1.0-dependency-cache-firefox- + - attach_workspace: + at: . - run: - name: Install firefox - command: ./.circleci/scripts/firefox-install.sh - - restore_cache: - key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - - run: - name: Get Scss Cache key - # this allows us to checksum against a whole directory - command: find ui/app/css -type f -exec md5sum {} \; | sort -k 2 > scss_checksum - - restore_cache: - key: scss-cache-{{ checksum "scss_checksum" }} + name: Install Firefox + command: ./.circleci/scripts/firefox-install - run: name: test:integration:flat command: npm run test:flat @@ -386,38 +335,22 @@ jobs: - image: circleci/node:8.11.3-browsers steps: - checkout - - restore_cache: - key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - - run: - name: Get Scss Cache key - # this allows us to checksum against a whole directory - command: find ui/app/css -type f -exec md5sum {} \; | sort -k 2 > scss_checksum - - restore_cache: - key: scss-cache-{{ checksum "scss_checksum" }} + - attach_workspace: + at: . - run: name: test:integration:flat command: npm run test:flat test-integration-mascara-firefox: - environment: - browsers: '["Firefox"]' docker: - image: circleci/node:8.11.3-browsers steps: - checkout - - restore_cache: - key: v1.0-dependency-cache-firefox- + - attach_workspace: + at: . - run: - name: Install firefox - command: ./.circleci/scripts/firefox-install.sh - - restore_cache: - key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - - run: - name: Get Scss Cache key - # this allows us to checksum against a whole directory - command: find ui/app/css -type f -exec md5sum {} \; | sort -k 2 > scss_checksum - - restore_cache: - key: scss-cache-{{ checksum "scss_checksum" }} + name: Install Firefox + command: ./.circleci/scripts/firefox-install - run: name: test:integration:mascara command: npm run test:mascara @@ -429,14 +362,8 @@ jobs: - image: circleci/node:8.11.3-browsers steps: - checkout - - restore_cache: - key: v1.0-dependency-cache-{{ checksum "package-lock.json" }} - - run: - name: Get Scss Cache key - # this allows us to checksum against a whole directory - command: find ui/app/css -type f -exec md5sum {} \; | sort -k 2 > scss_checksum - - restore_cache: - key: scss-cache-{{ checksum "scss_checksum" }} + - attach_workspace: + at: . - run: name: test:integration:mascara command: npm run test:mascara @@ -447,4 +374,4 @@ jobs: steps: - run: name: All Tests Passed - command: echo 'weew - everything passed!' \ No newline at end of file + command: echo 'weew - everything passed!' diff --git a/.circleci/scripts/firefox-download.sh b/.circleci/scripts/firefox-download.sh deleted file mode 100755 index 64f0c74e3e74..000000000000 --- a/.circleci/scripts/firefox-download.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash -echo "Checking if firefox was already downloaded" -if [ -d "firefox" ] -then - echo "Firefox found. No need to download" -else - FIREFOX_VERSION="61.0.1" - FIREFOX_BINARY="firefox-$FIREFOX_VERSION.tar.bz2" - echo "Downloading firefox..." - wget "https://ftp.mozilla.org/pub/firefox/releases/$FIREFOX_VERSION/linux-x86_64/en-US/$FIREFOX_BINARY" \ - && tar xjf "$FIREFOX_BINARY" - echo "firefox download complete" -fi diff --git a/.circleci/scripts/firefox-install b/.circleci/scripts/firefox-install new file mode 100755 index 000000000000..1d8e62d76743 --- /dev/null +++ b/.circleci/scripts/firefox-install @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +set -e +set -u +set -o pipefail + +FIREFOX_VERSION='61.0.2' +FIREFOX_BINARY="firefox-${FIREFOX_VERSION}.tar.bz2" +FIREFOX_BINARY_URL="https://ftp.mozilla.org/pub/firefox/releases/${FIREFOX_VERSION}/linux-x86_64/en-US/${FIREFOX_BINARY}" +FIREFOX_PATH='/opt/firefox' + +printf '%s\n' "Removing old Firefox installation" + +sudo rm -r "${FIREFOX_PATH}" + +printf '%s\n' "Downloading & installing Firefox ${FIREFOX_VERSION}" + +wget --quiet --show-progress -O- "${FIREFOX_BINARY_URL}" | sudo tar xj -C /opt + +printf '%s\n' "Firefox ${FIREFOX_VERSION} installed" + +{ + printf '%s\n' 'pref("general.config.filename", "firefox.cfg");' + printf '%s\n' 'pref("general.config.obscure_value", 0);' +} | sudo tee "${FIREFOX_PATH}/defaults/pref/autoconfig.js" + +sudo cp .circleci/scripts/firefox.cfg "${FIREFOX_PATH}" + +printf '%s\n' "Firefox ${FIREFOX_VERSION} configured" diff --git a/.circleci/scripts/firefox-install.sh b/.circleci/scripts/firefox-install.sh deleted file mode 100755 index 1c60f4de988c..000000000000 --- a/.circleci/scripts/firefox-install.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -echo "Installing firefox..." -sudo rm -r /opt/firefox -sudo mv firefox /opt/firefox61 -sudo mv /usr/bin/firefox /usr/bin/firefox-old -sudo ln -s /opt/firefox61/firefox /usr/bin/firefox -echo "Firefox installed." diff --git a/.circleci/scripts/firefox.cfg b/.circleci/scripts/firefox.cfg new file mode 100644 index 000000000000..68dd285f28e9 --- /dev/null +++ b/.circleci/scripts/firefox.cfg @@ -0,0 +1,13 @@ +// IMPORTANT: Start your code on the 2nd line + +lockPref("app.update.enabled", false); +lockPref("app.update.auto", false); +lockPref("app.update.mode", 0); +lockPref("app.update.service.enabled", false); + +pref("browser.rights.3.shown", true); + +pref("browser.startup.homepage_override.mstone","ignore"); + +lockPref("plugins.hide_infobar_for_outdated_plugin", true); +clearPref("plugins.update.url"); diff --git a/CHANGELOG.md b/CHANGELOG.md index c61d566b43c6..ddaa496dd2ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,8 @@ - [#4691](https://github.com/MetaMask/metamask-extension/pull/4691): Redesign of the Confirm Transaction Screen. - [#4840](https://github.com/MetaMask/metamask-extension/pull/4840): Now shows notifications when transactions are completed. - [#4855](https://github.com/MetaMask/metamask-extension/pull/4855): Allow the use of HTTP prefix for custom rpc urls. +- [#4855](https://github.com/MetaMask/metamask-extension/pull/4855): network.js: convert rpc protocol to lower case. +- [#4898](https://github.com/MetaMask/metamask-extension/pull/4898): Restore multiple consecutive accounts with balances. ## 4.8.0 Thur Jun 14 2018 diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index ddfcf6f1246c..5a1f7089ca9b 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -119,8 +119,8 @@ "close": { "message": "Close" }, - "chromeRequiredForTrezor":{ - "message": "You need to use MetaMask on Google Chrome in order to connect to your TREZOR device." + "chromeRequiredForHardwareWallets":{ + "message": "You need to use MetaMask on Google Chrome in order to connect to your Hardware Wallet." }, "confirm": { "message": "Confirm" @@ -146,15 +146,12 @@ "connecting": { "message": "Connecting..." }, + "connectToLedger": { + "message": "Connect to Ledger" + }, "connectToTrezor": { "message": "Connect to Trezor" }, - "connectToTrezorHelp": { - "message": "MetaMask is able to access your TREZOR Ethereum accounts. First make sure your device is connected and unlocked." - }, - "connectToTrezorTrouble": { - "message": "If you are having trouble, please make sure you are using the latest version of the TREZOR firmware." - }, "continue": { "message": "Continue" }, @@ -289,8 +286,8 @@ "downloadStateLogs": { "message": "Download State Logs" }, - "dontHaveATrezorWallet": { - "message": "Don't have a TREZOR hardware wallet?" + "dontHaveAHardwareWallet": { + "message": "Don’t have a hardware wallet?" }, "dropped": { "message": "Dropped" @@ -426,11 +423,11 @@ "hardwareWalletConnected": { "message": "Hardware wallet connected" }, - "hardwareSupport": { - "message": "Hardware Support" + "hardwareWallets": { + "message": "Connect a hardware wallet" }, - "hardwareSupportMsg": { - "message": "You can now view your Hardware accounts in MetaMask! Scroll down and read how it works." + "hardwareWalletsMsg": { + "message": "Select a hardware wallet you'd like to use with MetaMask" }, "havingTroubleConnecting": { "message": "Having trouble connecting?" @@ -538,6 +535,9 @@ "learnMore": { "message": "Learn more" }, + "ledgerAccountRestriction": { + "message": "You need to make use your last account before you can add a new one." + }, "lessThanMax": { "message": "must be less than or equal to $1.", "description": "helper for inputting hex as decimal input" @@ -635,6 +635,9 @@ "newRPC": { "message": "New RPC URL" }, + "optionalChainId": { + "message": "ChainId (optional)" + }, "next": { "message": "Next" }, @@ -817,6 +820,9 @@ "ropsten": { "message": "Ropsten Test Network" }, + "classic": { + "message": "Ethereum Classic Network" + }, "rpc": { "message": "Custom RPC" }, @@ -835,6 +841,9 @@ "connectingToRinkeby": { "message": "Connecting to Rinkeby Test Network" }, + "connectingToClassic": { + "message": "Connecting to Ethereum Classic Network" + }, "connectingToUnknown": { "message": "Connecting to Unknown Network" }, @@ -908,7 +917,7 @@ "description": "displays token symbol" }, "orderOneHere": { - "message": "Order one here." + "message": "Order a Trezor or Ledger and keep your funds in cold storage" }, "searchTokens": { "message": "Search Tokens" @@ -920,7 +929,13 @@ "message": "Select an Account" }, "selectAnAccountHelp": { - "message": "These are the accounts available in your hardware wallet. Select the one you’d like to use in MetaMask." + "message": "Select the account to view in MetaMask" + }, + "selectHdPath": { + "message": "Select HD Path" + }, + "selectPathHelp": { + "message": "If you don't see your existing Ledger accounts below, try switching paths to \"Legacy (MEW / MyCrypto)\"" }, "sendTokensAnywhere": { "message": "Send Tokens to anyone with an Ethereum account" diff --git a/app/images/etc_logo.svg b/app/images/etc_logo.svg new file mode 100644 index 000000000000..13bc35429fe0 --- /dev/null +++ b/app/images/etc_logo.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/images/ledger-logo.svg b/app/images/ledger-logo.svg new file mode 100644 index 000000000000..21b99d0e5b75 --- /dev/null +++ b/app/images/ledger-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/images/metamask-fox.svg b/app/images/logo/metamask-fox.svg similarity index 100% rename from app/images/metamask-fox.svg rename to app/images/logo/metamask-fox.svg diff --git a/app/images/logo/metamask-logo-horizontal-beta.svg b/app/images/logo/metamask-logo-horizontal-beta.svg new file mode 100644 index 000000000000..b1fa040acf15 --- /dev/null +++ b/app/images/logo/metamask-logo-horizontal-beta.svg @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/images/trezor-logo.svg b/app/images/trezor-logo.svg new file mode 100644 index 000000000000..b8d85e3afe75 --- /dev/null +++ b/app/images/trezor-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/scripts/controllers/currency.js b/app/scripts/controllers/currency.js index d5bc5fe2bb9a..b80e1344b190 100644 --- a/app/scripts/controllers/currency.js +++ b/app/scripts/controllers/currency.js @@ -25,6 +25,7 @@ class CurrencyController { */ constructor (opts = {}) { const initState = extend({ + fromCurrency: 'ETH', currentCurrency: 'usd', conversionRate: 0, conversionDate: 'N/A', @@ -36,6 +37,26 @@ class CurrencyController { // PUBLIC METHODS // + /** + * A getter for the fromCurrency property + * + * @returns {string} A 2-4 character shorthand that describes the specific currency + * + */ + getFromCurrency () { + return this.store.getState().fromCurrency + } + + /** + * A setter for the fromCurrency property + * + * @param {string} fromCurrency The new currency to set as the fromCurrency in the store + * + */ + setFromCurrency (fromCurrency) { + this.store.updateState({ ticker: fromCurrency, fromCurrency }) + } + /** * A getter for the currentCurrency property * @@ -104,15 +125,16 @@ class CurrencyController { * */ async updateConversionRate () { - let currentCurrency + let currentCurrency, fromCurrency try { currentCurrency = this.getCurrentCurrency() - const response = await fetch(`https://api.infura.io/v1/ticker/eth${currentCurrency.toLowerCase()}`) + fromCurrency = this.getFromCurrency() + const response = await fetch(`https://min-api.cryptocompare.com/data/pricehistorical?fsym=${fromCurrency.toUpperCase()}&tsyms=${currentCurrency.toUpperCase()}`) const parsedResponse = await response.json() - this.setConversionRate(Number(parsedResponse.bid)) - this.setConversionDate(Number(parsedResponse.timestamp)) + this.setConversionRate(Number(parsedResponse[fromCurrency.toUpperCase()][currentCurrency.toUpperCase()])) + this.setConversionDate(parseInt(new Date().getTime() / 1000)) } catch (err) { - log.warn(`MetaMask - Failed to query currency conversion:`, currentCurrency, err) + log.warn(`MetaMask - Failed to query currency conversion:`, fromCurrency, currentCurrency, err) this.setConversionRate(0) this.setConversionDate('N/A') } diff --git a/app/scripts/controllers/network/enums.js b/app/scripts/controllers/network/enums.js index 3190eb37c717..f0ef73f0f469 100644 --- a/app/scripts/controllers/network/enums.js +++ b/app/scripts/controllers/network/enums.js @@ -3,29 +3,35 @@ const RINKEBY = 'rinkeby' const KOVAN = 'kovan' const MAINNET = 'mainnet' const LOCALHOST = 'localhost' +const CLASSIC = 'classic' const MAINNET_CODE = 1 const ROPSTEN_CODE = 3 const RINKEYBY_CODE = 4 const KOVAN_CODE = 42 +const CLASSIC_CODE = 61 const ROPSTEN_DISPLAY_NAME = 'Ropsten' const RINKEBY_DISPLAY_NAME = 'Rinkeby' const KOVAN_DISPLAY_NAME = 'Kovan' const MAINNET_DISPLAY_NAME = 'Main Ethereum Network' +const CLASSIC_DISPLAY_NAME = 'Ethereum Classic' module.exports = { ROPSTEN, RINKEBY, KOVAN, MAINNET, + CLASSIC, LOCALHOST, MAINNET_CODE, ROPSTEN_CODE, RINKEYBY_CODE, KOVAN_CODE, + CLASSIC_CODE, ROPSTEN_DISPLAY_NAME, RINKEBY_DISPLAY_NAME, KOVAN_DISPLAY_NAME, MAINNET_DISPLAY_NAME, + CLASSIC_DISPLAY_NAME, } diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js index 76fdc339188f..168483a2f319 100644 --- a/app/scripts/controllers/network/network.js +++ b/app/scripts/controllers/network/network.js @@ -11,15 +11,19 @@ const createInfuraClient = require('./createInfuraClient') const createJsonRpcClient = require('./createJsonRpcClient') const createLocalhostClient = require('./createLocalhostClient') const { createSwappableProxy, createEventEmitterProxy } = require('swappable-obj-proxy') +const networks = require('./networks') +const extend = require('xtend') const { ROPSTEN, RINKEBY, KOVAN, MAINNET, + CLASSIC, LOCALHOST, } = require('./enums') const INFURA_PROVIDER_TYPES = [ROPSTEN, RINKEBY, KOVAN, MAINNET] +const ALL_PROVIDER_TYPES = [ROPSTEN, RINKEBY, KOVAN, MAINNET, CLASSIC] const env = process.env.METAMASK_ENV const METAMASK_DEBUG = process.env.METAMASK_DEBUG @@ -29,6 +33,10 @@ const defaultProviderConfig = { type: testMode ? RINKEBY : MAINNET, } +const defaultNetworkConfig = { + ticker: 'ETH', +} + module.exports = class NetworkController extends EventEmitter { constructor (opts = {}) { @@ -39,7 +47,8 @@ module.exports = class NetworkController extends EventEmitter { // create stores this.providerStore = new ObservableStore(providerConfig) this.networkStore = new ObservableStore('loading') - this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore }) + this.networkConfig = new ObservableStore(defaultNetworkConfig) + this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore, settings: this.networkConfig }) this.on('networkDidChange', this.lookupNetwork) // provider and block tracker this._provider = null @@ -51,8 +60,8 @@ module.exports = class NetworkController extends EventEmitter { initializeProvider (providerParams) { this._baseProviderParams = providerParams - const { type, rpcTarget } = this.providerStore.getState() - this._configureProvider({ type, rpcTarget }) + const { type, rpcTarget, chainId } = this.providerStore.getState() + this._configureProvider({ type, rpcTarget, chainId }) this.lookupNetwork() } @@ -72,7 +81,20 @@ module.exports = class NetworkController extends EventEmitter { return this.networkStore.getState() } - setNetworkState (network) { + getNetworkConfig () { + return this.networkConfig.getState() + } + + setNetworkState (network, type) { + if (network === 'loading') { + return this.networkStore.putState(network) + } + + // type must be defined + if (!type) { + return + } + network = networks.networkList[type] && networks.networkList[type].chainId ? networks.networkList[type].chainId : network return this.networkStore.putState(network) } @@ -85,25 +107,27 @@ module.exports = class NetworkController extends EventEmitter { if (!this._provider) { return log.warn('NetworkController - lookupNetwork aborted due to missing provider') } + var { type } = this.providerStore.getState() const ethQuery = new EthQuery(this._provider) ethQuery.sendAsync({ method: 'net_version' }, (err, network) => { if (err) return this.setNetworkState('loading') log.info('web3.getNetwork returned ' + network) - this.setNetworkState(network) + this.setNetworkState(network, type) }) } - setRpcTarget (rpcTarget) { + setRpcTarget (rpcTarget, chainId) { const providerConfig = { type: 'rpc', rpcTarget, + chainId, } this.providerConfig = providerConfig } async setProviderType (type) { assert.notEqual(type, 'rpc', `NetworkController - cannot call "setProviderType" with type 'rpc'. use "setRpcTarget"`) - assert(INFURA_PROVIDER_TYPES.includes(type) || type === LOCALHOST, `NetworkController - Unknown rpc type "${type}"`) + assert(ALL_PROVIDER_TYPES.includes(type) || type === LOCALHOST, `NetworkController - Unknown rpc type "${type}"`) const providerConfig = { type } this.providerConfig = providerConfig } @@ -132,17 +156,20 @@ module.exports = class NetworkController extends EventEmitter { } _configureProvider (opts) { - const { type, rpcTarget } = opts + const { type, rpcTarget, chainId } = opts // infura type-based endpoints const isInfura = INFURA_PROVIDER_TYPES.includes(type) if (isInfura) { this._configureInfuraProvider(opts) + // other predefined endpoints + } else if (ALL_PROVIDER_TYPES.includes(type)){ + this._configurePredefinedProvider(opts) // other type-based rpc endpoints } else if (type === LOCALHOST) { this._configureLocalhostProvider() // url-based rpc endpoints } else if (type === 'rpc') { - this._configureStandardProvider({ rpcUrl: rpcTarget }) + this._configureStandardProvider({ rpcUrl: rpcTarget, chainId }) } else { throw new Error(`NetworkController - _configureProvider - unknown type "${type}"`) } @@ -152,6 +179,11 @@ module.exports = class NetworkController extends EventEmitter { log.info('NetworkController - configureInfuraProvider', type) const networkClient = createInfuraClient({ network: type }) this._setNetworkClient(networkClient) + // setup networkConfig + var settings = { + ticker: 'ETH', + } + this.networkConfig.putState(settings) } _configureLocalhostProvider () { @@ -160,9 +192,34 @@ module.exports = class NetworkController extends EventEmitter { this._setNetworkClient(networkClient) } - _configureStandardProvider ({ rpcUrl }) { + _configurePredefinedProvider ({ type }) { + log.info('NetworkController - configurePredefinedProvider', type) + // setup networkConfig + var settings = { + network: networks.networkList[type].chainId, + } + settings = extend(settings, networks.networkList[type]) + const rpcUrl = networks.networkList[type].rpcUrl + const networkClient = createJsonRpcClient({ rpcUrl }) + this.networkConfig.putState(settings) + this._setNetworkClient(networkClient) + } + + _configureStandardProvider ({ rpcUrl, chainId }) { log.info('NetworkController - configureStandardProvider', rpcUrl) const networkClient = createJsonRpcClient({ rpcUrl }) + // hack to add a 'rpc' network with chainId + networks.networkList['rpc'] = { + chainId: chainId, + rpcUrl, + ticker: 'ETH', + } + // setup networkConfig + var settings = { + network: chainId, + } + settings = extend(settings, networks.networkList['rpc']) + this.networkConfig.putState(settings) this._setNetworkClient(networkClient) } diff --git a/app/scripts/controllers/network/networks.js b/app/scripts/controllers/network/networks.js new file mode 100644 index 000000000000..9b188980df95 --- /dev/null +++ b/app/scripts/controllers/network/networks.js @@ -0,0 +1,23 @@ +'use strict' +var networks = function() {} + +const { + CLASSIC, + CLASSIC_CODE, +} = require('./enums') + +networks.networkList = { + [CLASSIC]: { + 'chainId': CLASSIC_CODE, + 'ticker': 'ETC', + 'blockExplorerTx': 'https://gastracker.io/tx/[[txHash]]', + 'blockExplorerAddr': 'https://gastracker.io/addr/[[address]]', + 'blockExplorerToken': 'https://gastracker.io/token/[[tokenAddress]]/[[address]]', + 'service': 'ETC Cooperative', + 'rpcUrl': 'https://ethereumclassic.network', + 'exchanges': ['ShapeShift'], + 'buyUrl': '', + }, +} + +module.exports = networks diff --git a/app/scripts/controllers/network/util.js b/app/scripts/controllers/network/util.js index 261dae7211cd..3d5059db4812 100644 --- a/app/scripts/controllers/network/util.js +++ b/app/scripts/controllers/network/util.js @@ -3,13 +3,16 @@ const { RINKEBY, KOVAN, MAINNET, + CLASSIC, ROPSTEN_CODE, RINKEYBY_CODE, KOVAN_CODE, + CLASSIC_CODE, ROPSTEN_DISPLAY_NAME, RINKEBY_DISPLAY_NAME, KOVAN_DISPLAY_NAME, MAINNET_DISPLAY_NAME, + CLASSIC_DISPLAY_NAME, } = require('./enums') const networkToNameMap = { @@ -17,9 +20,11 @@ const networkToNameMap = { [RINKEBY]: RINKEBY_DISPLAY_NAME, [KOVAN]: KOVAN_DISPLAY_NAME, [MAINNET]: MAINNET_DISPLAY_NAME, + [CLASSIC]: CLASSIC_DISPLAY_NAME, [ROPSTEN_CODE]: ROPSTEN_DISPLAY_NAME, [RINKEYBY_CODE]: RINKEBY_DISPLAY_NAME, [KOVAN_CODE]: KOVAN_DISPLAY_NAME, + [CLASSIC_CODE]: CLASSIC_DISPLAY_NAME, } const getNetworkDisplayName = key => networkToNameMap[key] diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 707fd7de9eb3..51489d73b1a5 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -23,7 +23,7 @@ class PreferencesController { */ constructor (opts = {}) { const initState = extend({ - frequentRpcList: [], + frequentRpcListDetail: [], currentAccountTab: 'history', accountTokens: {}, tokens: [], @@ -298,10 +298,10 @@ class PreferencesController { * @returns {Promise} Promise resolves with undefined * */ - updateFrequentRpcList (_url) { - return this.addToFrequentRpcList(_url) + updateFrequentRpcList (_url, chainId) { + return this.addToFrequentRpcList(_url, chainId) .then((rpcList) => { - this.store.updateState({ frequentRpcList: rpcList }) + this.store.updateState({ frequentRpcListDetail: rpcList }) return Promise.resolve() }) } @@ -329,29 +329,29 @@ class PreferencesController { * @returns {Promise} The updated frequentRpcList. * */ - addToFrequentRpcList (_url) { - const rpcList = this.getFrequentRpcList() - const index = rpcList.findIndex((element) => { return element === _url }) + addToFrequentRpcList (_url, chainId) { + const rpcList = this.getFrequentRpcListDetail() + const index = rpcList.findIndex((element) => { return element.rpcUrl === _url }) if (index !== -1) { rpcList.splice(index, 1) } if (_url !== 'http://localhost:8545') { - rpcList.push(_url) + rpcList.push({rpcUrl : _url, chainId }) } - if (rpcList.length > 2) { + if (rpcList.length > 3) { rpcList.shift() } return Promise.resolve(rpcList) } /** - * Getter for the `frequentRpcList` property. + * Getter for the `frequentRpcListDetail` property. * - * @returns {array} An array of one or two rpc urls. + * @returns {array} An array of rpc urls. * */ - getFrequentRpcList () { - return this.store.getState().frequentRpcList + getFrequentRpcListDetail () { + return this.store.getState().frequentRpcListDetail } /** diff --git a/app/scripts/lib/buy-eth-url.js b/app/scripts/lib/buy-eth-url.js index 4e2d0bc79429..a1df018e8d89 100644 --- a/app/scripts/lib/buy-eth-url.js +++ b/app/scripts/lib/buy-eth-url.js @@ -11,7 +11,7 @@ module.exports = getBuyEthUrl * network does not match any of the specified cases, or if no network is given, returns undefined. * */ -function getBuyEthUrl ({ network, amount, address }) { +function getBuyEthUrl ({ network, amount, address, link }) { let url switch (network) { case '1': @@ -29,6 +29,14 @@ function getBuyEthUrl ({ network, amount, address }) { case '42': url = 'https://github.com/kovan-testnet/faucet' break + + default: + if (link) { + url = link.replace('[[amount]]', amount).replace('[[address]]', address) + } else { + url = '' + } + break } return url } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 81bb080abf6e..6525f9d584b2 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -48,6 +48,8 @@ const percentile = require('percentile') const seedPhraseVerifier = require('./lib/seed-phrase-verifier') const log = require('loglevel') const TrezorKeyring = require('eth-trezor-keyring') +const LedgerBridgeKeyring = require('eth-ledger-bridge-keyring') +const EthQuery = require('eth-query') module.exports = class MetamaskController extends EventEmitter { @@ -127,7 +129,7 @@ module.exports = class MetamaskController extends EventEmitter { }) // key mgmt - const additionalKeyrings = [TrezorKeyring] + const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring] this.keyringController = new KeyringController({ keyringTypes: additionalKeyrings, initState: initState.KeyringController, @@ -191,6 +193,8 @@ module.exports = class MetamaskController extends EventEmitter { }) this.networkController.on('networkDidChange', () => { this.balancesController.updateAllBalances() + var currentCurrency = this.currencyController.getCurrentCurrency() + this.setCurrentCurrency(currentCurrency, function() {}) }) this.balancesController.updateAllBalances() @@ -371,9 +375,7 @@ module.exports = class MetamaskController extends EventEmitter { connectHardware: nodeify(this.connectHardware, this), forgetDevice: nodeify(this.forgetDevice, this), checkHardwareStatus: nodeify(this.checkHardwareStatus, this), - - // TREZOR - unlockTrezorAccount: nodeify(this.unlockTrezorAccount, this), + unlockHardwareWalletAccount: nodeify(this.unlockHardwareWalletAccount, this), // vault management submitPassword: nodeify(this.submitPassword, this), @@ -475,12 +477,32 @@ module.exports = class MetamaskController extends EventEmitter { async createNewVaultAndRestore (password, seed) { const releaseLock = await this.createVaultMutex.acquire() try { + let accounts, lastBalance + + const keyringController = this.keyringController + // clear known identities this.preferencesController.setAddresses([]) // create new vault - const vault = await this.keyringController.createNewVaultAndRestore(password, seed) + const vault = await keyringController.createNewVaultAndRestore(password, seed) + + const ethQuery = new EthQuery(this.provider) + accounts = await keyringController.getAccounts() + lastBalance = await this.getBalance(accounts[accounts.length - 1], ethQuery) + + const primaryKeyring = keyringController.getKeyringsByType('HD Key Tree')[0] + if (!primaryKeyring) { + throw new Error('MetamaskController - No HD Key Tree found') + } + + // seek out the first zero balance + while (lastBalance !== '0x0') { + await keyringController.addNewAccount(primaryKeyring) + accounts = await keyringController.getAccounts() + lastBalance = await this.getBalance(accounts[accounts.length - 1], ethQuery) + } + // set new identities - const accounts = await this.keyringController.getAccounts() this.preferencesController.setAddresses(accounts) this.selectFirstIdentity() releaseLock() @@ -491,6 +513,30 @@ module.exports = class MetamaskController extends EventEmitter { } } + /** + * Get an account balance from the AccountTracker or request it directly from the network. + * @param {string} address - The account address + * @param {EthQuery} ethQuery - The EthQuery instance to use when asking the network + */ + getBalance (address, ethQuery) { + return new Promise((resolve, reject) => { + const cached = this.accountTracker.store.getState().accounts[address] + + if (cached && cached.balance) { + resolve(cached.balance) + } else { + ethQuery.getBalance(address, (error, balance) => { + if (error) { + reject(error) + log.error(error) + } else { + resolve(balance || '0x0') + } + }) + } + }) + } + /* * Submits the user's password and attempts to unlock the vault. * Also synchronizes the preferencesController, to ensure its schema @@ -534,45 +580,57 @@ module.exports = class MetamaskController extends EventEmitter { // Hardware // + async getKeyringForDevice (deviceName, hdPath = null) { + let keyringName = null + switch (deviceName) { + case 'trezor': + keyringName = TrezorKeyring.type + break + case 'ledger': + keyringName = LedgerBridgeKeyring.type + break + default: + throw new Error('MetamaskController:getKeyringForDevice - Unknown device') + } + let keyring = await this.keyringController.getKeyringsByType(keyringName)[0] + if (!keyring) { + keyring = await this.keyringController.addNewKeyring(keyringName) + } + if (hdPath && keyring.setHdPath) { + keyring.setHdPath(hdPath) + } + + keyring.network = this.networkController.getProviderConfig().type + + return keyring + + } + /** * Fetch account list from a trezor device. * * @returns [] accounts */ - async connectHardware (deviceName, page) { - - switch (deviceName) { - case 'trezor': - const keyringController = this.keyringController - const oldAccounts = await keyringController.getAccounts() - let keyring = await keyringController.getKeyringsByType( - 'Trezor Hardware' - )[0] - if (!keyring) { - keyring = await this.keyringController.addNewKeyring('Trezor Hardware') - } - let accounts = [] - - switch (page) { - case -1: - accounts = await keyring.getPreviousPage() - break - case 1: - accounts = await keyring.getNextPage() - break - default: - accounts = await keyring.getFirstPage() - } - - // Merge with existing accounts - // and make sure addresses are not repeated - const accountsToTrack = [...new Set(oldAccounts.concat(accounts.map(a => a.address.toLowerCase())))] - this.accountTracker.syncWithAddresses(accountsToTrack) - return accounts - - default: - throw new Error('MetamaskController:connectHardware - Unknown device') + async connectHardware (deviceName, page, hdPath) { + const keyring = await this.getKeyringForDevice(deviceName, hdPath) + let accounts = [] + switch (page) { + case -1: + accounts = await keyring.getPreviousPage() + break + case 1: + accounts = await keyring.getNextPage() + break + default: + accounts = await keyring.getFirstPage() } + + // Merge with existing accounts + // and make sure addresses are not repeated + const oldAccounts = await this.keyringController.getAccounts() + const accountsToTrack = [...new Set(oldAccounts.concat(accounts.map(a => a.address.toLowerCase())))] + this.accountTracker.syncWithAddresses(accountsToTrack) + return accounts } /** @@ -580,21 +638,9 @@ module.exports = class MetamaskController extends EventEmitter { * * @returns {Promise} */ - async checkHardwareStatus (deviceName) { - - switch (deviceName) { - case 'trezor': - const keyringController = this.keyringController - const keyring = await keyringController.getKeyringsByType( - 'Trezor Hardware' - )[0] - if (!keyring) { - return false - } - return keyring.isUnlocked() - default: - throw new Error('MetamaskController:checkHardwareStatus - Unknown device') - } + async checkHardwareStatus (deviceName, hdPath) { + const keyring = await this.getKeyringForDevice(deviceName, hdPath) + return keyring.isUnlocked() } /** @@ -604,20 +650,9 @@ module.exports = class MetamaskController extends EventEmitter { */ async forgetDevice (deviceName) { - switch (deviceName) { - case 'trezor': - const keyringController = this.keyringController - const keyring = await keyringController.getKeyringsByType( - 'Trezor Hardware' - )[0] - if (!keyring) { - throw new Error('MetamaskController:forgetDevice - Trezor Hardware keyring not found') - } - keyring.forgetDevice() - return true - default: - throw new Error('MetamaskController:forgetDevice - Unknown device') - } + const keyring = await this.getKeyringForDevice(deviceName) + keyring.forgetDevice() + return true } /** @@ -625,23 +660,17 @@ module.exports = class MetamaskController extends EventEmitter { * * @returns {} keyState */ - async unlockTrezorAccount (index) { - const keyringController = this.keyringController - const keyring = await keyringController.getKeyringsByType( - 'Trezor Hardware' - )[0] - if (!keyring) { - throw new Error('MetamaskController - No Trezor Hardware Keyring found') - } + async unlockHardwareWalletAccount (index, deviceName, hdPath) { + const keyring = await this.getKeyringForDevice(deviceName, hdPath) keyring.setAccountToUnlock(index) - const oldAccounts = await keyringController.getAccounts() - const keyState = await keyringController.addNewAccount(keyring) - const newAccounts = await keyringController.getAccounts() + const oldAccounts = await this.keyringController.getAccounts() + const keyState = await this.keyringController.addNewAccount(keyring) + const newAccounts = await this.keyringController.getAccounts() this.preferencesController.setAddresses(newAccounts) newAccounts.forEach(address => { if (!oldAccounts.includes(address)) { - this.preferencesController.setAccountLabel(address, `TREZOR #${parseInt(index, 10) + 1}`) + this.preferencesController.setAccountLabel(address, `${deviceName.toUpperCase()} ${parseInt(index, 10) + 1}`) this.preferencesController.setSelectedAddress(address) } }) @@ -1302,10 +1331,13 @@ module.exports = class MetamaskController extends EventEmitter { * @param {Function} cb - A callback function returning currency info. */ setCurrentCurrency (currencyCode, cb) { + const { ticker } = this.networkController.getNetworkConfig() try { + this.currencyController.setFromCurrency(ticker) this.currencyController.setCurrentCurrency(currencyCode) this.currencyController.updateConversionRate() const data = { + fromCurrency: ticker || 'ETH', conversionRate: this.currencyController.getConversionRate(), currentCurrency: this.currencyController.getCurrentCurrency(), conversionDate: this.currencyController.getConversionDate(), @@ -1326,7 +1358,8 @@ module.exports = class MetamaskController extends EventEmitter { buyEth (address, amount) { if (!amount) amount = '5' const network = this.networkController.getNetworkState() - const url = getBuyEthUrl({ network, address, amount }) + const link = this.networkController.getNetworkConfig().buyUrl + const url = getBuyEthUrl({ network, address, amount, link }) if (url) this.platform.openWindow({ url }) } @@ -1346,9 +1379,9 @@ module.exports = class MetamaskController extends EventEmitter { * @param {string} rpcTarget - A URL for a valid Ethereum RPC API. * @returns {Promise} - The RPC Target URL confirmed. */ - async setCustomRpc (rpcTarget) { - this.networkController.setRpcTarget(rpcTarget) - await this.preferencesController.updateFrequentRpcList(rpcTarget) + async setCustomRpc (rpcTarget, chainId) { + this.networkController.setRpcTarget(rpcTarget, chainId) + await this.preferencesController.updateFrequentRpcList(rpcTarget, chainId) return rpcTarget } diff --git a/old-ui/app/app.js b/old-ui/app/app.js index d3e9e823b0d5..d2689330d20a 100644 --- a/old-ui/app/app.js +++ b/old-ui/app/app.js @@ -72,7 +72,7 @@ function mapStateToProps (state) { forgottenPassword: state.appState.forgottenPassword, nextUnreadNotice: state.metamask.nextUnreadNotice, lostAccounts: state.metamask.lostAccounts, - frequentRpcList: state.metamask.frequentRpcList || [], + frequentRpcListDetail: state.metamask.frequentRpcListDetail || [], featureFlags, // state needed to get account dropdown temporarily rendering from app bar @@ -298,6 +298,8 @@ App.prototype.getNetworkName = function () { name = 'Kovan Test Network' } else if (providerName === 'rinkeby') { name = 'Rinkeby Test Network' + } else if (providerName === 'classic') { + name = 'Ethereum Classic Network' } else { name = 'Unknown Private Network' } diff --git a/old-ui/app/components/account-dropdowns.js b/old-ui/app/components/account-dropdowns.js index 262de66019aa..2b9284278b0d 100644 --- a/old-ui/app/components/account-dropdowns.js +++ b/old-ui/app/components/account-dropdowns.js @@ -2,7 +2,7 @@ const Component = require('react').Component const PropTypes = require('prop-types') const h = require('react-hyperscript') const actions = require('../../../ui/app/actions') -const genAccountLink = require('etherscan-link').createAccountLink +const genAccountLink = require('../../../ui/lib/account-link.js') const connect = require('react-redux').connect const Dropdown = require('./dropdown').Dropdown const DropdownMenuItem = require('./dropdown').DropdownMenuItem @@ -189,7 +189,11 @@ class AccountDropdowns extends Component { closeMenu: () => {}, onClick: () => { const { selected, network } = this.props - const url = genAccountLink(selected, network) + let url + if (this.props.settings && this.props.settings.blockExplorerAddr) { + url = this.props.settings.blockExplorerAddr + } + url = genAccountLink(selected, network, url) global.platform.openWindow({ url }) }, }, @@ -298,6 +302,7 @@ AccountDropdowns.propTypes = { keyrings: PropTypes.array, actions: PropTypes.objectOf(PropTypes.func), network: PropTypes.string, + settings: PropTypes.object, style: PropTypes.object, enableAccountOptions: PropTypes.bool, enableAccountsSelector: PropTypes.bool, @@ -316,6 +321,12 @@ const mapDispatchToProps = (dispatch) => { } } +function mapStateToProps (state) { + return { + settings: state.metamask.settings, + } +} + module.exports = { - AccountDropdowns: connect(null, mapDispatchToProps)(AccountDropdowns), + AccountDropdowns: connect(mapStateToProps, mapDispatchToProps)(AccountDropdowns), } diff --git a/old-ui/app/components/app-bar.js b/old-ui/app/components/app-bar.js index 8ab647efdbf9..d1dfcf4858dc 100644 --- a/old-ui/app/components/app-bar.js +++ b/old-ui/app/components/app-bar.js @@ -17,7 +17,7 @@ module.exports = class AppBar extends Component { static propTypes = { dispatch: PropTypes.func.isRequired, - frequentRpcList: PropTypes.array.isRequired, + frequentRpcListDetail: PropTypes.array.isRequired, isMascara: PropTypes.bool.isRequired, isOnboarding: PropTypes.bool.isRequired, identities: PropTypes.any.isRequired, @@ -196,7 +196,7 @@ module.exports = class AppBar extends Component { renderNetworkDropdown () { const { dispatch, - frequentRpcList: rpcList, + frequentRpcListDetail: rpcList, provider, } = this.props const { @@ -287,6 +287,20 @@ module.exports = class AppBar extends Component { ? h('.check', '✓') : null, ]), + h(DropdownMenuItem, { + key: 'classic', + closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), + onClick: () => dispatch(actions.setProviderType('classic')), + style: { + fontSize: '18px', + }, + }, [ + h('.menu-icon.diamond'), + 'Ethereum Classic Network', + providerType === 'classic' + ? h('.check', '✓') + : null, + ]), h(DropdownMenuItem, { key: 'default', closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }), @@ -322,7 +336,7 @@ module.exports = class AppBar extends Component { } renderCustomOption ({ rpcTarget, type }) { - const {dispatch} = this.props + const {dispatch, network} = this.props if (type !== 'rpc') { return null @@ -340,7 +354,7 @@ module.exports = class AppBar extends Component { default: return h(DropdownMenuItem, { key: rpcTarget, - onClick: () => dispatch(actions.setRpcTarget(rpcTarget)), + onClick: () => dispatch(actions.setRpcTarget(rpcTarget, network)), closeMenu: () => this.setState({ isNetworkMenuOpen: false }), }, [ h('i.fa.fa-question-circle.fa-lg.menu-icon'), @@ -350,21 +364,24 @@ module.exports = class AppBar extends Component { } } - renderCommonRpc (rpcList, {rpcTarget}) { + renderCommonRpc (rpcList, provider) { const {dispatch} = this.props + const {rpcTarget, type} = provider - return rpcList.map((rpc) => { - if ((rpc === LOCALHOST_RPC_URL) || (rpc === rpcTarget)) { + return rpcList.map((entry) => { + const rpc = entry.rpcUrl + const selected = type === 'rpc' && rpcTarget === rpc + if ((rpc === LOCALHOST_RPC_URL) || selected) { return null } else { return h(DropdownMenuItem, { key: `common${rpc}`, closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - onClick: () => dispatch(actions.setRpcTarget(rpc)), + onClick: () => dispatch(actions.setRpcTarget(rpc, entry.chainId)), }, [ h('i.fa.fa-question-circle.fa-lg.menu-icon'), rpc, - rpcTarget === rpc + selected ? h('.check', '✓') : null, ]) diff --git a/old-ui/app/components/balance.js b/old-ui/app/components/balance.js index 57ca845649ee..d1fbc2c78b7a 100644 --- a/old-ui/app/components/balance.js +++ b/old-ui/app/components/balance.js @@ -1,12 +1,18 @@ const Component = require('react').Component const h = require('react-hyperscript') +const connect = require('react-redux').connect const inherits = require('util').inherits const formatBalance = require('../util').formatBalance const generateBalanceObject = require('../util').generateBalanceObject const Tooltip = require('./tooltip.js') const FiatValue = require('./fiat-value.js') -module.exports = EthBalanceComponent +module.exports = connect(mapStateToProps)(EthBalanceComponent) +function mapStateToProps (state) { + return { + ticker: state.metamask.ticker, + } +} inherits(EthBalanceComponent, Component) function EthBalanceComponent () { @@ -16,11 +22,16 @@ function EthBalanceComponent () { EthBalanceComponent.prototype.render = function () { var props = this.props let { value } = props + const { ticker } = props var style = props.style var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true value = value ? formatBalance(value, 6, needsParse) : '...' var width = props.width + if (ticker !== 'ETH') { + value = value.replace(/ETH/, ticker) + } + return ( h('.ether-balance.ether-balance-amount', { diff --git a/old-ui/app/components/buy-button-subview.js b/old-ui/app/components/buy-button-subview.js index 8bb73ae3e9c3..a7e29859c569 100644 --- a/old-ui/app/components/buy-button-subview.js +++ b/old-ui/app/components/buy-button-subview.js @@ -20,6 +20,8 @@ function mapStateToProps (state) { buyView: state.appState.buyView, network: state.metamask.network, provider: state.metamask.provider, + ticker: state.metamask.ticker, + settings: state.metamask.settings, context: state.appState.currentView.context, isSubLoading: state.appState.isSubLoading, } @@ -170,15 +172,39 @@ BuyButtonSubview.prototype.primarySubview = function () { ) default: - return ( - h('h2.error', 'Unknown network ID') - ) - + return this.mainnetSubview() } } BuyButtonSubview.prototype.mainnetSubview = function () { const props = this.props + const network = parseInt(props.network) + + let selected + if (network === 1) { + selected = [ + 'Coinbase', + 'ShapeShift', + ] + } else { + selected = this.props.settings.exchanges || [] + } + + const subtext = { + 'Coinbase': 'Crypto/FIAT (USA only)', + 'ShapeShift': 'Crypto', + } + + let texts = {} + selected.forEach(ex => { + texts[ex] = subtext[ex] + }) + + if (selected.length === 0) { + return ( + h('h2.error', 'No exchange supported') + ) + } return ( @@ -198,14 +224,8 @@ BuyButtonSubview.prototype.mainnetSubview = function () { }, [ h(RadioList, { defaultFocus: props.buyView.subview, - labels: [ - 'Coinbase', - 'ShapeShift', - ], - subtext: { - 'Coinbase': 'Crypto/FIAT (USA only)', - 'ShapeShift': 'Crypto', - }, + labels: selected, + subtext: texts, onClick: this.radioHandler.bind(this), }), ]), @@ -229,13 +249,10 @@ BuyButtonSubview.prototype.mainnetSubview = function () { } BuyButtonSubview.prototype.formVersionSubview = function () { - const network = this.props.network - if (network === '1') { - if (this.props.buyView.formView.coinbase) { - return h(CoinbaseForm, this.props) - } else if (this.props.buyView.formView.shapeshift) { - return h(ShapeshiftForm, this.props) - } + if (this.props.buyView.formView.coinbase) { + return h(CoinbaseForm, this.props) + } else if (this.props.buyView.formView.shapeshift) { + return h(ShapeshiftForm, this.props) } } @@ -252,10 +269,11 @@ BuyButtonSubview.prototype.backButtonContext = function () { } BuyButtonSubview.prototype.radioHandler = function (event) { + const ticker = this.props.ticker switch (event.target.title) { case 'Coinbase': return this.props.dispatch(actions.coinBaseSubview()) case 'ShapeShift': - return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type)) + return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type, ticker)) } } diff --git a/old-ui/app/components/eth-balance.js b/old-ui/app/components/eth-balance.js index 4f538fd31f31..38219d00ebea 100644 --- a/old-ui/app/components/eth-balance.js +++ b/old-ui/app/components/eth-balance.js @@ -1,12 +1,18 @@ const Component = require('react').Component const h = require('react-hyperscript') +const connect = require('react-redux').connect const inherits = require('util').inherits const formatBalance = require('../util').formatBalance const generateBalanceObject = require('../util').generateBalanceObject const Tooltip = require('./tooltip.js') const FiatValue = require('./fiat-value.js') -module.exports = EthBalanceComponent +module.exports = connect(mapStateToProps)(EthBalanceComponent) +function mapStateToProps (state) { + return { + ticker: state.metamask.ticker, + } +} inherits(EthBalanceComponent, Component) function EthBalanceComponent () { @@ -16,10 +22,14 @@ function EthBalanceComponent () { EthBalanceComponent.prototype.render = function () { var props = this.props let { value } = props - const { style, width } = props + const { ticker, style, width } = props var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true value = value ? formatBalance(value, 6, needsParse) : '...' + if (ticker !== 'ETH') { + value = value.replace(/ETH/, ticker) + } + return ( h('.ether-balance.ether-balance-amount', { diff --git a/old-ui/app/components/network.js b/old-ui/app/components/network.js index 59596dabd31e..4e12e2733ca5 100644 --- a/old-ui/app/components/network.js +++ b/old-ui/app/components/network.js @@ -55,6 +55,9 @@ Network.prototype.render = function () { } else if (providerName === 'rinkeby') { hoverText = 'Rinkeby Test Network' iconName = 'rinkeby-test-network' + } else if (providerName === 'classic') { + hoverText = 'Ethereum Classic' + iconName = 'ethereum-classic-network' } else { hoverText = 'Unknown Private Network' iconName = 'unknown-private-network' @@ -108,6 +111,16 @@ Network.prototype.render = function () { 'Rinkeby Test Net'), props.onClick && h('i.fa.fa-caret-down.fa-lg'), ]) + case 'ethereum-classic-network': + return h('.network-indicator', [ + h('.menu-icon.diamond'), + h('.network-name', { + style: { + color: '#267f00', + }}, + 'Ethereum Classic Network'), + props.onClick && h('i.fa.fa-caret-down.fa-lg'), + ]) default: return h('.network-indicator', [ h('i.fa.fa-question-circle.fa-lg', { diff --git a/old-ui/app/components/shapeshift-form.js b/old-ui/app/components/shapeshift-form.js index 14de309aba74..e17f53ff9c93 100644 --- a/old-ui/app/components/shapeshift-form.js +++ b/old-ui/app/components/shapeshift-form.js @@ -10,6 +10,7 @@ function mapStateToProps (state) { return { warning: state.appState.warning, isSubLoading: state.appState.isSubLoading, + ticker: state.metamask.ticker, } } @@ -237,7 +238,7 @@ ShapeshiftForm.prototype.updateCoin = function (event) { var message = 'Not a valid coin' return props.dispatch(actions.displayWarning(message)) } else { - return props.dispatch(actions.pairUpdate(coin)) + return props.dispatch(actions.pairUpdate(coin, props.ticker)) } } @@ -249,7 +250,7 @@ ShapeshiftForm.prototype.handleLiveInput = function () { if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') { return null } else { - return props.dispatch(actions.pairUpdate(coin)) + return props.dispatch(actions.pairUpdate(coin, props.ticker)) } } diff --git a/old-ui/app/components/shift-list-item.js b/old-ui/app/components/shift-list-item.js index 5454a90bc438..517219d23bc7 100644 --- a/old-ui/app/components/shift-list-item.js +++ b/old-ui/app/components/shift-list-item.js @@ -3,7 +3,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const connect = require('react-redux').connect const vreme = new (require('vreme'))() -const explorerLink = require('etherscan-link').createExplorerLink +const explorerLink = require('../../../ui/lib/explorer-link.js') const actions = require('../../../ui/app/actions') const addressSummary = require('../util').addressSummary @@ -18,6 +18,7 @@ function mapStateToProps (state) { return { conversionRate: state.metamask.conversionRate, currentCurrency: state.metamask.currentCurrency, + ticker: state.metamask.ticker, } } @@ -79,7 +80,7 @@ ShiftListItem.prototype.renderUtilComponents = function () { title: 'QR Code', }, [ h('i.fa.fa-qrcode.pointer.pop-hover', { - onClick: () => props.dispatch(actions.reshowQrCode(props.depositAddress, props.depositType)), + onClick: () => props.dispatch(actions.reshowQrCode(props.depositAddress, props.depositType, props.ticker)), style: { margin: '5px', marginLeft: '23px', diff --git a/old-ui/app/components/token-cell.js b/old-ui/app/components/token-cell.js index 19d7139bb892..5c224c2a00c0 100644 --- a/old-ui/app/components/token-cell.js +++ b/old-ui/app/components/token-cell.js @@ -1,10 +1,16 @@ const Component = require('react').Component const h = require('react-hyperscript') +const connect = require('react-redux').connect const inherits = require('util').inherits const Identicon = require('./identicon') const prefixForNetwork = require('../../lib/etherscan-prefix-for-network') -module.exports = TokenCell +module.exports = connect(mapStateToProps)(TokenCell) +function mapStateToProps (state) { + return { + settings: state.metamask.settings, + } +} inherits(TokenCell, Component) function TokenCell () { @@ -44,14 +50,22 @@ TokenCell.prototype.render = function () { TokenCell.prototype.send = function (address, event) { event.preventDefault() event.stopPropagation() - const url = tokenFactoryFor(address) + let url + if (this.props.settings && this.props.settings.blockExplorerTokenFactory) { + url = this.props.settings.blockExplorerTokenFactory + } + url = tokenFactoryFor(address, url) if (url) { navigateTo(url) } } TokenCell.prototype.view = function (address, userAddress, network, event) { - const url = etherscanLinkFor(address, userAddress, network) + let url + if (this.props.settings && this.props.settings.blockExplorerToken) { + url = this.props.settings.blockExplorerToken + } + url = etherscanLinkFor(address, userAddress, network, url) if (url) { navigateTo(url) } @@ -61,12 +75,20 @@ function navigateTo (url) { global.platform.openWindow({ url }) } -function etherscanLinkFor (tokenAddress, address, network) { +function etherscanLinkFor (tokenAddress, address, network, url) { + if (url) { + return url.replace('[[tokenAddress]]', tokenAddress).replace('[[address]]', address) + } + const prefix = prefixForNetwork(network) return `https://${prefix}etherscan.io/token/${tokenAddress}?a=${address}` } -function tokenFactoryFor (tokenAddress) { +function tokenFactoryFor (tokenAddress, url) { + if (url) { + return url.replace('[[tokenAddress]]', tokenAddress) + } + return `https://tokenfactory.surge.sh/#/token/${tokenAddress}` } diff --git a/old-ui/app/components/transaction-list-item.js b/old-ui/app/components/transaction-list-item.js index 6ecf7d193c08..d9b8257bccb4 100644 --- a/old-ui/app/components/transaction-list-item.js +++ b/old-ui/app/components/transaction-list-item.js @@ -5,7 +5,7 @@ const connect = require('react-redux').connect const EthBalance = require('./eth-balance') const addressSummary = require('../util').addressSummary -const explorerLink = require('etherscan-link').createExplorerLink +const explorerLink = require('../../../ui/lib/explorer-link.js') const CopyButton = require('./copyButton') const vreme = new (require('vreme'))() const Tooltip = require('./tooltip') @@ -15,13 +15,19 @@ const actions = require('../../../ui/app/actions') const TransactionIcon = require('./transaction-list-item-icon') const ShiftListItem = require('./shift-list-item') +function mapStateToProps (state) { + return { + settings: state.metamask.settings, + } +} + const mapDispatchToProps = dispatch => { return { retryTransaction: transactionId => dispatch(actions.retryTransaction(transactionId)), } } -module.exports = connect(null, mapDispatchToProps)(TransactionListItem) +module.exports = connect(mapStateToProps, mapDispatchToProps)(TransactionListItem) inherits(TransactionListItem, Component) function TransactionListItem () { @@ -88,7 +94,11 @@ TransactionListItem.prototype.render = function () { } event.stopPropagation() if (!transaction.hash || !isLinkable) return - var url = explorerLink(transaction.hash, parseInt(network)) + let url + if (this.props.settings && this.props.settings.blockExplorerTx) { + url = this.props.settings.blockExplorerTx + } + url = explorerLink(transaction.hash, parseInt(network), url) global.platform.openWindow({ url }) }, style: { diff --git a/old-ui/app/config.js b/old-ui/app/config.js index 392a6dba7c1b..1e7fa6a3fe35 100644 --- a/old-ui/app/config.js +++ b/old-ui/app/config.js @@ -68,7 +68,7 @@ ConfigScreen.prototype.render = function () { currentProviderDisplay(metamaskState), - h('div', { style: {display: 'flex'} }, [ + h('div', { style: {display: 'block'} }, [ h('input#new_rpc', { placeholder: 'New RPC URL', style: { @@ -81,7 +81,26 @@ ConfigScreen.prototype.render = function () { if (event.key === 'Enter') { var element = event.target var newRpc = element.value - rpcValidation(newRpc, state) + var chainid = document.querySelector('input#chainid') + rpcValidation(newRpc, chainid.value, state) + } + }, + }), + h('br'), + h('input#chainid', { + placeholder: 'ChainId (optional)', + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + onKeyPress (event) { + if (event.key === 'Enter') { + var element = document.querySelector('input#new_rpc') + var newRpc = element.value + var chainid = document.querySelector('input#chainid') + rpcValidation(newRpc, chainid.value, state) } }, }), @@ -93,7 +112,8 @@ ConfigScreen.prototype.render = function () { event.preventDefault() var element = document.querySelector('input#new_rpc') var newRpc = element.value - rpcValidation(newRpc, state) + var chainid = document.querySelector('input#chainid') + rpcValidation(newRpc, chainid.value, state) }, }, 'Save'), ]), @@ -189,9 +209,9 @@ ConfigScreen.prototype.render = function () { ) } -function rpcValidation (newRpc, state) { +function rpcValidation (newRpc, chainid, state) { if (validUrl.isWebUri(newRpc)) { - state.dispatch(actions.setRpcTarget(newRpc)) + state.dispatch(actions.setRpcTarget(newRpc, chainid)) } else { var appendedRpc = `http://${newRpc}` if (validUrl.isWebUri(appendedRpc)) { @@ -249,6 +269,11 @@ function currentProviderDisplay (metamaskState) { value = 'Rinkeby Test Network' break + case 'classic': + title = 'Current Network' + value = 'Ethereum Classic Network' + break + default: title = 'Current RPC' value = metamaskState.provider.rpcTarget diff --git a/package-lock.json b/package-lock.json index f6d25d7508c6..ec0192500eb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1585,9 +1585,9 @@ } }, "@zxing/library": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.7.0.tgz", - "integrity": "sha512-VJ1cJaCWVF8MspnuyaZKGKlrSQLqQ5usgSap8uuCAvWGQ6W6OwN1NeGvnjhT+9hmnwkHK8XjaflvzaDBC7nKnw==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.8.0.tgz", + "integrity": "sha512-D7oopukr7cJ0Va01Er2zXiSPXvmvc6D1PpOq/THRvd/57yEsBs+setRsiDo7tSRnYHcw7FrRZSZ7rwyzNSLJeA==", "requires": { "text-encoding": "^0.6.4", "ts-custom-error": "^2.2.1" @@ -8372,6 +8372,63 @@ } } }, + "eth-ledger-bridge-keyring": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/eth-ledger-bridge-keyring/-/eth-ledger-bridge-keyring-0.1.0.tgz", + "integrity": "sha512-fZQry1rxA23swq7Qw9JolFltRePwIbKXCn9Vo6Qfr122cqqA3MBzV3WSI+ABQvwf3obQrMpbtqP5tiRxpX/0Vg==", + "requires": { + "eth-sig-util": "^1.4.2", + "ethereumjs-tx": "^1.3.4", + "ethereumjs-util": "^5.1.5", + "events": "^2.0.0", + "hdkey": "0.8.0" + }, + "dependencies": { + "ethereum-common": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.0.18.tgz", + "integrity": "sha1-L9w1dvIykDNYl26znaeDIT/5Uj8=" + }, + "ethereumjs-tx": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-1.3.7.tgz", + "integrity": "sha512-wvLMxzt1RPhAQ9Yi3/HKZTn0FZYpnsmQdbKYfUUpi4j1SEIcbkd9tndVjcPrufY3V7j2IebOpC00Zp2P/Ay2kA==", + "requires": { + "ethereum-common": "^0.0.18", + "ethereumjs-util": "^5.0.0" + } + }, + "ethereumjs-util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz", + "integrity": "sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA==", + "requires": { + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "ethjs-util": "^0.1.3", + "keccak": "^1.0.2", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1", + "secp256k1": "^3.0.1" + } + }, + "events": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/events/-/events-2.1.0.tgz", + "integrity": "sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg==" + }, + "hdkey": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/hdkey/-/hdkey-0.8.0.tgz", + "integrity": "sha512-oYsdlK22eobT68N5faWI3776f6tOLyqxLLYwxMx+TP0rkWzuCs0oiOm2VbLWcxdpHFP4LtiRR8udaIX8VkEaZQ==", + "requires": { + "coinstring": "^2.0.0", + "safe-buffer": "^5.1.1", + "secp256k1": "^3.0.1" + } + } + } + }, "eth-lib": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/eth-lib/-/eth-lib-0.1.27.tgz", @@ -8511,13 +8568,12 @@ "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz", "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", "requires": { - "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", "ethereumjs-util": "^5.1.1" }, "dependencies": { "ethereumjs-abi": { "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", "requires": { "bn.js": "^4.10.0", "ethereumjs-util": "^5.0.0" @@ -8556,8 +8612,17 @@ "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz", "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", "requires": { - "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", "ethereumjs-util": "^5.1.1" + }, + "dependencies": { + "ethereumjs-abi": { + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#00ba8463a7f7a67fcad737ff9c2ebd95643427f7", + "requires": { + "bn.js": "^4.10.0", + "ethereumjs-util": "^5.0.0" + } + } } }, "ethereumjs-abi": { @@ -8868,6 +8933,20 @@ } } }, + "ethereumjs-util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz", + "integrity": "sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA==", + "requires": { + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "ethjs-util": "^0.1.3", + "keccak": "^1.0.2", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1", + "secp256k1": "^3.0.1" + } + }, "ethereumjs-vm": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/ethereumjs-vm/-/ethereumjs-vm-2.3.4.tgz", @@ -29504,7 +29583,6 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, "requires": { "is-typedarray": "^1.0.0" } @@ -30494,7 +30572,6 @@ "resolved": "https://registry.npmjs.org/web3/-/web3-0.20.3.tgz", "integrity": "sha1-yqRDc9yIFayHZ73ba6cwc5ZMqos=", "requires": { - "bignumber.js": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934", "crypto-js": "^3.1.4", "utf8": "^2.1.1", "xhr2": "*", @@ -30503,7 +30580,7 @@ "dependencies": { "bignumber.js": { "version": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934", - "from": "git+https://github.com/frozeman/bignumber.js-nolookahead.git" + "from": "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934" } } }, @@ -30901,8 +30978,7 @@ "dev": true, "requires": { "underscore": "1.8.3", - "web3-core-helpers": "1.0.0-beta.34", - "websocket": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2" + "web3-core-helpers": "1.0.0-beta.34" }, "dependencies": { "underscore": { @@ -30913,8 +30989,7 @@ }, "websocket": { "version": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2", - "from": "git://github.com/frozeman/WebSocket-Node.git#browserifyCompatible", - "dev": true, + "from": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2", "requires": { "debug": "^2.2.0", "nan": "^2.3.3", @@ -31503,8 +31578,7 @@ "yaeti": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", - "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=", - "dev": true + "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=" }, "yallist": { "version": "2.1.2", diff --git a/package.json b/package.json index 517f407919cf..1e0215a8dd9a 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "eth-hd-keyring": "^1.2.2", "eth-json-rpc-filters": "^2.1.1", "eth-json-rpc-infura": "^3.0.0", + "eth-ledger-bridge-keyring": "^0.1.0", "eth-method-registry": "^1.0.0", "eth-phishing-detect": "^1.1.4", "eth-query": "^2.1.2", diff --git a/test/e2e/beta/from-import-beta-ui.spec.js b/test/e2e/beta/from-import-beta-ui.spec.js index e14ee2361402..1261b6f9522f 100644 --- a/test/e2e/beta/from-import-beta-ui.spec.js +++ b/test/e2e/beta/from-import-beta-ui.spec.js @@ -366,7 +366,10 @@ describe('Using MetaMask with an existing account', function () { }) it('should open the TREZOR Connect popup', async () => { - const connectButtons = await findElements(driver, By.xpath(`//button[contains(text(), 'Connect to Trezor')]`)) + const trezorButton = await findElements(driver, By.css('.hw-connect__btn')) + await trezorButton[1].click() + await delay(regularDelayMs) + const connectButtons = await findElements(driver, By.xpath(`//button[contains(text(), 'Connect')]`)) await connectButtons[0].click() await delay(regularDelayMs) const allWindows = await driver.getAllWindowHandles() diff --git a/test/unit/app/controllers/metamask-controller-test.js b/test/unit/app/controllers/metamask-controller-test.js index 471321b9b369..1fc604c9c3d6 100644 --- a/test/unit/app/controllers/metamask-controller-test.js +++ b/test/unit/app/controllers/metamask-controller-test.js @@ -7,11 +7,15 @@ const blacklistJSON = require('eth-phishing-detect/src/config') const MetaMaskController = require('../../../../app/scripts/metamask-controller') const firstTimeState = require('../../../unit/localhostState') const createTxMeta = require('../../../lib/createTxMeta') +const EthQuery = require('eth-query') const currentNetworkId = 42 const DEFAULT_LABEL = 'Account 1' +const DEFAULT_LABEL_2 = 'Account 2' const TEST_SEED = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium' const TEST_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc' +const TEST_ADDRESS_2 = '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b' +const TEST_ADDRESS_3 = '0xeb9e64b93097bc15f01f13eae97015c57ab64823' const TEST_SEED_ALT = 'setup olympic issue mobile velvet surge alcohol burger horse view reopen gentle' const TEST_ADDRESS_ALT = '0xc42edfcc21ed14dda456aa0756c153f7985d8813' const CUSTOM_RPC_URL = 'http://localhost:8545' @@ -136,6 +140,9 @@ describe('MetaMaskController', function () { describe('#createNewVaultAndRestore', function () { it('should be able to call newVaultAndRestore despite a mistake.', async function () { const password = 'what-what-what' + sandbox.stub(metamaskController, 'getBalance') + metamaskController.getBalance.callsFake(() => { return Promise.resolve('0x0') }) + await metamaskController.createNewVaultAndRestore(password, TEST_SEED.slice(0, -1)).catch((e) => null) await metamaskController.createNewVaultAndRestore(password, TEST_SEED) @@ -143,6 +150,9 @@ describe('MetaMaskController', function () { }) it('should clear previous identities after vault restoration', async () => { + sandbox.stub(metamaskController, 'getBalance') + metamaskController.getBalance.callsFake(() => { return Promise.resolve('0x0') }) + await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED) assert.deepEqual(metamaskController.getState().identities, { [TEST_ADDRESS]: { address: TEST_ADDRESS, name: DEFAULT_LABEL }, @@ -158,6 +168,54 @@ describe('MetaMaskController', function () { [TEST_ADDRESS_ALT]: { address: TEST_ADDRESS_ALT, name: DEFAULT_LABEL }, }) }) + + it('should restore any consecutive accounts with balances', async () => { + sandbox.stub(metamaskController, 'getBalance') + metamaskController.getBalance.withArgs(TEST_ADDRESS).callsFake(() => { + return Promise.resolve('0x14ced5122ce0a000') + }) + metamaskController.getBalance.withArgs(TEST_ADDRESS_2).callsFake(() => { + return Promise.resolve('0x0') + }) + metamaskController.getBalance.withArgs(TEST_ADDRESS_3).callsFake(() => { + return Promise.resolve('0x14ced5122ce0a000') + }) + + await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED) + assert.deepEqual(metamaskController.getState().identities, { + [TEST_ADDRESS]: { address: TEST_ADDRESS, name: DEFAULT_LABEL }, + [TEST_ADDRESS_2]: { address: TEST_ADDRESS_2, name: DEFAULT_LABEL_2 }, + }) + }) + }) + + describe('#getBalance', () => { + it('should return the balance known by accountTracker', async () => { + const accounts = {} + const balance = '0x14ced5122ce0a000' + accounts[TEST_ADDRESS] = { balance: balance } + + metamaskController.accountTracker.store.putState({ accounts: accounts }) + + const gotten = await metamaskController.getBalance(TEST_ADDRESS) + + assert.equal(balance, gotten) + }) + + it('should ask the network for a balance when not known by accountTracker', async () => { + const accounts = {} + const balance = '0x14ced5122ce0a000' + const ethQuery = new EthQuery() + sinon.stub(ethQuery, 'getBalance').callsFake((account, callback) => { + callback(undefined, balance) + }) + + metamaskController.accountTracker.store.putState({ accounts: accounts }) + + const gotten = await metamaskController.getBalance(TEST_ADDRESS, ethQuery) + + assert.equal(balance, gotten) + }) }) describe('#getApi', function () { @@ -228,9 +286,9 @@ describe('MetaMaskController', function () { it('should throw if it receives an unknown device name', async function () { try { - await metamaskController.connectHardware('Some random device name', 0) + await metamaskController.connectHardware('Some random device name', 0, `m/44/0'/0'`) } catch (e) { - assert.equal(e, 'Error: MetamaskController:connectHardware - Unknown device') + assert.equal(e, 'Error: MetamaskController:getKeyringForDevice - Unknown device') } }) @@ -244,14 +302,24 @@ describe('MetaMaskController', function () { assert.equal(keyrings.length, 1) }) + it('should add the Ledger Hardware keyring', async function () { + sinon.spy(metamaskController.keyringController, 'addNewKeyring') + await metamaskController.connectHardware('ledger', 0).catch((e) => null) + const keyrings = await metamaskController.keyringController.getKeyringsByType( + 'Ledger Hardware' + ) + assert.equal(metamaskController.keyringController.addNewKeyring.getCall(0).args, 'Ledger Hardware') + assert.equal(keyrings.length, 1) + }) + }) describe('checkHardwareStatus', function () { it('should throw if it receives an unknown device name', async function () { try { - await metamaskController.checkHardwareStatus('Some random device name') + await metamaskController.checkHardwareStatus('Some random device name', `m/44/0'/0'`) } catch (e) { - assert.equal(e, 'Error: MetamaskController:checkHardwareStatus - Unknown device') + assert.equal(e, 'Error: MetamaskController:getKeyringForDevice - Unknown device') } }) @@ -267,7 +335,7 @@ describe('MetaMaskController', function () { try { await metamaskController.forgetDevice('Some random device name') } catch (e) { - assert.equal(e, 'Error: MetamaskController:forgetDevice - Unknown device') + assert.equal(e, 'Error: MetamaskController:getKeyringForDevice - Unknown device') } }) @@ -284,7 +352,7 @@ describe('MetaMaskController', function () { }) }) - describe('unlockTrezorAccount', function () { + describe('unlockHardwareWalletAccount', function () { let accountToUnlock let windowOpenStub let addNewAccountStub @@ -307,16 +375,20 @@ describe('MetaMaskController', function () { sinon.spy(metamaskController.preferencesController, 'setAddresses') sinon.spy(metamaskController.preferencesController, 'setSelectedAddress') sinon.spy(metamaskController.preferencesController, 'setAccountLabel') - await metamaskController.connectHardware('trezor', 0).catch((e) => null) - await metamaskController.unlockTrezorAccount(accountToUnlock).catch((e) => null) + await metamaskController.connectHardware('trezor', 0, `m/44/0'/0'`).catch((e) => null) + await metamaskController.unlockHardwareWalletAccount(accountToUnlock, 'trezor', `m/44/0'/0'`) }) afterEach(function () { - metamaskController.keyringController.addNewAccount.restore() window.open.restore() + metamaskController.keyringController.addNewAccount.restore() + metamaskController.keyringController.getAccounts.restore() + metamaskController.preferencesController.setAddresses.restore() + metamaskController.preferencesController.setSelectedAddress.restore() + metamaskController.preferencesController.setAccountLabel.restore() }) - it('should set accountToUnlock in the keyring', async function () { + it('should set unlockedAccount in the keyring', async function () { const keyrings = await metamaskController.keyringController.getKeyringsByType( 'Trezor Hardware' ) @@ -324,7 +396,7 @@ describe('MetaMaskController', function () { }) - it('should call keyringController.addNewAccount', async function () { + it('should call keyringController.addNewAccount', async function () { assert(metamaskController.keyringController.addNewAccount.calledOnce) }) @@ -553,6 +625,8 @@ describe('MetaMaskController', function () { const data = '0x43727970746f6b697474696573' beforeEach(async () => { + sandbox.stub(metamaskController, 'getBalance') + metamaskController.getBalance.callsFake(() => { return Promise.resolve('0x0') }) await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED_ALT) @@ -622,6 +696,8 @@ describe('MetaMaskController', function () { const data = '0x43727970746f6b697474696573' beforeEach(async function () { + sandbox.stub(metamaskController, 'getBalance') + metamaskController.getBalance.callsFake(() => { return Promise.resolve('0x0') }) await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED_ALT) diff --git a/test/unit/app/controllers/network-contoller-test.js b/test/unit/app/controllers/network-contoller-test.js index 822311931e67..2721898bdb01 100644 --- a/test/unit/app/controllers/network-contoller-test.js +++ b/test/unit/app/controllers/network-contoller-test.js @@ -47,7 +47,7 @@ describe('# Network Controller', function () { describe('#setNetworkState', function () { it('should update the network', function () { - networkController.setNetworkState(1) + networkController.setNetworkState(1, 'rpc') const networkState = networkController.getNetworkState() assert.equal(networkState, 1, 'network is 1') }) @@ -80,6 +80,9 @@ describe('Network utils', () => { }, { input: 42, expected: 'Kovan', + }, { + input: 61, + expected: 'Ethereum Classic', }, { input: 'ropsten', expected: 'Ropsten', @@ -89,6 +92,9 @@ describe('Network utils', () => { }, { input: 'kovan', expected: 'Kovan', + }, { + input: 'classic', + expected: 'Ethereum Classic', }, { input: 'mainnet', expected: 'Main Ethereum Network', diff --git a/test/unit/components/balance-component-test.js b/test/unit/components/balance-component-test.js index 81e6fdf9eb37..0b3fe7c61dd7 100644 --- a/test/unit/components/balance-component-test.js +++ b/test/unit/components/balance-component-test.js @@ -8,6 +8,7 @@ const mockState = { accounts: { abc: {} }, network: 1, selectedAddress: 'abc', + ticker: 'ETH', }, } diff --git a/ui/app/actions.js b/ui/app/actions.js index bd5d2532749c..638395bc8a91 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -91,7 +91,7 @@ var actions = { connectHardware, checkHardwareStatus, forgetDevice, - unlockTrezorAccount, + unlockHardwareWalletAccount, NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN', navigateToNewAccountScreen, resetAccount, @@ -235,6 +235,8 @@ var actions = { UPDATE_TOKENS: 'UPDATE_TOKENS', setRpcTarget: setRpcTarget, setProviderType: setProviderType, + SET_HARDWARE_WALLET_DEFAULT_HD_PATH: 'SET_HARDWARE_WALLET_DEFAULT_HD_PATH', + setHardwareWalletDefaultHdPath, updateProviderType, // loading overlay SHOW_LOADING: 'SHOW_LOADING_INDICATION', @@ -639,12 +641,12 @@ function addNewAccount () { } } -function checkHardwareStatus (deviceName) { - log.debug(`background.checkHardwareStatus`, deviceName) +function checkHardwareStatus (deviceName, hdPath) { + log.debug(`background.checkHardwareStatus`, deviceName, hdPath) return (dispatch, getState) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { - background.checkHardwareStatus(deviceName, (err, unlocked) => { + background.checkHardwareStatus(deviceName, hdPath, (err, unlocked) => { if (err) { log.error(err) dispatch(actions.displayWarning(err.message)) @@ -681,12 +683,12 @@ function forgetDevice (deviceName) { } } -function connectHardware (deviceName, page) { - log.debug(`background.connectHardware`, deviceName, page) +function connectHardware (deviceName, page, hdPath) { + log.debug(`background.connectHardware`, deviceName, page, hdPath) return (dispatch, getState) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { - background.connectHardware(deviceName, page, (err, accounts) => { + background.connectHardware(deviceName, page, hdPath, (err, accounts) => { if (err) { log.error(err) dispatch(actions.displayWarning(err.message)) @@ -702,12 +704,12 @@ function connectHardware (deviceName, page) { } } -function unlockTrezorAccount (index) { - log.debug(`background.unlockTrezorAccount`, index) +function unlockHardwareWalletAccount (index, deviceName, hdPath) { + log.debug(`background.unlockHardwareWalletAccount`, index, deviceName, hdPath) return (dispatch, getState) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { - background.unlockTrezorAccount(index, (err, accounts) => { + background.unlockHardwareWalletAccount(index, deviceName, hdPath, (err, accounts) => { if (err) { log.error(err) dispatch(actions.displayWarning(err.message)) @@ -1749,10 +1751,10 @@ function updateProviderType (type) { } } -function setRpcTarget (newRpc) { +function setRpcTarget (newRpc, chainId) { return (dispatch) => { log.debug(`background.setRpcTarget: ${newRpc}`) - background.setCustomRpc(newRpc, (err, result) => { + background.setCustomRpc(newRpc, chainId, (err, result) => { if (err) { log.error(err) return dispatch(self.displayWarning('Had a problem changing networks!')) @@ -1854,6 +1856,13 @@ function showLoadingIndication (message) { } } +function setHardwareWalletDefaultHdPath ({ device, path }) { + return { + type: actions.SET_HARDWARE_WALLET_DEFAULT_HD_PATH, + value: {device, path}, + } +} + function hideLoadingIndication () { return { type: actions.HIDE_LOADING, @@ -2006,11 +2015,17 @@ function coinBaseSubview () { } } -function pairUpdate (coin) { +function pairUpdate (coin, ticker) { + if (!ticker) { + ticker = 'eth' + } else { + ticker = ticker.toLowerCase() + } + return (dispatch) => { dispatch(actions.showSubLoadingIndication()) dispatch(actions.hideWarning()) - shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { + shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_${ticker}`}, (mktResponse) => { dispatch(actions.hideSubLoadingIndication()) if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) dispatch({ @@ -2023,8 +2038,11 @@ function pairUpdate (coin) { } } -function shapeShiftSubview (network) { +function shapeShiftSubview (network, ticker) { var pair = 'btc_eth' + if (ticker) { + pair = `btc_${ticker.toLowerCase()}` + } return (dispatch) => { dispatch(actions.showSubLoadingIndication()) shapeShiftRequest('marketinfo', {pair}, (mktResponse) => { @@ -2079,10 +2097,10 @@ function showQrView (data, message) { }, } } -function reshowQrCode (data, coin) { +function reshowQrCode (data, coin, ticker) { return (dispatch) => { dispatch(actions.showLoadingIndication()) - shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { + shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_${ticker.toLowerCase()}`}, (mktResponse) => { if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) var message = [ diff --git a/ui/app/app.js b/ui/app/app.js index dbb6146d1a60..979d8a25c173 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -39,8 +39,7 @@ const Modal = require('./components/modals/index').Modal // Global Alert const Alert = require('./components/alert') -const AppHeader = require('./components/app-header') - +import AppHeader from './components/app-header' import UnlockPage from './components/pages/unlock-page' // Routes @@ -100,7 +99,7 @@ class App extends Component { network, isMouseUser, provider, - frequentRpcList, + frequentRpcListDetail, currentView, setMouseUserState, } = this.props @@ -140,7 +139,7 @@ class App extends Component { // network dropdown h(NetworkDropdown, { provider, - frequentRpcList, + frequentRpcListDetail, }, []), h(AccountMenu), @@ -229,6 +228,8 @@ class App extends Component { name = this.context.t('connectingToRopsten') } else if (providerName === 'rinkeby') { name = this.context.t('connectingToRinkeby') + } else if (providerName === 'classic') { + name = this.context.t('connectingToClassic') } else { name = this.context.t('connectingToUnknown') } @@ -250,6 +251,8 @@ class App extends Component { name = this.context.t('kovan') } else if (providerName === 'rinkeby') { name = this.context.t('rinkeby') + } else if (providerName === 'classic') { + name = this.context.t('classic') } else { name = this.context.t('unknownNetwork') } @@ -266,7 +269,7 @@ App.propTypes = { alertMessage: PropTypes.string, network: PropTypes.string, provider: PropTypes.object, - frequentRpcList: PropTypes.array, + frequentRpcListDetail: PropTypes.array, currentView: PropTypes.object, sidebarOpen: PropTypes.bool, alertOpen: PropTypes.bool, @@ -358,7 +361,7 @@ function mapStateToProps (state) { forgottenPassword: state.appState.forgottenPassword, nextUnreadNotice, lostAccounts, - frequentRpcList: state.metamask.frequentRpcList || [], + frequentRpcListDetail: state.metamask.frequentRpcListDetail || [], currentCurrency: state.metamask.currentCurrency, isMouseUser: state.appState.isMouseUser, betaUI: state.metamask.featureFlags.betaUI, diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js index 043008a36ef1..05300cd91e20 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/account-dropdowns.js @@ -2,7 +2,7 @@ const Component = require('react').Component const PropTypes = require('prop-types') const h = require('react-hyperscript') const actions = require('../actions') -const genAccountLink = require('etherscan-link').createAccountLink +const genAccountLink = require('../../lib/account-link.js') const connect = require('react-redux').connect const Dropdown = require('./dropdown').Dropdown const DropdownMenuItem = require('./dropdown').DropdownMenuItem @@ -188,7 +188,11 @@ class AccountDropdowns extends Component { closeMenu: () => {}, onClick: () => { const { selected, network } = this.props - const url = genAccountLink(selected, network) + let url + if (this.props.settings && this.props.settings.blockExplorerAddr) { + url = this.props.settings.blockExplorerAddr + } + url = genAccountLink(selected, network, url) global.platform.openWindow({ url }) }, }, @@ -297,6 +301,7 @@ AccountDropdowns.propTypes = { actions: PropTypes.objectOf(PropTypes.func), network: PropTypes.string, style: PropTypes.object, + settings: PropTypes.object, enableAccountOptions: PropTypes.bool, enableAccountsSelector: PropTypes.bool, t: PropTypes.func, @@ -315,10 +320,16 @@ const mapDispatchToProps = (dispatch) => { } } +function mapStateToProps (state) { + return { + settings: state.metamask.settings, + } +} + AccountDropdowns.contextTypes = { t: PropTypes.func, } module.exports = { - AccountDropdowns: connect(null, mapDispatchToProps)(AccountDropdowns), + AccountDropdowns: connect(mapStateToProps, mapDispatchToProps)(AccountDropdowns), } diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js index 9c063d31e6a1..3f1e9c9871a0 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -40,6 +40,7 @@ function mapStateToProps (state) { selectedAddress: state.metamask.selectedAddress, isAccountMenuOpen: state.metamask.isAccountMenuOpen, keyrings: state.metamask.keyrings, + ticker: state.metamask.ticker, identities: state.metamask.identities, accounts: state.metamask.accounts, } @@ -152,6 +153,7 @@ AccountMenu.prototype.renderAccounts = function () { identities, accounts, selectedAddress, + ticker, keyrings, showAccountDetail, } = this.props @@ -163,8 +165,11 @@ AccountMenu.prototype.renderAccounts = function () { const isSelected = identity.address === selectedAddress const balanceValue = accounts[address] ? accounts[address].balance : '' - const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...' const simpleAddress = identity.address.substring(2).toLowerCase() + let formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...' + if (ticker !== 'ETH') { + formattedBalance = formattedBalance.replace(/ETH/, ticker) + } const keyring = keyrings.find((kr) => { return kr.accounts.includes(simpleAddress) || @@ -229,6 +234,7 @@ AccountMenu.prototype.renderKeyringType = function (keyring) { let label switch (type) { case 'Trezor Hardware': + case 'Ledger Hardware': label = this.context.t('hardware') break case 'Simple Key Pair': diff --git a/ui/app/components/app-header/app-header.component.js b/ui/app/components/app-header/app-header.component.js index 07ca6cf84f99..b8b002dcc4e5 100644 --- a/ui/app/components/app-header/app-header.component.js +++ b/ui/app/components/app-header/app-header.component.js @@ -1,4 +1,4 @@ -import React, { Component } from 'react' +import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' import { matchPath } from 'react-router-dom' @@ -11,7 +11,7 @@ const { DEFAULT_ROUTE, INITIALIZE_ROUTE, CONFIRM_TRANSACTION_ROUTE } = require(' const Identicon = require('../identicon') const NetworkIndicator = require('../network') -class AppHeader extends Component { +export default class AppHeader extends PureComponent { static propTypes = { history: PropTypes.object, location: PropTypes.object, @@ -107,20 +107,19 @@ class AppHeader extends Component { onClick={() => history.push(DEFAULT_ROUTE)} > + - - { this.context.t('appName') } - - { this.context.t('beta') } - - - + { const identity = identities[key] const isSelected = identity.address === selected const balanceValue = accounts[key].balance - const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...' const simpleAddress = identity.address.substring(2).toLowerCase() + let formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...' + if (ticker !== 'ETH') { + formattedBalance = formattedBalance.replace(/ETH/, ticker) + } const keyring = keyrings.find((kr) => { return kr.accounts.includes(simpleAddress) || @@ -253,6 +256,11 @@ class AccountDropdowns extends Component { padding: '8px', } + let link + if (this.props.settings && this.props.settings.blockExplorerAddr) { + link = this.props.settings.blockExplorerAddr + } + return h( Dropdown, { @@ -295,7 +303,7 @@ class AccountDropdowns extends Component { closeMenu: () => {}, onClick: () => { const { selected, network } = this.props - const url = genAccountLink(selected, network) + const url = genAccountLink(selected, network, link) global.platform.openWindow({ url }) }, style: Object.assign( @@ -421,6 +429,8 @@ AccountDropdowns.propTypes = { network: PropTypes.number, // actions.showExportPrivateKeyModal: , style: PropTypes.object, + settings: PropTypes.object, + ticker: PropTypes.string, enableAccountsSelector: PropTypes.bool, enableAccountOption: PropTypes.bool, enableAccountOptions: PropTypes.bool, @@ -458,8 +468,10 @@ const mapDispatchToProps = (dispatch) => { function mapStateToProps (state) { return { + ticker: state.metamask.ticker, keyrings: state.metamask.keyrings, sidebarOpen: state.appState.sidebarOpen, + settings: state.metamask.settings, } } diff --git a/ui/app/components/dropdowns/network-dropdown.js b/ui/app/components/dropdowns/network-dropdown.js index e5363ff56a25..df77d1220c5a 100644 --- a/ui/app/components/dropdowns/network-dropdown.js +++ b/ui/app/components/dropdowns/network-dropdown.js @@ -24,8 +24,9 @@ const notToggleElementClassnames = [ function mapStateToProps (state) { return { provider: state.metamask.provider, - frequentRpcList: state.metamask.frequentRpcList || [], + frequentRpcListDetail: state.metamask.frequentRpcListDetail || [], networkDropdownOpen: state.appState.networkDropdownOpen, + network: state.metamask.network, } } @@ -40,8 +41,8 @@ function mapDispatchToProps (dispatch) { setDefaultRpcTarget: type => { dispatch(actions.setDefaultRpcTarget(type)) }, - setRpcTarget: (target) => { - dispatch(actions.setRpcTarget(target)) + setRpcTarget: (target, network) => { + dispatch(actions.setRpcTarget(target, network)) }, showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()), hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()), @@ -68,7 +69,7 @@ module.exports = compose( NetworkDropdown.prototype.render = function () { const props = this.props const { provider: { type: providerType, rpcTarget: activeNetwork } } = props - const rpcList = props.frequentRpcList + const rpcListDetail = props.frequentRpcListDetail const isOpen = this.props.networkDropdownOpen const dropdownMenuItemStyle = { fontSize: '16px', @@ -199,6 +200,28 @@ NetworkDropdown.prototype.render = function () { ] ), + h( + DropdownMenuItem, + { + key: 'classic', + closeMenu: () => this.props.hideNetworkDropdown(), + onClick: () => props.setProviderType('classic'), + style: dropdownMenuItemStyle, + }, + [ + providerType === 'classic' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'), + h(NetworkDropdownIcon, { + backgroundColor: '#228B22', // forest green + isSelected: providerType === 'classic', + }), + h('span.network-name-item', { + style: { + color: providerType === 'classic' ? '#ffffff' : '#9b9b9b', + }, + }, this.context.t('classic')), + ] + ), + h( DropdownMenuItem, { @@ -222,7 +245,7 @@ NetworkDropdown.prototype.render = function () { ), this.renderCustomOption(props.provider), - this.renderCommonRpc(rpcList, props.provider), + this.renderCommonRpc(rpcListDetail, props.provider), h( DropdownMenuItem, @@ -263,6 +286,8 @@ NetworkDropdown.prototype.getNetworkName = function () { name = this.context.t('kovan') } else if (providerName === 'rinkeby') { name = this.context.t('rinkeby') + } else if (providerName === 'classic') { + name = this.context.t('classic') } else { name = this.context.t('unknownNetwork') } @@ -270,20 +295,24 @@ NetworkDropdown.prototype.getNetworkName = function () { return name } -NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) { +NetworkDropdown.prototype.renderCommonRpc = function (rpcListDetail, provider) { const props = this.props - const rpcTarget = provider.rpcTarget + const { rpcTarget, type } = provider + const network = props.network - return rpcList.map((rpc) => { - if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) { + return rpcListDetail.map((entry) => { + const rpc = entry.rpcUrl + const selected = type === 'rpc' && rpcTarget === rpc + if ((rpc === 'http://localhost:8545') || selected) { return null } else { + const chainId = entry.chainId || network return h( DropdownMenuItem, { key: `common${rpc}`, closeMenu: () => this.props.hideNetworkDropdown(), - onClick: () => props.setRpcTarget(rpc), + onClick: () => props.setRpcTarget(rpc, chainId), style: { fontSize: '16px', lineHeight: '20px', @@ -291,11 +320,11 @@ NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) { }, }, [ - rpcTarget === rpc ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'), + selected ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'), h('i.fa.fa-question-circle.fa-med.menu-icon-circle'), h('span.network-name-item', { style: { - color: rpcTarget === rpc ? '#ffffff' : '#9b9b9b', + color: selected ? '#ffffff' : '#9b9b9b', }, }, rpc), ] @@ -307,6 +336,7 @@ NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) { NetworkDropdown.prototype.renderCustomOption = function (provider) { const { rpcTarget, type } = provider const props = this.props + const network = props.network if (type !== 'rpc') return null @@ -320,7 +350,7 @@ NetworkDropdown.prototype.renderCustomOption = function (provider) { DropdownMenuItem, { key: rpcTarget, - onClick: () => props.setRpcTarget(rpcTarget), + onClick: () => props.setRpcTarget(rpcTarget, network), closeMenu: () => this.props.hideNetworkDropdown(), style: { fontSize: '16px', diff --git a/ui/app/components/dropdowns/token-menu-dropdown.js b/ui/app/components/dropdowns/token-menu-dropdown.js index 5a794c7c186e..4ffab823d2ec 100644 --- a/ui/app/components/dropdowns/token-menu-dropdown.js +++ b/ui/app/components/dropdowns/token-menu-dropdown.js @@ -4,7 +4,7 @@ const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect const actions = require('../../actions') -const genAccountLink = require('etherscan-link').createAccountLink +const genAccountLink = require('../../../lib/account-link.js') const copyToClipboard = require('copy-to-clipboard') const { Menu, Item, CloseArea } = require('./components/menu') @@ -17,6 +17,7 @@ module.exports = connect(mapStateToProps, mapDispatchToProps)(TokenMenuDropdown) function mapStateToProps (state) { return { network: state.metamask.network, + settings: state.metamask.settings, } } @@ -67,7 +68,11 @@ TokenMenuDropdown.prototype.render = function () { h(Item, { onClick: (e) => { e.stopPropagation() - const url = genAccountLink(this.props.token.address, this.props.network) + let url + if (this.props.settings && this.props.settings.blockExplorerAddr) { + url = this.props.settings.blockExplorerAddr + } + url = genAccountLink(this.props.token.address, this.props.network, url) global.platform.openWindow({ url }) this.props.onClose() }, diff --git a/ui/app/components/eth-balance.js b/ui/app/components/eth-balance.js index c3d084bdcb22..dcf865535c1a 100644 --- a/ui/app/components/eth-balance.js +++ b/ui/app/components/eth-balance.js @@ -1,5 +1,6 @@ const { Component } = require('react') const h = require('react-hyperscript') +const connect = require('react-redux').connect const { inherits } = require('util') const { formatBalance, @@ -8,7 +9,12 @@ const { const Tooltip = require('./tooltip.js') const FiatValue = require('./fiat-value.js') -module.exports = EthBalanceComponent +module.exports = connect(mapStateToProps)(EthBalanceComponent) +function mapStateToProps (state) { + return { + ticker: state.metamask.ticker, + } +} inherits(EthBalanceComponent, Component) function EthBalanceComponent () { @@ -17,9 +23,12 @@ function EthBalanceComponent () { EthBalanceComponent.prototype.render = function () { const props = this.props - const { value, style, width, needsParse = true } = props + const { ticker, value, style, width, needsParse = true } = props - const formattedValue = value ? formatBalance(value, 6, needsParse) : '...' + let formattedValue = value ? formatBalance(value, 6, needsParse) : '...' + if (ticker !== 'ETH') { + formattedValue = formattedValue.replace(/ETH/, ticker) + } return ( diff --git a/ui/app/components/identicon.js b/ui/app/components/identicon.js index 4240487451ed..bfbeb110942e 100644 --- a/ui/app/components/identicon.js +++ b/ui/app/components/identicon.js @@ -20,15 +20,22 @@ function IdenticonComponent () { function mapStateToProps (state) { return { + ticker: state.metamask.ticker, useBlockie: state.metamask.useBlockie, } } IdenticonComponent.prototype.render = function () { var props = this.props - const { className = '', address } = props + const { className = '', address, ticker } = props var diameter = props.diameter || this.defaultDiameter + // default logo + var logo = './images/eth_logo.svg' + if (ticker && ticker !== 'ETH') { + logo = `./images/${ticker.toLowerCase()}_logo.svg` + } + return address ? ( h('div', { @@ -48,7 +55,7 @@ IdenticonComponent.prototype.render = function () { ) : ( h('img.balance-icon', { - src: './images/eth_logo.svg', + src: logo, style: { height: diameter, width: diameter, diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss index b3e14ce23186..35d38e2a3ad5 100644 --- a/ui/app/components/index.scss +++ b/ui/app/components/index.scss @@ -19,3 +19,5 @@ @import './sender-to-recipient/index'; @import './tabs/index'; + +@import './app-header/index'; diff --git a/ui/app/components/modals/account-details-modal.js b/ui/app/components/modals/account-details-modal.js index 5607cf0512bf..ad3a47009b35 100644 --- a/ui/app/components/modals/account-details-modal.js +++ b/ui/app/components/modals/account-details-modal.js @@ -14,6 +14,8 @@ function mapStateToProps (state) { return { network: state.metamask.network, selectedIdentity: getSelectedIdentity(state), + keyrings: state.metamask.keyrings, + settings: state.metamask.settings, } } @@ -50,9 +52,25 @@ AccountDetailsModal.prototype.render = function () { network, showExportPrivateKeyModal, setAccountLabel, + keyrings, } = this.props const { name, address } = selectedIdentity + const keyring = keyrings.find((kr) => { + return kr.accounts.includes(address) + }) + + let exportPrivateKeyFeatureEnabled = true + // This feature is disabled for hardware wallets + if (keyring.type.search('Hardware') !== -1) { + exportPrivateKeyFeatureEnabled = false + } + + let link + if (this.props.settings && this.props.settings.blockExplorerAddr) { + link = this.props.settings.blockExplorerAddr + } + return h(AccountModalContainer, {}, [ h(EditableLabel, { className: 'account-modal__name', @@ -69,13 +87,13 @@ AccountDetailsModal.prototype.render = function () { h('div.account-modal-divider'), h('button.btn-primary.account-modal__button', { - onClick: () => global.platform.openWindow({ url: genAccountLink(address, network) }), + onClick: () => global.platform.openWindow({ url: genAccountLink(address, network, link) }), }, this.context.t('etherscanView')), // Holding on redesign for Export Private Key functionality - h('button.btn-primary.account-modal__button', { + exportPrivateKeyFeatureEnabled ? h('button.btn-primary.account-modal__button', { onClick: () => showExportPrivateKeyModal(), - }, this.context.t('exportPrivateKey')), + }, this.context.t('exportPrivateKey')) : null, ]) } diff --git a/ui/app/components/modals/buy-options-modal.js b/ui/app/components/modals/buy-options-modal.js index c70510b5fcde..a4b8458faadf 100644 --- a/ui/app/components/modals/buy-options-modal.js +++ b/ui/app/components/modals/buy-options-modal.js @@ -10,6 +10,7 @@ function mapStateToProps (state) { return { network: state.metamask.network, address: state.metamask.selectedAddress, + settings: state.metamask.settings, } } @@ -24,7 +25,7 @@ function mapDispatchToProps (dispatch) { showAccountDetailModal: () => { dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' })) }, - toFaucet: network => dispatch(actions.buyEth({ network })), + toFaucet: (network, address, link) => dispatch(actions.buyEth({ network, address, amount: 0, link })), } } @@ -51,9 +52,15 @@ BuyOptions.prototype.renderModalContentOption = function (title, header, onClick BuyOptions.prototype.render = function () { const { network, toCoinbase, address, toFaucet } = this.props - const isTestNetwork = ['3', '4', '42'].find(n => n === network) + let isTestNetwork = ['3', '4', '42'].find(n => n === network) const networkName = getNetworkDisplayName(network) + let link + if (this.props.settings.isTestNet) { + isTestNetwork = true + link = this.props.settings.buyUrl + } + return h('div', {}, [ h('div.buy-modal-content.transfers-subview', { }, [ @@ -69,7 +76,7 @@ BuyOptions.prototype.render = function () { h('div.buy-modal-content-options.flex-column.flex-center', {}, [ isTestNetwork - ? this.renderModalContentOption(networkName, this.context.t('testFaucet'), () => toFaucet(network)) + ? this.renderModalContentOption(networkName, this.context.t('testFaucet'), () => toFaucet(network, address, link)) : this.renderModalContentOption('Coinbase', this.context.t('depositFiat'), () => toCoinbase(address)), // h('div.buy-modal-content-option', {}, [ diff --git a/ui/app/components/modals/deposit-ether-modal.js b/ui/app/components/modals/deposit-ether-modal.js index 2daa7fa1d75a..d24a326a5a27 100644 --- a/ui/app/components/modals/deposit-ether-modal.js +++ b/ui/app/components/modals/deposit-ether-modal.js @@ -19,6 +19,7 @@ function mapStateToProps (state) { return { network: state.metamask.network, address: state.metamask.selectedAddress, + settings: state.metamask.settings, } } @@ -36,7 +37,7 @@ function mapDispatchToProps (dispatch) { showAccountDetailModal: () => { dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' })) }, - toFaucet: network => dispatch(actions.buyEth({ network })), + toFaucet: (network, address, link) => dispatch(actions.buyEth({ network, address, amount: 0, link })), } } @@ -119,9 +120,25 @@ DepositEtherModal.prototype.renderRow = function ({ DepositEtherModal.prototype.render = function () { const { network, toCoinbase, address, toFaucet } = this.props - const { buyingWithShapeshift } = this.state + let { buyingWithShapeshift } = this.state + let isTestNetwork = ['3', '4', '42'].find(n => n === network) + let noCoinbase = false + let noShapeShift = false + + if (this.props.settings.exchanges) { + if (this.props.settings.exchanges.indexOf('Coinbase') === -1) { + noCoinbase = true + } + if (this.props.settings.exchanges.indexOf('ShapeShift') === -1) { + noShapeShift = true + } + } + let link + if (this.props.settings.isTestNet) { + isTestNetwork = true + link = this.props.settings.buyUrl + } - const isTestNetwork = ['3', '4', '42'].find(n => n === network) const networkName = getNetworkDisplayName(network) return h('div.page-container.page-container--full-width.page-container--full-height', {}, [ @@ -164,7 +181,7 @@ DepositEtherModal.prototype.render = function () { title: FAUCET_ROW_TITLE, text: this.facuetRowText(networkName), buttonLabel: this.context.t('getEther'), - onButtonClick: () => toFaucet(network), + onButtonClick: () => toFaucet(network, address, link), hide: !isTestNetwork || buyingWithShapeshift, }), @@ -179,7 +196,7 @@ DepositEtherModal.prototype.render = function () { text: COINBASE_ROW_TEXT, buttonLabel: this.context.t('continueToCoinbase'), onButtonClick: () => toCoinbase(address), - hide: isTestNetwork || buyingWithShapeshift, + hide: noCoinbase || isTestNetwork || buyingWithShapeshift, }), this.renderRow({ @@ -192,7 +209,7 @@ DepositEtherModal.prototype.render = function () { text: SHAPESHIFT_ROW_TEXT, buttonLabel: this.context.t('shapeshiftBuy'), onButtonClick: () => this.setState({ buyingWithShapeshift: true }), - hide: isTestNetwork, + hide: noShapeShift || isTestNetwork, hideButton: buyingWithShapeshift, hideTitle: buyingWithShapeshift, onBackClick: () => this.setState({ buyingWithShapeshift: false }), diff --git a/ui/app/components/network-display/index.scss b/ui/app/components/network-display/index.scss index 2085cff67cc3..89d46a7b18f5 100644 --- a/ui/app/components/network-display/index.scss +++ b/ui/app/components/network-display/index.scss @@ -23,6 +23,10 @@ &--rinkeby { background-color: lighten($tulip-tree, 35%); } + + &--classic { + background-color: lighten($java, 45%); + } } &__name { @@ -50,5 +54,9 @@ &--rinkeby { background-color: $tulip-tree; } + + &--classic { + background-color: $java; + } } } diff --git a/ui/app/components/network-display/network-display.component.js b/ui/app/components/network-display/network-display.component.js index 38626af20667..64913bdae263 100644 --- a/ui/app/components/network-display/network-display.component.js +++ b/ui/app/components/network-display/network-display.component.js @@ -6,6 +6,7 @@ import { ROPSTEN_CODE, RINKEYBY_CODE, KOVAN_CODE, + CLASSIC_CODE, } from '../../../../app/scripts/controllers/network/enums' const networkToClassHash = { @@ -13,6 +14,7 @@ const networkToClassHash = { [ROPSTEN_CODE]: 'ropsten', [RINKEYBY_CODE]: 'rinkeby', [KOVAN_CODE]: 'kovan', + [CLASSIC_CODE]: 'classic', } export default class NetworkDisplay extends Component { diff --git a/ui/app/components/network.js b/ui/app/components/network.js index 83297c4f2251..832f5f1c8abc 100644 --- a/ui/app/components/network.js +++ b/ui/app/components/network.js @@ -63,6 +63,9 @@ Network.prototype.render = function () { } else if (providerName === 'rinkeby') { hoverText = context.t('rinkeby') iconName = 'rinkeby-test-network' + } else if (providerName === 'classic') { + hoverText = context.t('classic') + iconName = 'ethereum-classic-network' } else { hoverText = context.t('unknownNetwork') iconName = 'unknown-private-network' @@ -76,6 +79,7 @@ Network.prototype.render = function () { 'ropsten-test-network': providerName === 'ropsten' || parseInt(networkNumber) === 3, 'kovan-test-network': providerName === 'kovan', 'rinkeby-test-network': providerName === 'rinkeby', + 'ethereum-classic-network': providerName === 'classic', }), title: hoverText, onClick: (event) => { @@ -122,6 +126,15 @@ Network.prototype.render = function () { h('.network-name', context.t('rinkeby')), h('i.fa.fa-chevron-down.fa-lg.network-caret'), ]) + case 'ethereum-classic-network': + return h('.network-indicator', [ + h(NetworkDropdownIcon, { + backgroundColor: '#228B22', // green + nonSelectBackgroundColor: '#46893D', + }), + h('.network-name', context.t('classic')), + h('i.fa.fa-chevron-down.fa-lg.network-caret'), + ]) default: return h('.network-indicator', [ h('i.fa.fa-question-circle.fa-lg', { diff --git a/ui/app/components/pages/create-account/connect-hardware/account-list.js b/ui/app/components/pages/create-account/connect-hardware/account-list.js index c722d1f551e0..488a189eac8c 100644 --- a/ui/app/components/pages/create-account/connect-hardware/account-list.js +++ b/ui/app/components/pages/create-account/connect-hardware/account-list.js @@ -2,16 +2,75 @@ const { Component } = require('react') const PropTypes = require('prop-types') const h = require('react-hyperscript') const genAccountLink = require('../../../../../lib/account-link.js') +const Select = require('react-select').default class AccountList extends Component { constructor (props, context) { super(props) } + getHdPaths () { + return [ + { + label: `Ledger Live`, + value: `m/44'/60'/0'/0/0`, + }, + { + label: `Legacy (MEW / MyCrypto)`, + value: `m/44'/60'/0'`, + }, + ] + } + + goToNextPage = () => { + // If we have < 5 accounts, it's restricted by BIP-44 + if (this.props.accounts.length === 5) { + this.props.getPage(this.props.device, 1, this.props.selectedPath) + } else { + this.props.onAccountRestriction() + } + } + + goToPreviousPage = () => { + this.props.getPage(this.props.device, -1, this.props.selectedPath) + } + + renderHdPathSelector () { + const { onPathChange, selectedPath } = this.props + + const options = this.getHdPaths() + return h('div', [ + h('h3.hw-connect__hdPath__title', {}, this.context.t('selectHdPath')), + h('p.hw-connect__msg', {}, this.context.t('selectPathHelp')), + h('div.hw-connect__hdPath', [ + h(Select, { + className: 'hw-connect__hdPath__select', + name: 'hd-path-select', + clearable: false, + value: selectedPath, + options, + onChange: (opt) => { + onPathChange(opt.value) + }, + }), + ]), + ]) + } + + capitalizeDevice (device) { + return device.slice(0, 1).toUpperCase() + device.slice(1) + } + renderHeader () { + const { device } = this.props return ( h('div.hw-connect', [ - h('h3.hw-connect__title', {}, this.context.t('selectAnAccount')), + + h('h3.hw-connect__unlock-title', {}, `${this.context.t('unlock')} ${this.capitalizeDevice(device)}`), + + device.toLowerCase() === 'ledger' ? this.renderHdPathSelector() : null, + + h('h3.hw-connect__hdPath__title', {}, this.context.t('selectAnAccount')), h('p.hw-connect__msg', {}, this.context.t('selectAnAccountHelp')), ]) ) @@ -61,7 +120,7 @@ class AccountList extends Component { h( 'button.hw-list-pagination__button', { - onClick: () => this.props.getPage(-1), + onClick: this.goToPreviousPage, }, `< ${this.context.t('prev')}` ), @@ -69,7 +128,7 @@ class AccountList extends Component { h( 'button.hw-list-pagination__button', { - onClick: () => this.props.getPage(1), + onClick: this.goToNextPage, }, `${this.context.t('next')} >` ), @@ -95,7 +154,7 @@ class AccountList extends Component { h( `button.btn-primary.btn--large.new-account-connect-form__button.unlock ${disabled ? '.btn-primary--disabled' : ''}`, { - onClick: this.props.onUnlockAccount.bind(this), + onClick: this.props.onUnlockAccount.bind(this, this.props.device), ...buttonProps, }, [this.context.t('unlock')] @@ -106,7 +165,7 @@ class AccountList extends Component { renderForgetDevice () { return h('div.hw-forget-device-container', {}, [ h('a', { - onClick: this.props.onForgetDevice.bind(this), + onClick: this.props.onForgetDevice.bind(this, this.props.device), }, this.context.t('forgetDevice')), ]) } @@ -125,6 +184,9 @@ class AccountList extends Component { AccountList.propTypes = { + onPathChange: PropTypes.func.isRequired, + selectedPath: PropTypes.string.isRequired, + device: PropTypes.string.isRequired, accounts: PropTypes.array.isRequired, onAccountChange: PropTypes.func.isRequired, onForgetDevice: PropTypes.func.isRequired, @@ -134,6 +196,7 @@ AccountList.propTypes = { history: PropTypes.object, onUnlockAccount: PropTypes.func, onCancel: PropTypes.func, + onAccountRestriction: PropTypes.func, } AccountList.contextTypes = { diff --git a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js index cb2b865958ef..b3dfa4ee2a78 100644 --- a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js +++ b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js @@ -5,6 +5,52 @@ const h = require('react-hyperscript') class ConnectScreen extends Component { constructor (props, context) { super(props) + this.state = { + selectedDevice: null, + } + } + + connect = () => { + if (this.state.selectedDevice) { + this.props.connectToHardwareWallet(this.state.selectedDevice) + } + return null + } + + renderConnectToTrezorButton () { + return h( + `button.hw-connect__btn${this.state.selectedDevice === 'trezor' ? '.selected' : ''}`, + { onClick: _ => this.setState({selectedDevice: 'trezor'}) }, + h('img.hw-connect__btn__img', { + src: 'images/trezor-logo.svg', + }) + ) + } + + renderConnectToLedgerButton () { + return h( + `button.hw-connect__btn${this.state.selectedDevice === 'ledger' ? '.selected' : ''}`, + { onClick: _ => this.setState({selectedDevice: 'ledger'}) }, + h('img.hw-connect__btn__img', { + src: 'images/ledger-logo.svg', + }) + ) + } + + renderButtons () { + return ( + h('div', {}, [ + h('div.hw-connect__btn-wrapper', {}, [ + this.renderConnectToLedgerButton(), + this.renderConnectToTrezorButton(), + ]), + h( + `button.hw-connect__connect-btn${!this.state.selectedDevice ? '.disabled' : ''}`, + { onClick: this.connect }, + this.context.t('connect') + ), + ]) + ) } renderUnsupportedBrowser () { @@ -12,7 +58,7 @@ class ConnectScreen extends Component { h('div.new-account-connect-form.unsupported-browser', {}, [ h('div.hw-connect', [ h('h3.hw-connect__title', {}, this.context.t('browserNotSupported')), - h('p.hw-connect__msg', {}, this.context.t('chromeRequiredForTrezor')), + h('p.hw-connect__msg', {}, this.context.t('chromeRequiredForHardwareWallets')), ]), h( 'button.btn-primary.btn--large', @@ -30,29 +76,31 @@ class ConnectScreen extends Component { renderHeader () { return ( h('div.hw-connect__header', {}, [ - h('h3.hw-connect__header__title', {}, this.context.t(`hardwareSupport`)), - h('p.hw-connect__header__msg', {}, this.context.t(`hardwareSupportMsg`)), + h('h3.hw-connect__header__title', {}, this.context.t(`hardwareWallets`)), + h('p.hw-connect__header__msg', {}, this.context.t(`hardwareWalletsMsg`)), ]) ) } + getAffiliateLinks () { + const links = { + trezor: `Trezor`, + ledger: `Ledger`, + } + + const text = this.context.t('orderOneHere') + const response = text.replace('Trezor', links.trezor).replace('Ledger', links.ledger) + + return h('div.hw-connect__get-hw__msg', { dangerouslySetInnerHTML: {__html: response }}) + } + renderTrezorAffiliateLink () { - return h('div.hw-connect__get-trezor', {}, [ - h('p.hw-connect__get-trezor__msg', {}, this.context.t(`dontHaveATrezorWallet`)), - h('a.hw-connect__get-trezor__link', { - href: 'https://shop.trezor.io/?a=metamask', - target: '_blank', - }, this.context.t('orderOneHere')), + return h('div.hw-connect__get-hw', {}, [ + h('p.hw-connect__get-hw__msg', {}, this.context.t(`dontHaveAHardwareWallet`)), + this.getAffiliateLinks(), ]) } - renderConnectToTrezorButton () { - return h( - 'button.btn-primary.btn--large', - { onClick: this.props.connectToTrezor.bind(this) }, - this.props.btnText - ) - } scrollToTutorial = (e) => { if (this.referenceNode) this.referenceNode.scrollIntoView({behavior: 'smooth'}) @@ -102,7 +150,7 @@ class ConnectScreen extends Component { return ( h('div.hw-connect__footer', {}, [ h('h3.hw-connect__footer__title', {}, this.context.t(`readyToConnect`)), - this.renderConnectToTrezorButton(), + this.renderButtons(), h('p.hw-connect__footer__msg', {}, [ this.context.t(`havingTroubleConnecting`), h('a.hw-connect__footer__link', { @@ -118,8 +166,8 @@ class ConnectScreen extends Component { return ( h('div.new-account-connect-form', {}, [ this.renderHeader(), + this.renderButtons(), this.renderTrezorAffiliateLink(), - this.renderConnectToTrezorButton(), this.renderLearnMore(), this.renderTutorialSteps(), this.renderFooter(), @@ -136,8 +184,7 @@ class ConnectScreen extends Component { } ConnectScreen.propTypes = { - connectToTrezor: PropTypes.func.isRequired, - btnText: PropTypes.string.isRequired, + connectToHardwareWallet: PropTypes.func.isRequired, browserSupported: PropTypes.bool.isRequired, } diff --git a/ui/app/components/pages/create-account/connect-hardware/index.js b/ui/app/components/pages/create-account/connect-hardware/index.js index 3f66e7098641..547df522355c 100644 --- a/ui/app/components/pages/create-account/connect-hardware/index.js +++ b/ui/app/components/pages/create-account/connect-hardware/index.js @@ -7,17 +7,19 @@ const ConnectScreen = require('./connect-screen') const AccountList = require('./account-list') const { DEFAULT_ROUTE } = require('../../../../routes') const { formatBalance } = require('../../../../util') +const { getPlatform } = require('../../../../../../app/scripts/lib/util') +const { PLATFORM_FIREFOX } = require('../../../../../../app/scripts/lib/enums') class ConnectHardwareForm extends Component { constructor (props, context) { super(props) this.state = { error: null, - btnText: context.t('connectToTrezor'), selectedAccount: null, accounts: [], browserSupported: true, unlocked: false, + device: null, } } @@ -38,25 +40,44 @@ class ConnectHardwareForm extends Component { } async checkIfUnlocked () { - const unlocked = await this.props.checkHardwareStatus('trezor') - if (unlocked) { - this.setState({unlocked: true}) - this.getPage(0) - } + ['trezor', 'ledger'].forEach(async device => { + const unlocked = await this.props.checkHardwareStatus(device, this.props.defaultHdPaths[device]) + if (unlocked) { + this.setState({unlocked: true}) + this.getPage(device, 0, this.props.defaultHdPaths[device]) + } + }) } - connectToTrezor = () => { + connectToHardwareWallet = (device) => { + // None of the hardware wallets are supported + // At least for now + if (getPlatform() === PLATFORM_FIREFOX) { + this.setState({ browserSupported: false, error: null}) + return null + } + if (this.state.accounts.length) { return null } - this.setState({ btnText: this.context.t('connecting')}) - this.getPage(0) + + // Default values + this.getPage(device, 0, this.props.defaultHdPaths[device]) + } + + onPathChange = (path) => { + this.props.setHardwareWalletDefaultHdPath({device: this.state.device, path}) + this.getPage(this.state.device, 0, path) } onAccountChange = (account) => { this.setState({selectedAccount: account.toString(), error: null}) } + onAccountRestriction = () => { + this.setState({error: this.context.t('ledgerAccountRestriction') }) + } + showTemporaryAlert () { this.props.showAlert(this.context.t('hardwareWalletConnected')) // Autohide the alert after 5 seconds @@ -65,9 +86,9 @@ class ConnectHardwareForm extends Component { }, 5000) } - getPage = (page) => { + getPage = (device, page, hdPath) => { this.props - .connectHardware('trezor', page) + .connectHardware(device, page, hdPath) .then(accounts => { if (accounts.length) { @@ -77,7 +98,7 @@ class ConnectHardwareForm extends Component { this.showTemporaryAlert() } - const newState = { unlocked: true } + const newState = { unlocked: true, device, error: null } // Default to the first account if (this.state.selectedAccount === null) { accounts.forEach((a, i) => { @@ -104,18 +125,18 @@ class ConnectHardwareForm extends Component { }) .catch(e => { if (e === 'Window blocked') { - this.setState({ browserSupported: false }) + this.setState({ browserSupported: false, error: null}) + } else if (e !== 'Window closed') { + this.setState({ error: e.toString() }) } - this.setState({ btnText: this.context.t('connectToTrezor') }) }) } - onForgetDevice = () => { - this.props.forgetDevice('trezor') + onForgetDevice = (device) => { + this.props.forgetDevice(device) .then(_ => { this.setState({ error: null, - btnText: this.context.t('connectToTrezor'), selectedAccount: null, accounts: [], unlocked: false, @@ -125,13 +146,13 @@ class ConnectHardwareForm extends Component { }) } - onUnlockAccount = () => { + onUnlockAccount = (device) => { if (this.state.selectedAccount === null) { this.setState({ error: this.context.t('accountSelectionRequired') }) } - this.props.unlockTrezorAccount(this.state.selectedAccount) + this.props.unlockHardwareWalletAccount(this.state.selectedAccount, device) .then(_ => { this.props.history.push(DEFAULT_ROUTE) }).catch(e => { @@ -145,20 +166,22 @@ class ConnectHardwareForm extends Component { renderError () { return this.state.error - ? h('span.error', { style: { marginBottom: 40 } }, this.state.error) + ? h('span.error', { style: { margin: '20px 20px 10px', display: 'block', textAlign: 'center' } }, this.state.error) : null } renderContent () { if (!this.state.accounts.length) { return h(ConnectScreen, { - connectToTrezor: this.connectToTrezor, - btnText: this.state.btnText, + connectToHardwareWallet: this.connectToHardwareWallet, browserSupported: this.state.browserSupported, }) } return h(AccountList, { + onPathChange: this.onPathChange, + selectedPath: this.props.defaultHdPaths[this.state.device], + device: this.state.device, accounts: this.state.accounts, selectedAccount: this.state.selectedAccount, onAccountChange: this.onAccountChange, @@ -168,6 +191,7 @@ class ConnectHardwareForm extends Component { onUnlockAccount: this.onUnlockAccount, onForgetDevice: this.onForgetDevice, onCancel: this.onCancel, + onAccountRestriction: this.onAccountRestriction, }) } @@ -188,13 +212,15 @@ ConnectHardwareForm.propTypes = { forgetDevice: PropTypes.func, showAlert: PropTypes.func, hideAlert: PropTypes.func, - unlockTrezorAccount: PropTypes.func, + unlockHardwareWalletAccount: PropTypes.func, + setHardwareWalletDefaultHdPath: PropTypes.func, numberOfExistingAccounts: PropTypes.number, history: PropTypes.object, t: PropTypes.func, network: PropTypes.string, accounts: PropTypes.object, address: PropTypes.string, + defaultHdPaths: PropTypes.object, } const mapStateToProps = state => { @@ -202,28 +228,35 @@ const mapStateToProps = state => { metamask: { network, selectedAddress, identities = {}, accounts = [] }, } = state const numberOfExistingAccounts = Object.keys(identities).length + const { + appState: { defaultHdPaths }, + } = state return { network, accounts, address: selectedAddress, numberOfExistingAccounts, + defaultHdPaths, } } const mapDispatchToProps = dispatch => { return { - connectHardware: (deviceName, page) => { - return dispatch(actions.connectHardware(deviceName, page)) + setHardwareWalletDefaultHdPath: ({device, path}) => { + return dispatch(actions.setHardwareWalletDefaultHdPath({device, path})) + }, + connectHardware: (deviceName, page, hdPath) => { + return dispatch(actions.connectHardware(deviceName, page, hdPath)) }, - checkHardwareStatus: (deviceName) => { - return dispatch(actions.checkHardwareStatus(deviceName)) + checkHardwareStatus: (deviceName, hdPath) => { + return dispatch(actions.checkHardwareStatus(deviceName, hdPath)) }, forgetDevice: (deviceName) => { return dispatch(actions.forgetDevice(deviceName)) }, - unlockTrezorAccount: index => { - return dispatch(actions.unlockTrezorAccount(index)) + unlockHardwareWalletAccount: (index, deviceName, hdPath) => { + return dispatch(actions.unlockHardwareWalletAccount(index, deviceName, hdPath)) }, showImportPage: () => dispatch(actions.showImportPage()), showConnectPage: () => dispatch(actions.showConnectPage()), diff --git a/ui/app/components/pages/home.js b/ui/app/components/pages/home.js index 5e3fdc9af36f..0263924ef231 100644 --- a/ui/app/components/pages/home.js +++ b/ui/app/components/pages/home.js @@ -141,7 +141,7 @@ Home.propTypes = { loadingMessage: PropTypes.string, network: PropTypes.string, provider: PropTypes.object, - frequentRpcList: PropTypes.array, + frequentRpcListDetail: PropTypes.array, currentView: PropTypes.object, sidebarOpen: PropTypes.bool, isMascara: PropTypes.bool, @@ -220,7 +220,7 @@ function mapStateToProps (state) { forgottenPassword: state.appState.forgottenPassword, nextUnreadNotice, lostAccounts, - frequentRpcList: state.metamask.frequentRpcList || [], + frequentRpcListDetail: state.metamask.frequentRpcListDetail || [], currentCurrency: state.metamask.currentCurrency, isMouseUser: state.appState.isMouseUser, isRevealingSeedWords: state.metamask.isRevealingSeedWords, diff --git a/ui/app/components/pages/settings/settings.js b/ui/app/components/pages/settings/settings.js index ff42a13dee27..38d8729ac938 100644 --- a/ui/app/components/pages/settings/settings.js +++ b/ui/app/components/pages/settings/settings.js @@ -173,14 +173,23 @@ class Settings extends Component { onChange: event => this.setState({ newRpc: event.target.value }), onKeyPress: event => { if (event.key === 'Enter') { - this.validateRpc(this.state.newRpc) + this.validateRpc(this.state.newRpc, this.state.chainId) + } + }, + }), + h('input.settings__input', { + placeholder: this.context.t('optionalChainId'), + onChange: event => this.setState({ chainId: event.target.value }), + onKeyPress: event => { + if (event.key === 'Enter') { + this.validateRpc(this.state.newRpc, this.state.chainId) } }, }), h('div.settings__rpc-save-button', { onClick: event => { event.preventDefault() - this.validateRpc(this.state.newRpc) + this.validateRpc(this.state.newRpc, this.state.chainId) }, }, this.context.t('save')), ]), @@ -189,11 +198,11 @@ class Settings extends Component { ) } - validateRpc (newRpc) { + validateRpc (newRpc, chainId) { const { setRpcTarget, displayWarning } = this.props if (validUrl.isWebUri(newRpc)) { - setRpcTarget(newRpc) + setRpcTarget(newRpc, chainId) } else { const appendedRpc = `http://${newRpc}` @@ -341,7 +350,7 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => { return { setCurrentCurrency: currency => dispatch(actions.setCurrentCurrency(currency)), - setRpcTarget: newRpc => dispatch(actions.setRpcTarget(newRpc)), + setRpcTarget: (newRpc, chainId) => dispatch(actions.setRpcTarget(newRpc, chainId)), displayWarning: warning => dispatch(actions.displayWarning(warning)), revealSeedConfirmation: () => dispatch(actions.revealSeedConfirmation()), setUseBlockie: value => dispatch(actions.setUseBlockie(value)), diff --git a/ui/app/components/send/currency-display/currency-display.js b/ui/app/components/send/currency-display/currency-display.js index 2b8eaa41f70f..b0e75cd754f1 100644 --- a/ui/app/components/send/currency-display/currency-display.js +++ b/ui/app/components/send/currency-display/currency-display.js @@ -1,5 +1,6 @@ const Component = require('react').Component const h = require('react-hyperscript') +const connect = require('react-redux').connect const inherits = require('util').inherits const { conversionUtil, multiplyCurrencies } = require('../../../conversion-util') const { removeLeadingZeroes } = require('../send.utils') @@ -12,7 +13,12 @@ CurrencyDisplay.contextTypes = { t: PropTypes.func, } -module.exports = CurrencyDisplay +module.exports = connect(mapStateToProps)(CurrencyDisplay) +function mapStateToProps (state) { + return { + fromCurrency: state.metamask.fromCurrency, + } +} inherits(CurrencyDisplay, Component) function CurrencyDisplay () { @@ -78,7 +84,7 @@ CurrencyDisplay.prototype.getValueToRender = function ({ selectedToken, conversi } CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValue) { - const { primaryCurrency, convertedCurrency, conversionRate } = this.props + const { fromCurrency, primaryCurrency, convertedCurrency, conversionRate } = this.props if (conversionRate === 0 || conversionRate === null || conversionRate === undefined) { if (nonFormattedValue !== 0) { @@ -88,7 +94,7 @@ CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValu let convertedValue = conversionUtil(nonFormattedValue, { fromNumericBase: 'dec', - fromCurrency: primaryCurrency, + fromCurrency: fromCurrency || primaryCurrency, toCurrency: convertedCurrency, numberOfDecimals: 2, conversionRate, @@ -132,6 +138,7 @@ CurrencyDisplay.prototype.render = function () { const { className = 'currency-display', primaryBalanceClassName = 'currency-display__input', + fromCurrency, primaryCurrency, readOnly = false, inError = false, @@ -174,7 +181,7 @@ CurrencyDisplay.prototype.render = function () { step, }), - h('span.currency-display__currency-symbol', primaryCurrency), + h('span.currency-display__currency-symbol', fromCurrency || primaryCurrency), ]), diff --git a/ui/app/components/shapeshift-form.js b/ui/app/components/shapeshift-form.js index 2c4ba40bf576..b9cb92afd8c0 100644 --- a/ui/app/components/shapeshift-form.js +++ b/ui/app/components/shapeshift-form.js @@ -16,19 +16,23 @@ function mapStateToProps (state) { selectedAddress, } = state.metamask const { warning } = state.appState + const ticker = state.metamask.ticker + const provider = state.metamask.provider return { coinOptions, tokenExchangeRates, selectedAddress, + provider, + ticker, warning, } } function mapDispatchToProps (dispatch) { return { - shapeShiftSubview: () => dispatch(shapeShiftSubview()), - pairUpdate: coin => dispatch(pairUpdate(coin)), + shapeShiftSubview: (type, ticker) => dispatch(shapeShiftSubview(type, ticker)), + pairUpdate: (coin, ticker) => dispatch(pairUpdate(coin, ticker)), buyWithShapeShift: data => dispatch(buyWithShapeShift(data)), } } @@ -56,22 +60,27 @@ function ShapeshiftForm () { } ShapeshiftForm.prototype.getCoinPair = function () { - return `${this.state.depositCoin.toUpperCase()}_ETH` + const ticker = this.props.ticker + return `${this.state.depositCoin.toUpperCase()}_${ticker}` } ShapeshiftForm.prototype.componentWillMount = function () { - this.props.shapeShiftSubview() + const ticker = this.props.ticker + const type = this.props.provider.type + this.props.shapeShiftSubview(type, ticker) } ShapeshiftForm.prototype.onCoinChange = function (coin) { + const ticker = this.props.ticker this.setState({ depositCoin: coin, errorMessage: '', }) - this.props.pairUpdate(coin) + this.props.pairUpdate(coin, ticker) } ShapeshiftForm.prototype.onBuyWithShapeShift = function () { + const ticker = this.props.ticker this.setState({ isLoading: true, showQrCode: true, @@ -85,7 +94,7 @@ ShapeshiftForm.prototype.onBuyWithShapeShift = function () { refundAddress: returnAddress, depositCoin, } = this.state - const pair = `${depositCoin}_eth` + const pair = `${depositCoin}_${ticker.toLowerCase()}` const data = { withdrawal, pair, @@ -175,7 +184,7 @@ ShapeshiftForm.prototype.renderQrCode = function () { ShapeshiftForm.prototype.render = function () { const { coinOptions, btnClass, warning } = this.props const { errorMessage, showQrCode, depositAddress } = this.state - const { tokenExchangeRates } = this.props + const { tokenExchangeRates, ticker } = this.props const token = tokenExchangeRates[this.getCoinPair()] return h('div.shapeshift-form-wrapper', [ @@ -209,7 +218,7 @@ ShapeshiftForm.prototype.render = function () { this.context.t('receive'), ]), - h('div.shapeshift-form__selector-input', ['ETH']), + h('div.shapeshift-form__selector-input', [ticker]), ]), diff --git a/ui/app/components/shift-list-item.js b/ui/app/components/shift-list-item.js index 4334aacba56a..5071e6395c7b 100644 --- a/ui/app/components/shift-list-item.js +++ b/ui/app/components/shift-list-item.js @@ -4,7 +4,7 @@ const PropTypes = require('prop-types') const h = require('react-hyperscript') const connect = require('react-redux').connect const vreme = new (require('vreme'))() -const explorerLink = require('etherscan-link').createExplorerLink +const explorerLink = require('../../lib/explorer-link.js') const actions = require('../actions') const addressSummary = require('../util').addressSummary @@ -25,6 +25,7 @@ function mapStateToProps (state) { selectedAddress: state.metamask.selectedAddress, conversionRate: state.metamask.conversionRate, currentCurrency: state.metamask.currentCurrency, + ticker: state.metamask.ticker, } } @@ -84,7 +85,7 @@ ShiftListItem.prototype.renderUtilComponents = function () { title: this.context.t('qrCode'), }, [ h('i.fa.fa-qrcode.pointer.pop-hover', { - onClick: () => props.dispatch(actions.reshowQrCode(props.depositAddress, props.depositType)), + onClick: () => props.dispatch(actions.reshowQrCode(props.depositAddress, props.depositType, props.ticker)), style: { margin: '5px', marginLeft: '23px', diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js index 4100d76a59eb..af11f35cbbc1 100644 --- a/ui/app/components/token-cell.js +++ b/ui/app/components/token-cell.js @@ -19,6 +19,7 @@ function mapStateToProps (state) { contractExchangeRates: state.metamask.contractExchangeRates, conversionRate: state.metamask.conversionRate, sidebarOpen: state.appState.sidebarOpen, + settings: state.metamask.settings, } } @@ -143,7 +144,11 @@ TokenCell.prototype.send = function (address, event) { } TokenCell.prototype.view = function (address, userAddress, network, event) { - const url = etherscanLinkFor(address, userAddress, network) + let url + if (this.props.settings && this.props.settings.blockExplorerToken) { + url = this.props.settings.blockExplorerToken + } + url = etherscanLinkFor(address, userAddress, network, url) if (url) { navigateTo(url) } @@ -153,7 +158,11 @@ function navigateTo (url) { global.platform.openWindow({ url }) } -function etherscanLinkFor (tokenAddress, address, network) { +function etherscanLinkFor (tokenAddress, address, network, url) { + if (url) { + return url.replace('[[tokenAddress]]', tokenAddress).replace('[[address]]', address) + } + const prefix = prefixForNetwork(network) return `https://${prefix}etherscan.io/token/${tokenAddress}?a=${address}` } diff --git a/ui/app/components/tx-list-item.js b/ui/app/components/tx-list-item.js index 474d62638053..46eb9ef889f3 100644 --- a/ui/app/components/tx-list-item.js +++ b/ui/app/components/tx-list-item.js @@ -32,6 +32,7 @@ module.exports = compose( function mapStateToProps (state) { return { tokens: state.metamask.tokens, + ticker: state.metamask.ticker, currentCurrency: getCurrentCurrency(state), contractExchangeRates: state.metamask.contractExchangeRates, selectedAddressTxList: state.metamask.selectedAddressTxList, @@ -110,6 +111,7 @@ TxListItem.prototype.getSendEtherTotal = function () { const { transactionAmount, conversionRate, + ticker, address, currentCurrency, } = this.props @@ -121,7 +123,7 @@ TxListItem.prototype.getSendEtherTotal = function () { const totalInFiat = conversionUtil(transactionAmount, { fromNumericBase: 'hex', toNumericBase: 'dec', - fromCurrency: 'ETH', + fromCurrency: ticker, toCurrency: currentCurrency, fromDenomination: 'WEI', numberOfDecimals: 2, @@ -130,15 +132,15 @@ TxListItem.prototype.getSendEtherTotal = function () { const totalInETH = conversionUtil(transactionAmount, { fromNumericBase: 'hex', toNumericBase: 'dec', - fromCurrency: 'ETH', - toCurrency: 'ETH', + fromCurrency: ticker, + toCurrency: ticker, fromDenomination: 'WEI', conversionRate, numberOfDecimals: 6, }) return { - total: `${totalInETH} ETH`, + total: `${totalInETH} ${ticker}`, fiatTotal: `${totalInFiat} ${currentCurrency.toUpperCase()}`, } } diff --git a/ui/app/components/tx-list.js b/ui/app/components/tx-list.js index d8c4a9d19793..51c302641a2a 100644 --- a/ui/app/components/tx-list.js +++ b/ui/app/components/tx-list.js @@ -26,6 +26,7 @@ TxList.contextTypes = { function mapStateToProps (state) { return { + settings: state.metamask.settings, txsToRender: selectors.transactionsSelector(state), conversionRate: selectors.conversionRateSelector(state), selectedAddress: selectors.getSelectedAddress(state), @@ -155,7 +156,11 @@ TxList.prototype.renderTransactionListItem = function (transaction, conversionRa } TxList.prototype.view = function (txHash, network) { - const url = etherscanLinkFor(txHash, network) + let url + if (this.props.settings && this.props.settings.blockExplorerTx) { + url = this.props.settings.blockExplorerTx + } + url = etherscanLinkFor(txHash, network, url) if (url) { navigateTo(url) } @@ -165,7 +170,10 @@ function navigateTo (url) { global.platform.openWindow({ url }) } -function etherscanLinkFor (txHash, network) { +function etherscanLinkFor (txHash, network, url) { const prefix = prefixForNetwork(network) + if (url) { + return url.replace('[[txHash]]', txHash) + } return `https://${prefix}etherscan.io/tx/${txHash}` } diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index 20c2be0f1f26..8e092364c4a7 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -118,8 +118,18 @@ WalletView.prototype.render = function () { return kr.accounts.includes(selectedAddress) }) - const type = keyring.type - const isLoose = type !== 'HD Key Tree' + let label = '' + let type + if (keyring) { + type = keyring.type + if (type !== 'HD Key Tree') { + if (type.toLowerCase().search('hardware') !== -1) { + label = this.context.t('hardware') + } else { + label = this.context.t('imported') + } + } + } return h('div.wallet-view.flex-column' + (responsiveDisplayClassname || ''), { style: {}, @@ -133,7 +143,7 @@ WalletView.prototype.render = function () { onClick: hideSidebar, }), - h('div.wallet-view__keyring-label.allcaps', isLoose ? this.context.t('imported') : ''), + h('div.wallet-view__keyring-label.allcaps', label), h('div.flex-column.flex-center.wallet-view__name-container', { style: { margin: '0 auto' }, diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss index 96ad5fe64339..821a6b6120ed 100644 --- a/ui/app/css/itcss/components/index.scss +++ b/ui/app/css/itcss/components/index.scss @@ -1,7 +1,5 @@ @import './buttons.scss'; -@import './header.scss'; - @import './footer.scss'; @import './network.scss'; diff --git a/ui/app/css/itcss/components/new-account.scss b/ui/app/css/itcss/components/new-account.scss index b12afb124764..e4c7a4e0d518 100644 --- a/ui/app/css/itcss/components/new-account.scss +++ b/ui/app/css/itcss/components/new-account.scss @@ -162,19 +162,99 @@ } .hw-connect { + width: 100%; + &__header { &__title { margin-top: 5px; margin-bottom: 15px; font-size: 22px; - text-align: center; } &__msg { font-size: 14px; color: #9b9b9b; margin-top: 10px; - margin-bottom: 0px; + margin-bottom: 20px; + } + } + + &__btn-wrapper { + flex: 1; + flex-direction: row; + display: flex; + } + + &__connect-btn { + background-color: #259De5; + color: #fff; + border: none; + width: 315px; + min-height: 54px; + font-weight: 300; + font-size: 14px; + margin-bottom: 20px; + margin-top: 20px; + border-radius: 5px; + display: flex; + flex: 1; + margin-left: 20px; + margin-right: 20px; + justify-content: center; + text-transform: uppercase; + } + + &__connect-btn.disabled { + cursor: not-allowed; + opacity: .5; + } + + &__btn { + background: #fbfbfb; + border: 1px solid #e5e5e5; + height: 100px; + width: 150px; + flex: 1; + display: flex; + align-items: center; + justify-content: center; + border-radius: 5px; + + &__img { + width: 95px; + } + } + + &__btn.selected { + border: 2px solid #00a8ee; + width: 149px; + } + + &__btn:first-child { + margin-right: 15px; + margin-left: 20px; + } + + &__btn:last-child { + margin-right: 20px; + } + + &__hdPath { + display: flex; + flex-direction: row; + margin-top: 15px; + margin-bottom: 30px; + font-size: 14px; + + &__title { + display: flex; + margin-top: 10px; + margin-right: 15px; + } + + &__select { + display: flex; + flex: 1; } } @@ -201,6 +281,13 @@ font-size: 18px; } + &__unlock-title { + padding-top: 10px; + font-weight: 400; + font-size: 22px; + margin-bottom: 15px; + } + &__msg { font-size: 14px; color: #9b9b9b; @@ -213,8 +300,6 @@ } &__footer { - width: 100%; - &__title { padding-top: 15px; padding-bottom: 12px; @@ -228,6 +313,9 @@ color: #9b9b9b; margin-top: 12px; margin-bottom: 27px; + width: 100%; + display: block; + margin-left: 20px; } &__link { @@ -236,10 +324,10 @@ } } - &__get-trezor { + &__get-hw { width: 100%; - padding-bottom: 20px; - padding-top: 20px; + padding-bottom: 10px; + padding-top: 10px; &__msg { font-size: 14px; @@ -390,6 +478,8 @@ &.account-list { height: auto; + padding-left: 20px; + padding-right: 20px; } &__buttons { @@ -412,6 +502,7 @@ min-height: 54px; font-weight: 300; font-size: 14px; + margin-bottom: 20px } &__button.unlock { diff --git a/ui/app/css/itcss/components/settings.scss b/ui/app/css/itcss/components/settings.scss index 0dd61ac5eb41..7a94c1cb648a 100644 --- a/ui/app/css/itcss/components/settings.scss +++ b/ui/app/css/itcss/components/settings.scss @@ -48,7 +48,6 @@ display: flex; flex-direction: column; padding: 0 5px; - height: 71px; @media screen and (max-width: 575px) { height: initial; @@ -78,10 +77,12 @@ } .settings__input { - padding-left: 10px; + padding-left: 15px; font-size: 14px; - height: 40px; + height: 56px; border: 1px solid $alto; + margin-bottom: 3px; + border-radius: 2px; } .settings__input::-webkit-input-placeholder { diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index 98d467163b84..c246e7904cc2 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -67,6 +67,10 @@ function reduceApp (state, action) { isMouseUser: false, gasIsLoading: false, networkNonce: null, + defaultHdPaths: { + trezor: `m/44'/60'/0'/0`, + ledger: `m/44'/60'/0'/0/0`, + }, }, state.appState) switch (action.type) { @@ -525,6 +529,15 @@ function reduceApp (state, action) { warning: '', }) + case actions.SET_HARDWARE_WALLET_DEFAULT_HD_PATH: + const { device, path } = action.value + const newDefaults = {...appState.defaultHdPaths} + newDefaults[device] = path + + return extend(appState, { + defaultHdPaths: newDefaults, + }) + case actions.SHOW_LOADING: return extend(appState, { isLoading: true, diff --git a/ui/app/selectors/confirm-transaction.js b/ui/app/selectors/confirm-transaction.js index aa1fc5404a62..6e760c429a82 100644 --- a/ui/app/selectors/confirm-transaction.js +++ b/ui/app/selectors/confirm-transaction.js @@ -159,7 +159,7 @@ export const approveTokenAmountAndToAddressSelector = createSelector( if (tokenDecimals) { tokenAmount = calcTokenAmount(value, tokenDecimals) } - + tokenAmount = roundExponential(tokenAmount) } diff --git a/ui/lib/account-link.js b/ui/lib/account-link.js index 037d990fa223..ad6d38cfbd7d 100644 --- a/ui/lib/account-link.js +++ b/ui/lib/account-link.js @@ -1,4 +1,4 @@ -module.exports = function (address, network) { +module.exports = function (address, network, url) { const net = parseInt(network) let link switch (net) { @@ -18,6 +18,10 @@ module.exports = function (address, network) { link = `https://kovan.etherscan.io/address/${address}` break default: + if (url) { + return url.replace('[[address]]', address) + } + link = '' break } diff --git a/ui/lib/explorer-link.js b/ui/lib/explorer-link.js new file mode 100644 index 000000000000..ce38a02f3bfd --- /dev/null +++ b/ui/lib/explorer-link.js @@ -0,0 +1,10 @@ +const prefixForNetwork = require('./etherscan-prefix-for-network') + +module.exports = function (hash, network, url) { + if (url) { + return url.replace('[[txHash]]', hash) + } + + const prefix = prefixForNetwork(network) + return `https://${prefix}etherscan.io/tx/${hash}` +}