diff options
author | Dan Finlay <542863+danfinlay@users.noreply.github.com> | 2019-05-15 04:22:54 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-05-15 04:22:54 +0800 |
commit | bf5b7f8e77684ce4a66d7e0301ed19d048a545f9 (patch) | |
tree | 9c68af7d6cb4b10410d5deeb33b9c9b61996cd02 | |
parent | 7dcc707a4329d69e9d4d7c6befbfd228ed7d714b (diff) | |
parent | 985f51a764560311e8aa5bb149e34b3c25b78c92 (diff) | |
download | tangerine-wallet-browser-bf5b7f8e77684ce4a66d7e0301ed19d048a545f9.tar.gz tangerine-wallet-browser-bf5b7f8e77684ce4a66d7e0301ed19d048a545f9.tar.zst tangerine-wallet-browser-bf5b7f8e77684ce4a66d7e0301ed19d048a545f9.zip |
Merge pull request #6599 from MetaMask/develop
Master Version Bump (v6.5.0)
154 files changed, 3403 insertions, 1337 deletions
@@ -133,7 +133,7 @@ "no-unneeded-ternary": [2, { "defaultAssignment": false }], "no-unreachable": 2, "no-unsafe-finally": 2, - "no-unused-vars": [2, { "vars": "all", "args": "none" }], + "no-unused-vars": [2, { "vars": "all", "args": "all", "argsIgnorePattern": "[_]+" }], "no-useless-call": 2, "no-useless-computed-key": 2, "no-useless-constructor": 2, diff --git a/CHANGELOG.md b/CHANGELOG.md index d62a32a25..d3e333e23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,29 @@ ## Current Develop Branch +## 6.5.1 Tue May 14 2019 + +- Fix bug where approve method would show a warning. #6602 +- [#6593](https://github.com/MetaMask/metamask-extension/pull/6593): Fix wording of autoLogoutTimeLimitDescription + +## 6.5.0 Fri May 10 2019 + +- [#6568](https://github.com/MetaMask/metamask-extension/pull/6568): feature: integrate gaba/PhishingController +- [#6490](https://github.com/MetaMask/metamask-extension/pull/6490): Redesign custom RPC form +- [#6558](https://github.com/MetaMask/metamask-extension/pull/6558): Adds auto logout with customizable time frame +- [#6578](https://github.com/MetaMask/metamask-extension/pull/6578): Fixes ability to send to token contract addresses +- [#6557](https://github.com/MetaMask/metamask-extension/pull/6557): Adds drag and drop functionality to seed phrase entry. +- [#6526](https://github.com/MetaMask/metamask-extension/pull/6526): Include token checksum address in prices lookup for token rates +- [#6502](https://github.com/MetaMask/metamask-extension/pull/6502): Add subheader to all settings subviews +- [#6501](https://github.com/MetaMask/metamask-extension/pull/6501): Improve confirm screen loading performance by fixing home screen rendering bug + ## 6.4.1 Fri Apr 26 2019 - [#6521](https://github.com/MetaMask/metamask-extension/pull/6521): Revert "Adds 4byte registry fallback to getMethodData()" to fix stalling bug. ## 6.4.0 Wed Apr 17 2019 -- [#6445](https://github.com/MetaMask/metamask-extension/pull/6445): * Move send to pages/
+- [#6445](https://github.com/MetaMask/metamask-extension/pull/6445): * Move send to pages/ - [#6470](https://github.com/MetaMask/metamask-extension/pull/6470): update publishing.md with dev diagram - [#6403](https://github.com/MetaMask/metamask-extension/pull/6403): Update to eth-method-registry@1.2.0 - [#6468](https://github.com/MetaMask/metamask-extension/pull/6468): Fix switcher height when Custom RPC is selected or loading diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 184507cbb..bef278f79 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -83,6 +83,9 @@ "address": { "message": "Address" }, + "addNetwork": { + "message": "Add Network" + }, "advanced": { "message": "Advanced" }, @@ -154,6 +157,12 @@ "attributions": { "message": "Attributions" }, + "autoLogoutTimeLimit": { + "message": "Auto-Logout Timer (minutes)" + }, + "autoLogoutTimeLimitDescription": { + "message": "Set the idle time in minutes before MetaMask will automatically log out" + }, "available": { "message": "Available" }, @@ -185,6 +194,13 @@ "message": "must be greater than or equal to $1 and less than or equal to $2.", "description": "helper for inputting hex as decimal input" }, + "blockExplorerUrl": { + "message": "Block Explorer" + }, + "blockExplorerView": { + "message": "View account at $1", + "description": "$1 replaced by URL for custom block explorer" + }, "blockiesIdenticon": { "message": "Use Blockies Identicon" }, @@ -224,6 +240,9 @@ "ok": { "message": "Ok" }, + "optionalBlockExplorerUrl": { + "message": "Block Explorer URL (optional)" + }, "cancel": { "message": "Cancel" }, @@ -239,6 +258,9 @@ "cancelN": { "message": "Cancel all $1 transactions" }, + "chainId": { + "message": "Chain ID" + }, "classicInterface": { "message": "Use classic interface" }, @@ -496,6 +518,9 @@ "edit": { "message": "Edit" }, + "editNetwork": { + "message": "Edit Network" + }, "editAccountName": { "message": "Edit Account Name" }, @@ -928,9 +953,15 @@ "negativeETH": { "message": "Can not send negative amounts of ETH." }, + "networkName": { + "message": "Network Name" + }, "networks": { "message": "Networks" }, + "networkSettingsDescription": { + "message": "Add and edit custom RPC networks" + }, "nevermind": { "message": "Nevermind" }, @@ -971,7 +1002,7 @@ "protectYourKeysMessage2": { "message": "Keep your phrase safe. If you see something fishy, or you’re uncertain about a website, email support@metamask.io" }, - "rpcURL": { + "rpcUrl": { "message": "New RPC URL" }, "showAdvancedOptions": { @@ -1486,6 +1517,9 @@ "supportCenter": { "message": "Visit our Support Center" }, + "symbol": { + "message": "Symbol" + }, "symbolBetweenZeroTwelve": { "message": "Symbol must be between 0 and 12 characters." }, @@ -1708,9 +1742,15 @@ "viewAccount": { "message": "View Account" }, + "viewOnCustomBlockExplorer": { + "message": "View at $1" + }, "viewOnEtherscan": { "message": "View on Etherscan" }, + "viewNetworkInfo": { + "message": "View Network Info" + }, "visitWebSite": { "message": "Visit our web site" }, diff --git a/app/manifest.json b/app/manifest.json index bd10f60da..570e5b6cb 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "__MSG_appName__", "short_name": "__MSG_appName__", - "version": "6.4.1", + "version": "6.5.1", "manifest_version": 2, "author": "https://metamask.io", "description": "__MSG_appDescription__", diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 2325cecdd..0c55ae39f 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -1,18 +1,17 @@ const fs = require('fs') const path = require('path') const pump = require('pump') +const log = require('loglevel') +const Dnode = require('dnode') const querystring = require('querystring') const LocalMessageDuplexStream = require('post-message-stream') -const PongStream = require('ping-pong-stream/pong') const ObjectMultiplex = require('obj-multiplex') const extension = require('extensionizer') const PortStream = require('extension-port-stream') -const {Transform: TransformStream} = require('stream') const inpageContent = fs.readFileSync(path.join(__dirname, '..', '..', 'dist', 'chrome', 'inpage.js')).toString() const inpageSuffix = '//# sourceURL=' + extension.extension.getURL('inpage.js') + '\n' const inpageBundle = inpageContent + inpageSuffix -let isEnabled = false // Eventually this streaming injection could be replaced with: // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Language_Bindings/Components.utils.exportFunction @@ -23,9 +22,7 @@ let isEnabled = false if (shouldInjectWeb3()) { injectScript(inpageBundle) - setupStreams() - listenForProviderRequest() - checkPrivacyMode() + start() } /** @@ -47,148 +44,107 @@ function injectScript (content) { } /** + * Sets up the stream communication and submits site metadata + * + */ +async function start () { + await setupStreams() + await domIsReady() +} + +/** * Sets up two-way communication streams between the - * browser extension and local per-page browser context + * browser extension and local per-page browser context. + * */ -function setupStreams () { - // setup communication to page and plugin +async function setupStreams () { + // the transport-specific streams for communication between inpage and background const pageStream = new LocalMessageDuplexStream({ name: 'contentscript', target: 'inpage', }) - const pluginPort = extension.runtime.connect({ name: 'contentscript' }) - const pluginStream = new PortStream(pluginPort) + const extensionPort = extension.runtime.connect({ name: 'contentscript' }) + const extensionStream = new PortStream(extensionPort) - // Filter out selectedAddress until this origin is enabled - const approvalTransform = new TransformStream({ - objectMode: true, - transform: (data, _, done) => { - if (typeof data === 'object' && data.name && data.name === 'publicConfig' && !isEnabled) { - data.data.selectedAddress = undefined - } - done(null, { ...data }) - }, - }) + // create and connect channel muxers + // so we can handle the channels individually + const pageMux = new ObjectMultiplex() + pageMux.setMaxListeners(25) + const extensionMux = new ObjectMultiplex() + extensionMux.setMaxListeners(25) - // forward communication plugin->inpage pump( + pageMux, pageStream, - pluginStream, - approvalTransform, - pageStream, - (err) => logStreamDisconnectWarning('MetaMask Contentscript Forwarding', err) + pageMux, + (err) => logStreamDisconnectWarning('MetaMask Inpage Multiplex', err) ) - - // setup local multistream channels - const mux = new ObjectMultiplex() - mux.setMaxListeners(25) - pump( - mux, - pageStream, - mux, - (err) => logStreamDisconnectWarning('MetaMask Inpage', err) - ) - pump( - mux, - pluginStream, - mux, - (err) => logStreamDisconnectWarning('MetaMask Background', err) + extensionMux, + extensionStream, + extensionMux, + (err) => logStreamDisconnectWarning('MetaMask Background Multiplex', err) ) - // connect ping stream - const pongStream = new PongStream({ objectMode: true }) - pump( - mux, - pongStream, - mux, - (err) => logStreamDisconnectWarning('MetaMask PingPongStream', err) - ) + // forward communication across inpage-background for these channels only + forwardTrafficBetweenMuxers('provider', pageMux, extensionMux) + forwardTrafficBetweenMuxers('publicConfig', pageMux, extensionMux) - // connect phishing warning stream - const phishingStream = mux.createStream('phishing') + // connect "phishing" channel to warning system + const phishingStream = extensionMux.createStream('phishing') phishingStream.once('data', redirectToPhishingWarning) - // ignore unused channels (handled by background, inpage) - mux.ignoreStream('provider') - mux.ignoreStream('publicConfig') + // connect "publicApi" channel to submit page metadata + const publicApiStream = extensionMux.createStream('publicApi') + const background = await setupPublicApi(publicApiStream) + + return { background } } -/** - * Establishes listeners for requests to fully-enable the provider from the dapp context - * and for full-provider approvals and rejections from the background script context. Dapps - * should not post messages directly and should instead call provider.enable(), which - * handles posting these messages internally. - */ -function listenForProviderRequest () { - window.addEventListener('message', ({ source, data }) => { - if (source !== window || !data || !data.type) { return } - switch (data.type) { - case 'ETHEREUM_ENABLE_PROVIDER': - extension.runtime.sendMessage({ - action: 'init-provider-request', - force: data.force, - origin: source.location.hostname, - siteImage: getSiteIcon(source), - siteTitle: getSiteName(source), - }) - break - case 'ETHEREUM_IS_APPROVED': - extension.runtime.sendMessage({ - action: 'init-is-approved', - origin: source.location.hostname, - }) - break - case 'METAMASK_IS_UNLOCKED': - extension.runtime.sendMessage({ - action: 'init-is-unlocked', - }) - break - } - }) +function forwardTrafficBetweenMuxers (channelName, muxA, muxB) { + const channelA = muxA.createStream(channelName) + const channelB = muxB.createStream(channelName) + pump( + channelA, + channelB, + channelA, + (err) => logStreamDisconnectWarning(`MetaMask muxed traffic for channel "${channelName}" failed.`, err) + ) +} - extension.runtime.onMessage.addListener(({ action = '', isApproved, caching, isUnlocked, selectedAddress }) => { - switch (action) { - case 'approve-provider-request': - isEnabled = true - window.postMessage({ type: 'ethereumprovider', selectedAddress }, '*') - break - case 'approve-legacy-provider-request': - isEnabled = true - window.postMessage({ type: 'ethereumproviderlegacy', selectedAddress }, '*') - break - case 'reject-provider-request': - window.postMessage({ type: 'ethereumprovider', error: 'User denied account authorization' }, '*') - break - case 'answer-is-approved': - window.postMessage({ type: 'ethereumisapproved', isApproved, caching }, '*') - break - case 'answer-is-unlocked': - window.postMessage({ type: 'metamaskisunlocked', isUnlocked }, '*') - break - case 'metamask-set-locked': - isEnabled = false - window.postMessage({ type: 'metamasksetlocked' }, '*') - break - case 'ethereum-ping-success': - window.postMessage({ type: 'ethereumpingsuccess' }, '*') - break - case 'ethereum-ping-error': - window.postMessage({ type: 'ethereumpingerror' }, '*') +async function setupPublicApi (outStream) { + const api = { + getSiteMetadata: (cb) => cb(null, getSiteMetadata()), + } + const dnode = Dnode(api) + pump( + outStream, + dnode, + outStream, + (err) => { + // report any error + if (err) log.error(err) } - }) + ) + const background = await new Promise(resolve => dnode.once('remote', resolve)) + return background } /** - * Checks if MetaMask is currently operating in "privacy mode", meaning - * dapps must call ethereum.enable in order to access user accounts + * Gets site metadata and returns it + * */ -function checkPrivacyMode () { - extension.runtime.sendMessage({ action: 'init-privacy-request' }) +function getSiteMetadata () { + // get metadata + const metadata = { + name: getSiteName(window), + icon: getSiteIcon(window), + } + return metadata } /** - * Error handler for page to plugin stream disconnections + * Error handler for page to extension stream disconnections * * @param {string} remoteLabel Remote stream name * @param {Error} err Stream connection error @@ -301,6 +257,10 @@ function redirectToPhishingWarning () { })}` } + +/** + * Extracts a name for the site from the DOM + */ function getSiteName (window) { const document = window.document const siteName = document.querySelector('head > meta[property="og:site_name"]') @@ -316,6 +276,9 @@ function getSiteName (window) { return document.title } +/** + * Extracts an icon for the site from the DOM + */ function getSiteIcon (window) { const document = window.document @@ -333,3 +296,13 @@ function getSiteIcon (window) { return null } + +/** + * Returns a promise that resolves when the DOM is loaded (does not wait for images to load) + */ +async function domIsReady () { + // already loaded + if (['interactive', 'complete'].includes(document.readyState)) return + // wait for load + await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve, { once: true })) +} diff --git a/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js new file mode 100644 index 000000000..9533fd458 --- /dev/null +++ b/app/scripts/controllers/app-state.js @@ -0,0 +1,73 @@ +const ObservableStore = require('obs-store') +const extend = require('xtend') + +class AppStateController { + /** + * @constructor + * @param opts + */ + constructor (opts = {}) { + const {initState, onInactiveTimeout, preferencesStore} = opts + const {preferences} = preferencesStore.getState() + + this.onInactiveTimeout = onInactiveTimeout || (() => {}) + this.store = new ObservableStore(extend({ + timeoutMinutes: 0, + }, initState)) + this.timer = null + + preferencesStore.subscribe(state => { + this._setInactiveTimeout(state.preferences.autoLogoutTimeLimit) + }) + + this._setInactiveTimeout(preferences.autoLogoutTimeLimit) + } + + /** + * Sets the last active time to the current time + * @return {void} + */ + setLastActiveTime () { + this._resetTimer() + } + + /** + * Sets the inactive timeout for the app + * @param {number} timeoutMinutes the inactive timeout in minutes + * @return {void} + * @private + */ + _setInactiveTimeout (timeoutMinutes) { + this.store.putState({ + timeoutMinutes, + }) + + this._resetTimer() + } + + /** + * Resets the internal inactive timer + * + * If the {@code timeoutMinutes} state is falsy (i.e., zero) then a new + * timer will not be created. + * + * @return {void} + * @private + */ + _resetTimer () { + const {timeoutMinutes} = this.store.getState() + + if (this.timer) { + clearTimeout(this.timer) + } + + if (!timeoutMinutes) { + return + } + + this.timer = setTimeout(() => this.onInactiveTimeout(), timeoutMinutes * 60 * 1000) + } +} + +module.exports = AppStateController + diff --git a/app/scripts/controllers/balance.js b/app/scripts/controllers/balance.js index 465751e61..b227d5d0a 100644 --- a/app/scripts/controllers/balance.js +++ b/app/scripts/controllers/balance.js @@ -68,7 +68,7 @@ class BalanceController { _registerUpdates () { const update = this.updateBalance.bind(this) - this.txController.on('tx:status-update', (txId, status) => { + this.txController.on('tx:status-update', (_, status) => { switch (status) { case 'submitted': case 'confirmed': diff --git a/app/scripts/controllers/blacklist.js b/app/scripts/controllers/blacklist.js deleted file mode 100644 index e55b09d03..000000000 --- a/app/scripts/controllers/blacklist.js +++ /dev/null @@ -1,136 +0,0 @@ -const ObservableStore = require('obs-store') -const extend = require('xtend') -const PhishingDetector = require('eth-phishing-detect/src/detector') -const log = require('loglevel') - -// compute phishing lists -const PHISHING_DETECTION_CONFIG = require('eth-phishing-detect/src/config.json') -// every four minutes -const POLLING_INTERVAL = 4 * 60 * 1000 - -class BlacklistController { - - /** - * Responsible for polling for and storing an up to date 'eth-phishing-detect' config.json file, while - * exposing a method that can check whether a given url is a phishing attempt. The 'eth-phishing-detect' - * config.json file contains a fuzzylist, whitelist and blacklist. - * - * - * @typedef {Object} BlacklistController - * @param {object} opts Overrides the defaults for the initial state of this.store - * @property {object} store The the store of the current phishing config - * @property {object} store.phishing Contains fuzzylist, whitelist and blacklist arrays. @see - * {@link https://github.com/MetaMask/eth-phishing-detect/blob/master/src/config.json} - * @property {object} _phishingDetector The PhishingDetector instantiated by passing store.phishing to - * PhishingDetector. - * @property {object} _phishingUpdateIntervalRef Id of the interval created to periodically update the blacklist - * - */ - constructor (opts = {}) { - const initState = extend({ - phishing: PHISHING_DETECTION_CONFIG, - whitelist: [], - }, opts.initState) - this.store = new ObservableStore(initState) - // phishing detector - this._phishingDetector = null - this._setupPhishingDetector(initState.phishing) - // polling references - this._phishingUpdateIntervalRef = null - } - - /** - * Adds the given hostname to the runtime whitelist - * @param {string} hostname the hostname to whitelist - */ - whitelistDomain (hostname) { - if (!hostname) { - return - } - - const { whitelist } = this.store.getState() - this.store.updateState({ - whitelist: [...new Set([hostname, ...whitelist])], - }) - } - - /** - * Given a url, returns the result of checking if that url is in the store.phishing blacklist - * - * @param {string} hostname The hostname portion of a url; the one that will be checked against the white and - * blacklists of store.phishing - * @returns {boolean} Whether or not the passed hostname is on our phishing blacklist - * - */ - checkForPhishing (hostname) { - if (!hostname) return false - - const { whitelist } = this.store.getState() - if (whitelist.some((e) => e === hostname)) { - return false - } - - const { result } = this._phishingDetector.check(hostname) - return result - } - - /** - * Queries `https://api.infura.io/v2/blacklist` for an updated blacklist config. This is passed to this._phishingDetector - * to update our phishing detector instance, and is updated in the store. The new phishing config is returned - * - * - * @returns {Promise<object>} Promises the updated blacklist config for the phishingDetector - * - */ - async updatePhishingList () { - // make request - let response - try { - response = await fetch('https://api.infura.io/v2/blacklist') - } catch (err) { - log.error(new Error(`BlacklistController - failed to fetch blacklist:\n${err.stack}`)) - return - } - // parse response - let rawResponse - let phishing - try { - const rawResponse = await response.text() - phishing = JSON.parse(rawResponse) - } catch (err) { - log.error(new Error(`BlacklistController - failed to parse blacklist:\n${rawResponse}`)) - return - } - // update current blacklist - this.store.updateState({ phishing }) - this._setupPhishingDetector(phishing) - return phishing - } - - /** - * Initiates the updating of the local blacklist at a set interval. The update is done via this.updatePhishingList(). - * Also, this method store a reference to that interval at this._phishingUpdateIntervalRef - * - */ - scheduleUpdates () { - if (this._phishingUpdateIntervalRef) return - this.updatePhishingList() - this._phishingUpdateIntervalRef = setInterval(() => { - this.updatePhishingList() - }, POLLING_INTERVAL) - } - - /** - * Sets this._phishingDetector to a new PhishingDetector instance. - * @see {@link https://github.com/MetaMask/eth-phishing-detect} - * - * @private - * @param {object} config A config object like that found at {@link https://github.com/MetaMask/eth-phishing-detect/blob/master/src/config.json} - * - */ - _setupPhishingDetector (config) { - this._phishingDetector = new PhishingDetector(config) - } -} - -module.exports = BlacklistController diff --git a/app/scripts/controllers/network/createBlockTracker.js b/app/scripts/controllers/network/createBlockTracker.js deleted file mode 100644 index 6573b18a1..000000000 --- a/app/scripts/controllers/network/createBlockTracker.js +++ /dev/null @@ -1,19 +0,0 @@ -const BlockTracker = require('eth-block-tracker') - -/** - * Creates a block tracker that sends platform events on success and failure - */ -module.exports = function createBlockTracker (args, platform) { - const blockTracker = new BlockTracker(args) - blockTracker.on('latest', () => { - if (platform && platform.sendMessage) { - platform.sendMessage({ action: 'ethereum-ping-success' }) - } - }) - blockTracker.on('error', () => { - if (platform && platform.sendMessage) { - platform.sendMessage({ action: 'ethereum-ping-error' }) - } - }) - return blockTracker -} diff --git a/app/scripts/controllers/network/createInfuraClient.js b/app/scripts/controllers/network/createInfuraClient.js index 70b332867..0a6e9ecb0 100644 --- a/app/scripts/controllers/network/createInfuraClient.js +++ b/app/scripts/controllers/network/createInfuraClient.js @@ -7,14 +7,14 @@ const createInflightMiddleware = require('eth-json-rpc-middleware/inflight-cache const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware') const createInfuraMiddleware = require('eth-json-rpc-infura') -const createBlockTracker = require('./createBlockTracker') +const BlockTracker = require('eth-block-tracker') module.exports = createInfuraClient -function createInfuraClient ({ network, platform }) { +function createInfuraClient ({ network }) { const infuraMiddleware = createInfuraMiddleware({ network, maxAttempts: 5, source: 'metamask' }) const infuraProvider = providerFromMiddleware(infuraMiddleware) - const blockTracker = createBlockTracker({ provider: infuraProvider }, platform) + const blockTracker = new BlockTracker({ provider: infuraProvider }) const networkMiddleware = mergeMiddleware([ createNetworkAndChainIdMiddleware({ network }), diff --git a/app/scripts/controllers/network/createJsonRpcClient.js b/app/scripts/controllers/network/createJsonRpcClient.js index 369dcd299..a8cbf2aaf 100644 --- a/app/scripts/controllers/network/createJsonRpcClient.js +++ b/app/scripts/controllers/network/createJsonRpcClient.js @@ -5,14 +5,14 @@ const createBlockCacheMiddleware = require('eth-json-rpc-middleware/block-cache' const createInflightMiddleware = require('eth-json-rpc-middleware/inflight-cache') const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware') -const createBlockTracker = require('./createBlockTracker') +const BlockTracker = require('eth-block-tracker') module.exports = createJsonRpcClient -function createJsonRpcClient ({ rpcUrl, platform }) { +function createJsonRpcClient ({ rpcUrl }) { const fetchMiddleware = createFetchMiddleware({ rpcUrl }) const blockProvider = providerFromMiddleware(fetchMiddleware) - const blockTracker = createBlockTracker({ provider: blockProvider }, platform) + const blockTracker = new BlockTracker({ provider: blockProvider }) const networkMiddleware = mergeMiddleware([ createBlockRefRewriteMiddleware({ blockTracker }), diff --git a/app/scripts/controllers/network/createLocalhostClient.js b/app/scripts/controllers/network/createLocalhostClient.js index 36593dc70..09b1d3c1c 100644 --- a/app/scripts/controllers/network/createLocalhostClient.js +++ b/app/scripts/controllers/network/createLocalhostClient.js @@ -3,14 +3,14 @@ const createFetchMiddleware = require('eth-json-rpc-middleware/fetch') const createBlockRefRewriteMiddleware = require('eth-json-rpc-middleware/block-ref-rewrite') const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware') -const createBlockTracker = require('./createBlockTracker') +const BlockTracker = require('eth-block-tracker') module.exports = createLocalhostClient -function createLocalhostClient ({ platform }) { +function createLocalhostClient () { const fetchMiddleware = createFetchMiddleware({ rpcUrl: 'http://localhost:8545/' }) const blockProvider = providerFromMiddleware(fetchMiddleware) - const blockTracker = createBlockTracker({ provider: blockProvider, pollingInterval: 1000 }, platform) + const blockTracker = new BlockTracker({ provider: blockProvider, pollingInterval: 1000 }) const networkMiddleware = mergeMiddleware([ createBlockRefRewriteMiddleware({ blockTracker }), diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js index c00ac7e6a..2c68e4378 100644 --- a/app/scripts/controllers/network/network.js +++ b/app/scripts/controllers/network/network.js @@ -46,9 +46,8 @@ const defaultNetworkConfig = { module.exports = class NetworkController extends EventEmitter { - constructor (opts = {}, platform) { + constructor (opts = {}) { super() - this.platform = platform // parse options const providerConfig = opts.provider || defaultProviderConfig @@ -130,13 +129,14 @@ module.exports = class NetworkController extends EventEmitter { }) } - setRpcTarget (rpcTarget, chainId, ticker = 'ETH', nickname = '') { + setRpcTarget (rpcTarget, chainId, ticker = 'ETH', nickname = '', rpcPrefs) { const providerConfig = { type: 'rpc', rpcTarget, chainId, ticker, nickname, + rpcPrefs, } this.providerConfig = providerConfig } @@ -190,7 +190,7 @@ module.exports = class NetworkController extends EventEmitter { _configureInfuraProvider ({ type }) { log.info('NetworkController - configureInfuraProvider', type) - const networkClient = createInfuraClient({ network: type, platform: this.platform }) + const networkClient = createInfuraClient({ network: type }) this._setNetworkClient(networkClient) // setup networkConfig var settings = { @@ -201,13 +201,13 @@ module.exports = class NetworkController extends EventEmitter { _configureLocalhostProvider () { log.info('NetworkController - configureLocalhostProvider') - const networkClient = createLocalhostClient({ platform: this.platform }) + const networkClient = createLocalhostClient() this._setNetworkClient(networkClient) } _configureStandardProvider ({ rpcUrl, chainId, ticker, nickname }) { log.info('NetworkController - configureStandardProvider', rpcUrl) - const networkClient = createJsonRpcClient({ rpcUrl, platform: this.platform }) + const networkClient = createJsonRpcClient({ rpcUrl }) // hack to add a 'rpc' network with chainId networks.networkList['rpc'] = { chainId: chainId, diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 737411890..acf952bb1 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -117,6 +117,14 @@ class PreferencesController { return metaMetricsId } + getMetaMetricsId () { + return this.store.getState().metaMetricsId + } + + getParticipateInMetaMetrics () { + return this.store.getState().participateInMetaMetrics + } + setMetaMetricsSendCount (val) { this.store.updateState({ metaMetricsSendCount: val }) } @@ -331,7 +339,7 @@ class PreferencesController { } removeSuggestedTokens () { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { this.store.updateState({ suggestedTokens: {} }) resolve({}) }) @@ -388,7 +396,7 @@ class PreferencesController { const newEntry = { address, symbol, decimals } const tokens = this.store.getState().tokens const assetImages = this.getAssetImages() - const previousEntry = tokens.find((token, index) => { + const previousEntry = tokens.find((token) => { return token.address === address }) const previousIndex = tokens.indexOf(previousEntry) @@ -453,7 +461,7 @@ class PreferencesController { * */ setCurrentAccountTab (currentAccountTab) { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { this.store.updateState({ currentAccountTab }) resolve() }) @@ -480,8 +488,8 @@ class PreferencesController { rpcList[index] = updatedRpc this.store.updateState({ frequentRpcListDetail: rpcList }) } else { - const { rpcUrl, chainId, ticker, nickname } = newRpcDetails - return this.addToFrequentRpcList(rpcUrl, chainId, ticker, nickname) + const { rpcUrl, chainId, ticker, nickname, rpcPrefs = {} } = newRpcDetails + return this.addToFrequentRpcList(rpcUrl, chainId, ticker, nickname, rpcPrefs) } return Promise.resolve(rpcList) } @@ -495,22 +503,22 @@ class PreferencesController { * @returns {Promise<array>} Promise resolving to updated frequentRpcList. * */ - addToFrequentRpcList (url, chainId, ticker = 'ETH', nickname = '') { - 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') { - let checkedChainId - if (!!chainId && !Number.isNaN(parseInt(chainId))) { - checkedChainId = chainId + addToFrequentRpcList (url, chainId, ticker = 'ETH', nickname = '', rpcPrefs = {}) { + const rpcList = this.getFrequentRpcListDetail() + const index = rpcList.findIndex((element) => { return element.rpcUrl === url }) + if (index !== -1) { + rpcList.splice(index, 1) } - rpcList.push({ rpcUrl: url, chainId: checkedChainId, ticker, nickname }) + if (url !== 'http://localhost:8545') { + let checkedChainId + if (!!chainId && !Number.isNaN(parseInt(chainId))) { + checkedChainId = chainId + } + rpcList.push({ rpcUrl: url, chainId: checkedChainId, ticker, nickname, rpcPrefs }) + } + this.store.updateState({ frequentRpcListDetail: rpcList }) + return Promise.resolve(rpcList) } - this.store.updateState({ frequentRpcListDetail: rpcList }) - return Promise.resolve(rpcList) - } /** * Removes custom RPC url from state. diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js index 2c9182b52..8206b2f8a 100644 --- a/app/scripts/controllers/provider-approval.js +++ b/app/scripts/controllers/provider-approval.js @@ -1,9 +1,11 @@ const ObservableStore = require('obs-store') +const SafeEventEmitter = require('safe-event-emitter') +const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware') /** * A controller that services user-approved requests for a full Ethereum provider API */ -class ProviderApprovalController { +class ProviderApprovalController extends SafeEventEmitter { /** * Determines if caching is enabled */ @@ -14,38 +16,43 @@ class ProviderApprovalController { * * @param {Object} [config] - Options to configure controller */ - constructor ({ closePopup, keyringController, openPopup, platform, preferencesController, publicConfigStore } = {}) { + constructor ({ closePopup, keyringController, openPopup, preferencesController } = {}) { + super() this.approvedOrigins = {} this.closePopup = closePopup this.keyringController = keyringController this.openPopup = openPopup - this.platform = platform this.preferencesController = preferencesController - this.publicConfigStore = publicConfigStore this.store = new ObservableStore({ providerRequests: [], }) + } - if (platform && platform.addMessageListener) { - platform.addMessageListener(({ action = '', force, origin, siteTitle, siteImage }, { tab }) => { - if (tab && tab.id) { - switch (action) { - case 'init-provider-request': - this._handleProviderRequest(origin, siteTitle, siteImage, force, tab.id) - break - case 'init-is-approved': - this._handleIsApproved(origin, tab.id) - break - case 'init-is-unlocked': - this._handleIsUnlocked(tab.id) - break - case 'init-privacy-request': - this._handlePrivacyRequest(tab.id) - break - } - } - }) - } + /** + * Called when a user approves access to a full Ethereum provider API + * + * @param {object} opts - opts for the middleware contains the origin for the middleware + */ + createMiddleware ({ origin, getSiteMetadata }) { + return createAsyncMiddleware(async (req, res, next) => { + // only handle requestAccounts + if (req.method !== 'eth_requestAccounts') return next() + // if already approved or privacy mode disabled, return early + if (this.shouldExposeAccounts(origin)) { + res.result = [this.preferencesController.getSelectedAddress()] + return + } + // register the provider request + const metadata = await getSiteMetadata(origin) + this._handleProviderRequest(origin, metadata.name, metadata.icon, false, null) + // wait for resolution of request + const approved = await new Promise(resolve => this.once(`resolvedRequest:${origin}`, ({ approved }) => resolve(approved))) + if (approved) { + res.result = [this.preferencesController.getSelectedAddress()] + } else { + throw new Error('User denied account authorization') + } + }) } /** @@ -59,79 +66,37 @@ class ProviderApprovalController { this.store.updateState({ providerRequests: [{ origin, siteTitle, siteImage, tabID }] }) const isUnlocked = this.keyringController.memStore.getState().isUnlocked if (!force && this.approvedOrigins[origin] && this.caching && isUnlocked) { - this.approveProviderRequest(tabID) return } this.openPopup && this.openPopup() } /** - * Called by a tab to determine if an origin has been approved in the past - * - * @param {string} origin - Origin of the window - */ - _handleIsApproved (origin, tabID) { - this.platform && this.platform.sendMessage({ - action: 'answer-is-approved', - isApproved: this.approvedOrigins[origin] && this.caching, - caching: this.caching, - }, { id: tabID }) - } - - /** - * Called by a tab to determine if MetaMask is currently locked or unlocked - */ - _handleIsUnlocked (tabID) { - const isUnlocked = this.keyringController.memStore.getState().isUnlocked - this.platform && this.platform.sendMessage({ action: 'answer-is-unlocked', isUnlocked }, { id: tabID }) - } - - /** - * Called to check privacy mode; if privacy mode is off, this will automatically enable the provider (legacy behavior) - */ - _handlePrivacyRequest (tabID) { - const privacyMode = this.preferencesController.getFeatureFlags().privacyMode - if (!privacyMode) { - this.platform && this.platform.sendMessage({ - action: 'approve-legacy-provider-request', - selectedAddress: this.publicConfigStore.getState().selectedAddress, - }, { id: tabID }) - this.publicConfigStore.emit('update', this.publicConfigStore.getState()) - } - } - - /** * Called when a user approves access to a full Ethereum provider API * - * @param {string} tabID - ID of the target window that approved provider access + * @param {string} origin - origin of the domain that had provider access approved */ - approveProviderRequest (tabID) { + approveProviderRequestByOrigin (origin) { this.closePopup && this.closePopup() const requests = this.store.getState().providerRequests - const origin = requests.find(request => request.tabID === tabID).origin - this.platform && this.platform.sendMessage({ - action: 'approve-provider-request', - selectedAddress: this.publicConfigStore.getState().selectedAddress, - }, { id: tabID }) - this.publicConfigStore.emit('update', this.publicConfigStore.getState()) - const providerRequests = requests.filter(request => request.tabID !== tabID) + const providerRequests = requests.filter(request => request.origin !== origin) this.store.updateState({ providerRequests }) this.approvedOrigins[origin] = true + this.emit(`resolvedRequest:${origin}`, { approved: true }) } /** * Called when a tab rejects access to a full Ethereum provider API * - * @param {string} tabID - ID of the target window that rejected provider access + * @param {string} origin - origin of the domain that had provider access approved */ - rejectProviderRequest (tabID) { + rejectProviderRequestByOrigin (origin) { this.closePopup && this.closePopup() const requests = this.store.getState().providerRequests - const origin = requests.find(request => request.tabID === tabID).origin - this.platform && this.platform.sendMessage({ action: 'reject-provider-request' }, { id: tabID }) - const providerRequests = requests.filter(request => request.tabID !== tabID) + const providerRequests = requests.filter(request => request.origin !== origin) this.store.updateState({ providerRequests }) delete this.approvedOrigins[origin] + this.emit(`resolvedRequest:${origin}`, { approved: false }) } /** @@ -149,16 +114,10 @@ class ProviderApprovalController { */ shouldExposeAccounts (origin) { const privacyMode = this.preferencesController.getFeatureFlags().privacyMode - return !privacyMode || this.approvedOrigins[origin] + const result = !privacyMode || Boolean(this.approvedOrigins[origin]) + return result } - /** - * Tells all tabs that MetaMask is now locked. This is primarily used to set - * internal flags in the contentscript and inpage script. - */ - setLocked () { - this.platform.sendMessage({ action: 'metamask-set-locked' }) - } } module.exports = ProviderApprovalController diff --git a/app/scripts/controllers/shapeshift.js b/app/scripts/controllers/shapeshift.js deleted file mode 100644 index b2a1462c2..000000000 --- a/app/scripts/controllers/shapeshift.js +++ /dev/null @@ -1,180 +0,0 @@ -const ObservableStore = require('obs-store') -const extend = require('xtend') -const log = require('loglevel') - -// every three seconds when an incomplete tx is waiting -const POLLING_INTERVAL = 3000 - -class ShapeshiftController { - - /** - * Controller responsible for managing the list of shapeshift transactions. On construction, it initiates a poll - * that queries a shapeshift.io API for updates to any pending shapeshift transactions - * - * @typedef {Object} ShapeshiftController - * @param {object} opts Overrides the defaults for the initial state of this.store - * @property {array} opts.initState initializes the the state of the ShapeshiftController. Can contain an - * shapeShiftTxList array. - * @property {array} shapeShiftTxList An array of ShapeShiftTx objects - * - */ - constructor (opts = {}) { - const initState = extend({ - shapeShiftTxList: [], - }, opts.initState) - this.store = new ObservableStore(initState) - this.pollForUpdates() - } - - /** - * Represents, and contains data about, a single shapeshift transaction. - * @typedef {Object} ShapeShiftTx - * @property {string} depositAddress - An address at which to send a crypto deposit, so that eth can be sent to the - * user's Metamask account - * @property {string} depositType - An abbreviation of the type of crypto currency to be deposited. - * @property {string} key - The 'shapeshift' key differentiates this from other types of txs in Metamask - * @property {number} time - The time at which the tx was created - * @property {object} response - Initiated as an empty object, which will be replaced by a Response object. @see {@link - * https://developer.mozilla.org/en-US/docs/Web/API/Response} - */ - - // - // PUBLIC METHODS - // - - /** - * A getter for the shapeShiftTxList property - * - * @returns {array<ShapeShiftTx>} - * - */ - getShapeShiftTxList () { - const shapeShiftTxList = this.store.getState().shapeShiftTxList - return shapeShiftTxList - } - - /** - * A getter for all ShapeShiftTx in the shapeShiftTxList that have not successfully completed a deposit. - * - * @returns {array<ShapeShiftTx>} Only includes ShapeShiftTx which has a response property with a status !== complete - * - */ - getPendingTxs () { - const txs = this.getShapeShiftTxList() - const pending = txs.filter(tx => tx.response && tx.response.status !== 'complete') - return pending - } - - /** - * A poll that exists as long as there are pending transactions. Each call attempts to update the data of any - * pendingTxs, and then calls itself again. If there are no pending txs, the recursive call is not made and - * the polling stops. - * - * this.updateTx is used to attempt the update to the pendingTxs in the ShapeShiftTxList, and that updated data - * is saved with saveTx. - * - */ - pollForUpdates () { - const pendingTxs = this.getPendingTxs() - - if (pendingTxs.length === 0) { - return - } - - Promise.all(pendingTxs.map((tx) => { - return this.updateTx(tx) - })) - .then((results) => { - results.forEach(tx => this.saveTx(tx)) - this.timeout = setTimeout(this.pollForUpdates.bind(this), POLLING_INTERVAL) - }) - } - - /** - * Attempts to update a ShapeShiftTx with data from a shapeshift.io API. Both the response and time properties - * can be updated. The response property is updated with every call, but the time property is only updated when - * the response status updates to 'complete'. This will occur once the user makes a deposit as the ShapeShiftTx - * depositAddress - * - * @param {ShapeShiftTx} tx The tx to update - * - */ - async updateTx (tx) { - try { - const url = `https://shapeshift.io/txStat/${tx.depositAddress}` - const response = await fetch(url) - const json = await response.json() - tx.response = json - if (tx.response.status === 'complete') { - tx.time = new Date().getTime() - } - return tx - } catch (err) { - log.warn(err) - } - } - - /** - * Saves an updated to a ShapeShiftTx in the shapeShiftTxList. If the passed ShapeShiftTx is not in the - * shapeShiftTxList, nothing happens. - * - * @param {ShapeShiftTx} tx The updated tx to save, if it exists in the current shapeShiftTxList - * - */ - saveTx (tx) { - const { shapeShiftTxList } = this.store.getState() - const index = shapeShiftTxList.indexOf(tx) - if (index !== -1) { - shapeShiftTxList[index] = tx - this.store.updateState({ shapeShiftTxList }) - } - } - - /** - * Removes a ShapeShiftTx from the shapeShiftTxList - * - * @param {ShapeShiftTx} tx The tx to remove - * - */ - removeShapeShiftTx (tx) { - const { shapeShiftTxList } = this.store.getState() - const index = shapeShiftTxList.indexOf(index) - if (index !== -1) { - shapeShiftTxList.splice(index, 1) - } - this.updateState({ shapeShiftTxList }) - } - - /** - * Creates a new ShapeShiftTx, adds it to the shapeShiftTxList, and initiates a new poll for updates of pending txs - * - * @param {string} depositAddress - An address at which to send a crypto deposit, so that eth can be sent to the - * user's Metamask account - * @param {string} depositType - An abbreviation of the type of crypto currency to be deposited. - * - */ - createShapeShiftTx (depositAddress, depositType) { - const state = this.store.getState() - let { shapeShiftTxList } = state - - var shapeShiftTx = { - depositAddress, - depositType, - key: 'shapeshift', - time: new Date().getTime(), - response: {}, - } - - if (!shapeShiftTxList) { - shapeShiftTxList = [shapeShiftTx] - } else { - shapeShiftTxList.push(shapeShiftTx) - } - - this.store.updateState({ shapeShiftTxList }) - this.pollForUpdates() - } - -} - -module.exports = ShapeshiftController diff --git a/app/scripts/controllers/token-rates.js b/app/scripts/controllers/token-rates.js index 4e396bb59..6b6265dba 100644 --- a/app/scripts/controllers/token-rates.js +++ b/app/scripts/controllers/token-rates.js @@ -1,6 +1,8 @@ const ObservableStore = require('obs-store') const log = require('loglevel') const normalizeAddress = require('eth-sig-util').normalize +const ethUtil = require('ethereumjs-util') + // By default, poll every 3 minutes const DEFAULT_INTERVAL = 180 * 1000 @@ -36,7 +38,7 @@ class TokenRatesController { const response = await fetch(`https://api.coingecko.com/api/v3/simple/token_price/ethereum?${query}`) const prices = await response.json() this._tokens.forEach(token => { - const price = prices[token.address.toLowerCase()] + const price = prices[token.address.toLowerCase()] || prices[ethUtil.toChecksumAddress(token.address)] contractExchangeRates[normalizeAddress(token.address)] = price ? price[nativeCurrency] : 0 }) } catch (error) { diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 2ce736beb..79dba7833 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -3,6 +3,17 @@ const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') const Transaction = require('ethereumjs-tx') const EthQuery = require('ethjs-query') +const abi = require('human-standard-token-abi') +const abiDecoder = require('abi-decoder') +abiDecoder.addABI(abi) +const { + TOKEN_METHOD_APPROVE, + TOKEN_METHOD_TRANSFER, + TOKEN_METHOD_TRANSFER_FROM, + SEND_ETHER_ACTION_KEY, + DEPLOY_CONTRACT_ACTION_KEY, + CONTRACT_INTERACTION_KEY, +} = require('../../../../ui/app/helpers/constants/transactions.js') const TransactionStateManager = require('./tx-state-manager') const TxGasUtil = require('./tx-gas-utils') const PendingTransactionTracker = require('./pending-tx-tracker') @@ -180,9 +191,11 @@ class TransactionController extends EventEmitter { } txUtils.validateTxParams(normalizedTxParams) // construct txMeta + const { transactionCategory, getCodeResponse } = await this._determineTransactionCategory(txParams) let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams, type: TRANSACTION_TYPE_STANDARD, + transactionCategory, }) this.addTx(txMeta) this.emit('newUnapprovedTx', txMeta) @@ -191,7 +204,7 @@ class TransactionController extends EventEmitter { // check whether recipient account is blacklisted recipientBlacklistChecker.checkAccount(txMeta.metamaskNetworkId, normalizedTxParams.to) // add default tx params - txMeta = await this.addTxGasDefaults(txMeta) + txMeta = await this.addTxGasDefaults(txMeta, getCodeResponse) } catch (error) { log.warn(error) txMeta.loadingDefaults = false @@ -211,7 +224,7 @@ class TransactionController extends EventEmitter { @param txMeta {Object} - the txMeta object @returns {Promise<object>} resolves with txMeta */ - async addTxGasDefaults (txMeta) { + async addTxGasDefaults (txMeta, getCodeResponse) { const txParams = txMeta.txParams // ensure value txParams.value = txParams.value ? ethUtil.addHexPrefix(txParams.value) : '0x0' @@ -222,7 +235,7 @@ class TransactionController extends EventEmitter { } txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16)) // set gasLimit - return await this.txGasUtil.analyzeGasUsage(txMeta) + return await this.txGasUtil.analyzeGasUsage(txMeta, getCodeResponse) } /** @@ -556,6 +569,43 @@ class TransactionController extends EventEmitter { } /** + Returns a "type" for a transaction out of the following list: simpleSend, tokenTransfer, tokenApprove, + contractDeployment, contractMethodCall + */ + async _determineTransactionCategory (txParams) { + const { data, to } = txParams + const { name } = data && abiDecoder.decodeMethod(data) || {} + const tokenMethodName = [ + TOKEN_METHOD_APPROVE, + TOKEN_METHOD_TRANSFER, + TOKEN_METHOD_TRANSFER_FROM, + ].find(tokenMethodName => tokenMethodName === name && name.toLowerCase()) + + let result + let code + if (!txParams.data) { + result = SEND_ETHER_ACTION_KEY + } else if (tokenMethodName) { + result = tokenMethodName + } else if (!to) { + result = DEPLOY_CONTRACT_ACTION_KEY + } else { + try { + code = await this.query.getCode(to) + } catch (e) { + code = null + log.warn(e) + } + // For an address with no code, geth will return '0x', and ganache-core v2.2.1 will return '0x0' + const codeIsEmpty = !code || code === '0x' || code === '0x0' + + result = codeIsEmpty ? SEND_ETHER_ACTION_KEY : CONTRACT_INTERACTION_KEY + } + + return { transactionCategory: result, getCodeResponse: code } + } + + /** Sets other txMeta statuses to dropped if the txMeta that has been confirmed has other transactions in the list have the same nonce diff --git a/app/scripts/controllers/transactions/tx-gas-utils.js b/app/scripts/controllers/transactions/tx-gas-utils.js index 765551167..287fb6f44 100644 --- a/app/scripts/controllers/transactions/tx-gas-utils.js +++ b/app/scripts/controllers/transactions/tx-gas-utils.js @@ -4,7 +4,9 @@ const { BnMultiplyByFraction, bnToHex, } = require('../../lib/util') +const log = require('loglevel') const { addHexPrefix } = require('ethereumjs-util') +const { SEND_ETHER_ACTION_KEY } = require('../../../../ui/app/helpers/constants/transactions.js') const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send. import { TRANSACTION_NO_CONTRACT_ERROR_KEY } from '../../../../ui/app/helpers/constants/error-keys' @@ -26,12 +28,13 @@ class TxGasUtil { @param txMeta {Object} - the txMeta object @returns {object} the txMeta object with the gas written to the txParams */ - async analyzeGasUsage (txMeta) { + async analyzeGasUsage (txMeta, getCodeResponse) { const block = await this.query.getBlockByNumber('latest', false) let estimatedGasHex try { - estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit) + estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit, getCodeResponse) } catch (err) { + log.warn(err) txMeta.simulationFails = { reason: err.message, errorKey: err.errorKey, @@ -54,7 +57,7 @@ class TxGasUtil { @param blockGasLimitHex {string} - hex string of the block's gas limit @returns {string} the estimated gas limit as a hex string */ - async estimateTxGas (txMeta, blockGasLimitHex) { + async estimateTxGas (txMeta, blockGasLimitHex, getCodeResponse) { const txParams = txMeta.txParams // check if gasLimit is already specified @@ -70,11 +73,10 @@ class TxGasUtil { // see if we can set the gas based on the recipient if (hasRecipient) { - const code = await this.query.getCode(recipient) // For an address with no code, geth will return '0x', and ganache-core v2.2.1 will return '0x0' - const codeIsEmpty = !code || code === '0x' || code === '0x0' + const categorizedAsSimple = txMeta.transactionCategory === SEND_ETHER_ACTION_KEY - if (codeIsEmpty) { + if (categorizedAsSimple) { // if there's data in the params, but there's no contract code, it's not a valid transaction if (txParams.data) { const err = new Error('TxGasUtil - Trying to call a function on a non-contract address') @@ -82,7 +84,7 @@ class TxGasUtil { err.errorKey = TRANSACTION_NO_CONTRACT_ERROR_KEY // set the response on the error so that we can see in logs what the actual response was - err.getCodeResponse = code + err.getCodeResponse = getCodeResponse throw err } diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js index 420191d9c..1a2cb5dee 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -126,10 +126,10 @@ class TransactionStateManager extends EventEmitter { @returns {object} the txMeta */ addTx (txMeta) { - this.once(`${txMeta.id}:signed`, function (txId) { + this.once(`${txMeta.id}:signed`, function () { this.removeAllListeners(`${txMeta.id}:rejected`) }) - this.once(`${txMeta.id}:rejected`, function (txId) { + this.once(`${txMeta.id}:rejected`, function () { this.removeAllListeners(`${txMeta.id}:signed`) }) // initialize history diff --git a/app/scripts/createStandardProvider.js b/app/scripts/createStandardProvider.js index a5f9c5d03..2059b9b3a 100644 --- a/app/scripts/createStandardProvider.js +++ b/app/scripts/createStandardProvider.js @@ -4,18 +4,10 @@ class StandardProvider { constructor (provider) { this._provider = provider - this._onMessage('ethereumpingerror', this._onClose.bind(this)) - this._onMessage('ethereumpingsuccess', this._onConnect.bind(this)) - window.addEventListener('load', () => { - this._subscribe() - this._ping() - }) - } - - _onMessage (type, handler) { - window.addEventListener('message', function ({ data }) { - if (!data || data.type !== type) return - handler.apply(this, arguments) + this._subscribe() + // indicate that we've connected, mostly just for standard compliance + setTimeout(() => { + this._onConnect() }) } @@ -34,15 +26,6 @@ class StandardProvider { this._isConnected = true } - async _ping () { - try { - await this.send('net_version') - window.postMessage({ type: 'ethereumpingsuccess' }, '*') - } catch (error) { - window.postMessage({ type: 'ethereumpingerror' }, '*') - } - } - _subscribe () { this._provider.on('data', (error, { method, params }) => { if (!error && method === 'eth_subscription') { @@ -59,11 +42,9 @@ class StandardProvider { * @returns {Promise<*>} Promise resolving to the result if successful */ send (method, params = []) { - if (method === 'eth_requestAccounts') return this._provider.enable() - return new Promise((resolve, reject) => { try { - this._provider.sendAsync({ method, params, beta: true }, (error, response) => { + this._provider.sendAsync({ id: 1, jsonrpc: '2.0', method, params }, (error, response) => { error = error || response.error error ? reject(error) : resolve(response) }) diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index 71cfb875c..a4fb552f1 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -7,32 +7,12 @@ const setupDappAutoReload = require('./lib/auto-reload.js') const MetamaskInpageProvider = require('metamask-inpage-provider') const createStandardProvider = require('./createStandardProvider').default -let isEnabled = false let warned = false -let providerHandle -let isApprovedHandle -let isUnlockedHandle restoreContextAfterImports() log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn') -/** - * Adds a postMessage listener for a specific message type - * - * @param {string} messageType - postMessage type to listen for - * @param {Function} handler - event handler - * @param {boolean} remove - removes this handler after being triggered - */ -function onMessage (messageType, callback, remove) { - const handler = function ({ data }) { - if (!data || data.type !== messageType) { return } - remove && window.removeEventListener('message', handler) - callback.apply(window, arguments) - } - window.addEventListener('message', handler) -} - // // setup plugin communication // @@ -49,45 +29,16 @@ const inpageProvider = new MetamaskInpageProvider(metamaskStream) // set a high max listener count to avoid unnecesary warnings inpageProvider.setMaxListeners(100) -// set up a listener for when MetaMask is locked -onMessage('metamasksetlocked', () => { isEnabled = false }) - -// set up a listener for privacy mode responses -onMessage('ethereumproviderlegacy', ({ data: { selectedAddress } }) => { - isEnabled = true - setTimeout(() => { - inpageProvider.publicConfigStore.updateState({ selectedAddress }) - }, 0) -}, true) - // augment the provider with its enable method inpageProvider.enable = function ({ force } = {}) { return new Promise((resolve, reject) => { - providerHandle = ({ data: { error, selectedAddress } }) => { - if (typeof error !== 'undefined') { - reject({ - message: error, - code: 4001, - }) + inpageProvider.sendAsync({ method: 'eth_requestAccounts', params: [force] }, (error, response) => { + if (error) { + reject(error) } else { - window.removeEventListener('message', providerHandle) - setTimeout(() => { - inpageProvider.publicConfigStore.updateState({ selectedAddress }) - }, 0) - - // wait for the background to update with an account - inpageProvider.sendAsync({ method: 'eth_accounts', params: [] }, (error, response) => { - if (error) { - reject(error) - } else { - isEnabled = true - resolve(response.result) - } - }) + resolve(response.result) } - } - onMessage('ethereumprovider', providerHandle, true) - window.postMessage({ type: 'ETHEREUM_ENABLE_PROVIDER', force }, '*') + }) }) } @@ -98,31 +49,23 @@ inpageProvider.autoRefreshOnNetworkChange = true // add metamask-specific convenience methods inpageProvider._metamask = new Proxy({ /** - * Determines if this domain is currently enabled + * Synchronously determines if this domain is currently enabled, with a potential false negative if called to soon * - * @returns {boolean} - true if this domain is currently enabled + * @returns {boolean} - returns true if this domain is currently enabled */ isEnabled: function () { - return isEnabled + const { isEnabled } = inpageProvider.publicConfigStore.getState() + return Boolean(isEnabled) }, /** - * Determines if this domain has been previously approved + * Asynchronously determines if this domain is currently enabled * - * @returns {Promise<boolean>} - Promise resolving to true if this domain has been previously approved + * @returns {Promise<boolean>} - Promise resolving to true if this domain is currently enabled */ - isApproved: function () { - return new Promise((resolve) => { - isApprovedHandle = ({ data: { caching, isApproved } }) => { - if (caching) { - resolve(!!isApproved) - } else { - resolve(false) - } - } - onMessage('ethereumisapproved', isApprovedHandle, true) - window.postMessage({ type: 'ETHEREUM_IS_APPROVED' }, '*') - }) + isApproved: async function () { + const { isEnabled } = await getPublicConfigWhenReady() + return Boolean(isEnabled) }, /** @@ -130,14 +73,9 @@ inpageProvider._metamask = new Proxy({ * * @returns {Promise<boolean>} - Promise resolving to true if MetaMask is currently unlocked */ - isUnlocked: function () { - return new Promise((resolve) => { - isUnlockedHandle = ({ data: { isUnlocked } }) => { - resolve(!!isUnlocked) - } - onMessage('metamaskisunlocked', isUnlockedHandle, true) - window.postMessage({ type: 'METAMASK_IS_UNLOCKED' }, '*') - }) + isUnlocked: async function () { + const { isUnlocked } = await getPublicConfigWhenReady() + return Boolean(isUnlocked) }, }, { get: function (obj, prop) { @@ -149,6 +87,19 @@ inpageProvider._metamask = new Proxy({ }, }) +// publicConfig isn't populated until we get a message from background. +// Using this getter will ensure the state is available +async function getPublicConfigWhenReady () { + const store = inpageProvider.publicConfigStore + let state = store.getState() + // if state is missing, wait for first update + if (!state.networkVersion) { + state = await new Promise(resolve => store.once('update', resolve)) + console.log('new state', state) + } + return state +} + // Work around for web3@1.0 deleting the bound `sendAsync` but not the unbound // `sendAsync` method on the prototype, causing `this` reference issues with drizzle const proxiedInpageProvider = new Proxy(inpageProvider, { @@ -159,19 +110,6 @@ const proxiedInpageProvider = new Proxy(inpageProvider, { window.ethereum = createStandardProvider(proxiedInpageProvider) -// detect eth_requestAccounts and pipe to enable for now -function detectAccountRequest (method) { - const originalMethod = inpageProvider[method] - inpageProvider[method] = function ({ method }) { - if (method === 'eth_requestAccounts') { - return window.ethereum.enable() - } - return originalMethod.apply(this, arguments) - } -} -detectAccountRequest('send') -detectAccountRequest('sendAsync') - // // setup web3 // diff --git a/app/scripts/lib/backend-metametrics.js b/app/scripts/lib/backend-metametrics.js new file mode 100644 index 000000000..e3c163c1a --- /dev/null +++ b/app/scripts/lib/backend-metametrics.js @@ -0,0 +1,26 @@ +const { + getMetaMetricState, +} = require('../../../ui/app/selectors/selectors') +const { + sendMetaMetricsEvent, +} = require('../../../ui/app/helpers/utils/metametrics.util') + +const inDevelopment = process.env.NODE_ENV === 'development' + +const METAMETRICS_TRACKING_URL = inDevelopment + ? 'http://www.metamask.io/metametrics' + : 'http://www.metamask.io/metametrics-prod' + +function backEndMetaMetricsEvent (metaMaskState, eventData) { + const stateEventData = getMetaMetricState({ metamask: metaMaskState }) + + if (stateEventData.participateInMetaMetrics) { + sendMetaMetricsEvent({ + ...stateEventData, + ...eventData, + url: METAMETRICS_TRACKING_URL + '/backend', + }) + } +} + +module.exports = backEndMetaMetricsEvent diff --git a/app/scripts/lib/createDnodeRemoteGetter.js b/app/scripts/lib/createDnodeRemoteGetter.js new file mode 100644 index 000000000..b70d218f3 --- /dev/null +++ b/app/scripts/lib/createDnodeRemoteGetter.js @@ -0,0 +1,16 @@ +module.exports = createDnodeRemoteGetter + +function createDnodeRemoteGetter (dnode) { + let remote + + dnode.once('remote', (_remote) => { + remote = _remote + }) + + async function getRemote () { + if (remote) return remote + return await new Promise(resolve => dnode.once('remote', resolve)) + } + + return getRemote +} diff --git a/app/scripts/lib/message-manager.js b/app/scripts/lib/message-manager.js index e86629590..ac41de523 100644 --- a/app/scripts/lib/message-manager.js +++ b/app/scripts/lib/message-manager.js @@ -34,7 +34,7 @@ module.exports = class MessageManager extends EventEmitter { * @property {array} messages Holds all messages that have been created by this MessageManager * */ - constructor (opts) { + constructor () { super() this.memStore = new ObservableStore({ unapprovedMsgs: {}, diff --git a/app/scripts/lib/personal-message-manager.js b/app/scripts/lib/personal-message-manager.js index fdb94f5ec..7c13e521a 100644 --- a/app/scripts/lib/personal-message-manager.js +++ b/app/scripts/lib/personal-message-manager.js @@ -36,7 +36,7 @@ module.exports = class PersonalMessageManager extends EventEmitter { * @property {array} messages Holds all messages that have been created by this PersonalMessageManager * */ - constructor (opts) { + constructor () { super() this.memStore = new ObservableStore({ unapprovedPersonalMsgs: {}, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 0506e3116..55ca96ad4 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -7,8 +7,10 @@ const EventEmitter = require('events') const pump = require('pump') const Dnode = require('dnode') +const pify = require('pify') const ObservableStore = require('obs-store') const ComposableObservableStore = require('./lib/ComposableObservableStore') +const createDnodeRemoteGetter = require('./lib/createDnodeRemoteGetter') const asStream = require('obs-store/lib/asStream') const AccountTracker = require('./lib/account-tracker') const RpcEngine = require('json-rpc-engine') @@ -23,10 +25,9 @@ const {setupMultiplex} = require('./lib/stream-utils.js') const KeyringController = require('eth-keyring-controller') const NetworkController = require('./controllers/network') const PreferencesController = require('./controllers/preferences') +const AppStateController = require('./controllers/app-state') const CurrencyController = require('./controllers/currency') -const ShapeShiftController = require('./controllers/shapeshift') const InfuraController = require('./controllers/infura') -const BlacklistController = require('./controllers/blacklist') const CachedBalancesController = require('./controllers/cached-balances') const RecentBlocksController = require('./controllers/recent-blocks') const MessageManager = require('./lib/message-manager') @@ -53,7 +54,12 @@ const HW_WALLETS_KEYRINGS = [TrezorKeyring.type, LedgerBridgeKeyring.type] const EthQuery = require('eth-query') const ethUtil = require('ethereumjs-util') const sigUtil = require('eth-sig-util') -const { AddressBookController } = require('gaba') +const { + AddressBookController, + ShapeShiftController, + PhishingController, +} = require('gaba') +const backEndMetaMetricsEvent = require('./lib/backend-metametrics') module.exports = class MetamaskController extends EventEmitter { @@ -86,7 +92,7 @@ module.exports = class MetamaskController extends EventEmitter { this.createVaultMutex = new Mutex() // network store - this.networkController = new NetworkController(initState.NetworkController, this.platform) + this.networkController = new NetworkController(initState.NetworkController) // preferences controller this.preferencesController = new PreferencesController({ @@ -96,6 +102,12 @@ module.exports = class MetamaskController extends EventEmitter { network: this.networkController, }) + // app-state controller + this.appStateController = new AppStateController({ + preferencesStore: this.preferencesController.store, + onInactiveTimeout: () => this.setLocked(), + }) + // currency controller this.currencyController = new CurrencyController({ initState: initState.CurrencyController, @@ -109,8 +121,7 @@ module.exports = class MetamaskController extends EventEmitter { }) this.infuraController.scheduleInfuraNetworkCheck() - this.blacklistController = new BlacklistController() - this.blacklistController.scheduleUpdates() + this.phishingController = new PhishingController() // rpc provider this.initializeProvider() @@ -190,10 +201,26 @@ module.exports = class MetamaskController extends EventEmitter { }) this.txController.on('newUnapprovedTx', () => opts.showUnapprovedTx()) - this.txController.on(`tx:status-update`, (txId, status) => { + this.txController.on(`tx:status-update`, async (txId, status) => { if (status === 'confirmed' || status === 'failed') { const txMeta = this.txController.txStateManager.getTx(txId) this.platform.showTransactionNotification(txMeta) + + const { txReceipt } = txMeta + const participateInMetaMetrics = this.preferencesController.getParticipateInMetaMetrics() + if (txReceipt && txReceipt.status === '0x0' && participateInMetaMetrics) { + const metamaskState = await this.getState() + backEndMetaMetricsEvent(metamaskState, { + customVariables: { + errorMessage: txMeta.simulationFails.reason, + }, + eventOpts: { + category: 'backend', + action: 'Transactions', + name: 'On Chain Failure', + }, + }) + } } }) @@ -210,38 +237,40 @@ module.exports = class MetamaskController extends EventEmitter { }) this.balancesController.updateAllBalances() - this.shapeshiftController = new ShapeShiftController({ - initState: initState.ShapeShiftController, - }) + this.shapeshiftController = new ShapeShiftController(undefined, initState.ShapeShiftController) this.networkController.lookupNetwork() this.messageManager = new MessageManager() this.personalMessageManager = new PersonalMessageManager() this.typedMessageManager = new TypedMessageManager({ networkController: this.networkController }) - this.publicConfigStore = this.initPublicConfigStore() + + // ensure isClientOpenAndUnlocked is updated when memState updates + this.on('update', (memState) => { + this.isClientOpenAndUnlocked = memState.isUnlocked && this._isClientOpen + }) this.providerApprovalController = new ProviderApprovalController({ closePopup: opts.closePopup, keyringController: this.keyringController, openPopup: opts.openPopup, - platform: opts.platform, preferencesController: this.preferencesController, - publicConfigStore: this.publicConfigStore, }) this.store.updateStructure({ + AppStateController: this.appStateController.store, TransactionController: this.txController.store, KeyringController: this.keyringController.store, PreferencesController: this.preferencesController.store, AddressBookController: this.addressBookController, CurrencyController: this.currencyController.store, - ShapeShiftController: this.shapeshiftController.store, + ShapeShiftController: this.shapeshiftController, NetworkController: this.networkController.store, InfuraController: this.infuraController.store, CachedBalancesController: this.cachedBalancesController.store, }) this.memStore = new ComposableObservableStore(null, { + AppStateController: this.appStateController.store, NetworkController: this.networkController.store, AccountTracker: this.accountTracker.store, TxController: this.txController.memStore, @@ -256,7 +285,7 @@ module.exports = class MetamaskController extends EventEmitter { RecentBlocksController: this.recentBlocksController.store, AddressBookController: this.addressBookController, CurrencyController: this.currencyController.store, - ShapeshiftController: this.shapeshiftController.store, + ShapeshiftController: this.shapeshiftController, InfuraController: this.infuraController.store, ProviderApprovalController: this.providerApprovalController.store, }) @@ -305,22 +334,32 @@ module.exports = class MetamaskController extends EventEmitter { * Constructor helper: initialize a public config store. * This store is used to make some config info available to Dapps synchronously. */ - initPublicConfigStore () { - // get init state + createPublicConfigStore ({ checkIsEnabled }) { + // subset of state for metamask inpage provider const publicConfigStore = new ObservableStore() - // memStore -> transform -> publicConfigStore - this.on('update', (memState) => { - this.isClientOpenAndUnlocked = memState.isUnlocked && this._isClientOpen + // setup memStore subscription hooks + this.on('update', updatePublicConfigStore) + updatePublicConfigStore(this.getState()) + + publicConfigStore.destroy = () => { + this.removeEventListener('update', updatePublicConfigStore) + } + + function updatePublicConfigStore (memState) { const publicState = selectPublicState(memState) publicConfigStore.putState(publicState) - }) + } - function selectPublicState (memState) { + function selectPublicState ({ isUnlocked, selectedAddress, network, completedOnboarding }) { + const isEnabled = checkIsEnabled() + const isReady = isUnlocked && isEnabled const result = { - selectedAddress: memState.isUnlocked ? memState.selectedAddress : undefined, - networkVersion: memState.network, - onboardingcomplete: memState.completedOnboarding, + isUnlocked, + isEnabled, + selectedAddress: isReady ? selectedAddress : undefined, + networkVersion: network, + onboardingcomplete: completedOnboarding, } return result } @@ -430,6 +469,9 @@ module.exports = class MetamaskController extends EventEmitter { // AddressController setAddressBook: this.addressBookController.set.bind(this.addressBookController), + // AppStateController + setLastActiveTime: nodeify(this.appStateController.setLastActiveTime, this.appStateController), + // KeyringController setLocked: nodeify(this.setLocked, this), createNewVaultAndKeychain: nodeify(this.createNewVaultAndKeychain, this), @@ -460,9 +502,10 @@ module.exports = class MetamaskController extends EventEmitter { signTypedMessage: nodeify(this.signTypedMessage, this), cancelTypedMessage: this.cancelTypedMessage.bind(this), - approveProviderRequest: providerApprovalController.approveProviderRequest.bind(providerApprovalController), + // provider approval + approveProviderRequestByOrigin: providerApprovalController.approveProviderRequestByOrigin.bind(providerApprovalController), + rejectProviderRequestByOrigin: providerApprovalController.rejectProviderRequestByOrigin.bind(providerApprovalController), clearApprovedOrigins: providerApprovalController.clearApprovedOrigins.bind(providerApprovalController), - rejectProviderRequest: providerApprovalController.rejectProviderRequest.bind(providerApprovalController), } } @@ -1190,9 +1233,8 @@ module.exports = class MetamaskController extends EventEmitter { * with higher gas. * * @param {string} txId - The ID of the transaction to speed up. - * @param {Function} cb - The callback function called with a full state update. */ - async retryTransaction (txId, gasPrice, cb) { + async retryTransaction (txId, gasPrice) { await this.txController.retryTransaction(txId, gasPrice) const state = await this.getState() return state @@ -1205,7 +1247,7 @@ module.exports = class MetamaskController extends EventEmitter { * @param {string=} customGasPrice - the hex value to use for the cancel transaction * @returns {object} MetaMask state */ - async createCancelTransaction (originalTxId, customGasPrice, cb) { + async createCancelTransaction (originalTxId, customGasPrice) { try { await this.txController.createCancelTransaction(originalTxId, customGasPrice) const state = await this.getState() @@ -1215,7 +1257,7 @@ module.exports = class MetamaskController extends EventEmitter { } } - async createSpeedUpTransaction (originalTxId, customGasPrice, cb) { + async createSpeedUpTransaction (originalTxId, customGasPrice) { await this.txController.createSpeedUpTransaction(originalTxId, customGasPrice) const state = await this.getState() return state @@ -1270,7 +1312,7 @@ module.exports = class MetamaskController extends EventEmitter { */ setupUntrustedCommunication (connectionStream, originDomain) { // Check if new connection is blacklisted - if (this.blacklistController.checkForPhishing(originDomain)) { + if (this.phishingController.test(originDomain)) { log.debug('MetaMask - sending phishing warning for', originDomain) this.sendPhishingWarning(connectionStream, originDomain) return @@ -1279,8 +1321,9 @@ module.exports = class MetamaskController extends EventEmitter { // setup multiplexing const mux = setupMultiplex(connectionStream) // connect features - this.setupProviderConnection(mux.createStream('provider'), originDomain) - this.setupPublicConfig(mux.createStream('publicConfig')) + const publicApi = this.setupPublicApi(mux.createStream('publicApi'), originDomain) + this.setupProviderConnection(mux.createStream('provider'), originDomain, publicApi) + this.setupPublicConfig(mux.createStream('publicConfig'), originDomain) } /** @@ -1353,7 +1396,7 @@ module.exports = class MetamaskController extends EventEmitter { * @param {*} outStream - The stream to provide over. * @param {string} origin - The URI of the requesting resource. */ - setupProviderConnection (outStream, origin) { + setupProviderConnection (outStream, origin, publicApi) { // setup json rpc engine stack const engine = new RpcEngine() const provider = this.provider @@ -1373,6 +1416,11 @@ module.exports = class MetamaskController extends EventEmitter { engine.push(subscriptionManager.middleware) // watch asset engine.push(this.preferencesController.requestWatchAsset.bind(this.preferencesController)) + // requestAccounts + engine.push(this.providerApprovalController.createMiddleware({ + origin, + getSiteMetadata: publicApi && publicApi.getSiteMetadata, + })) // forward to metamask primary provider engine.push(providerAsMiddleware(provider)) @@ -1401,12 +1449,18 @@ module.exports = class MetamaskController extends EventEmitter { * * @param {*} outStream - The stream to provide public config over. */ - setupPublicConfig (outStream) { - const configStream = asStream(this.publicConfigStore) + setupPublicConfig (outStream, originDomain) { + const configStore = this.createPublicConfigStore({ + // check the providerApprovalController's approvedOrigins + checkIsEnabled: () => this.providerApprovalController.shouldExposeAccounts(originDomain), + }) + const configStream = asStream(configStore) + pump( configStream, outStream, (err) => { + configStore.destroy() configStream.destroy() if (err) log.error(err) } @@ -1414,6 +1468,38 @@ module.exports = class MetamaskController extends EventEmitter { } /** + * A method for providing our public api over a stream. + * This includes a method for setting site metadata like title and image + * + * @param {*} outStream - The stream to provide the api over. + */ + setupPublicApi (outStream) { + const dnode = Dnode() + // connect dnode api to remote connection + pump( + outStream, + dnode, + outStream, + (err) => { + // report any error + if (err) log.error(err) + } + ) + + const getRemote = createDnodeRemoteGetter(dnode) + + const publicApi = { + // wrap with an await remote + getSiteMetadata: async () => { + const remote = await getRemote() + return await pify(remote.getSiteMetadata)() + }, + } + + return publicApi + } + + /** * Handle a KeyringController update * @param {object} state the KC state * @return {Promise<void>} @@ -1545,7 +1631,7 @@ module.exports = class MetamaskController extends EventEmitter { * @property {string} depositType - An abbreviation of the type of crypto currency to be deposited. */ createShapeShiftTx (depositAddress, depositType) { - this.shapeshiftController.createShapeShiftTx(depositAddress, depositType) + this.shapeshiftController.createTransaction(depositAddress, depositType) } // network @@ -1558,9 +1644,9 @@ module.exports = class MetamaskController extends EventEmitter { * @returns {Promise<String>} - The RPC Target URL confirmed. */ - async updateAndSetCustomRpc (rpcUrl, chainId, ticker = 'ETH', nickname) { - await this.preferencesController.updateRpc({ rpcUrl, chainId, ticker, nickname }) - this.networkController.setRpcTarget(rpcUrl, chainId, ticker, nickname) + async updateAndSetCustomRpc (rpcUrl, chainId, ticker = 'ETH', nickname, rpcPrefs) { + await this.preferencesController.updateRpc({ rpcUrl, chainId, ticker, nickname, rpcPrefs }) + this.networkController.setRpcTarget(rpcUrl, chainId, ticker, nickname, rpcPrefs) return rpcUrl } @@ -1573,15 +1659,15 @@ module.exports = class MetamaskController extends EventEmitter { * @param {string} nickname - Optional nickname of the selected network. * @returns {Promise<String>} - The RPC Target URL confirmed. */ - async setCustomRpc (rpcTarget, chainId, ticker = 'ETH', nickname = '') { + async setCustomRpc (rpcTarget, chainId, ticker = 'ETH', nickname = '', rpcPrefs = {}) { const frequentRpcListDetail = this.preferencesController.getFrequentRpcListDetail() const rpcSettings = frequentRpcListDetail.find((rpc) => rpcTarget === rpc.rpcUrl) if (rpcSettings) { - this.networkController.setRpcTarget(rpcSettings.rpcUrl, rpcSettings.chainId, rpcSettings.ticker, rpcSettings.nickname) + this.networkController.setRpcTarget(rpcSettings.rpcUrl, rpcSettings.chainId, rpcSettings.ticker, rpcSettings.nickname, rpcPrefs) } else { - this.networkController.setRpcTarget(rpcTarget, chainId, ticker, nickname) - await this.preferencesController.addToFrequentRpcList(rpcTarget, chainId, ticker, nickname) + this.networkController.setRpcTarget(rpcTarget, chainId, ticker, nickname, rpcPrefs) + await this.preferencesController.addToFrequentRpcList(rpcTarget, chainId, ticker, nickname, rpcPrefs) } return rpcTarget } @@ -1706,18 +1792,17 @@ module.exports = class MetamaskController extends EventEmitter { */ /** - * Adds a domain to the {@link BlacklistController} whitelist + * Adds a domain to the PhishingController whitelist * @param {string} hostname the domain to whitelist */ whitelistPhishingDomain (hostname) { - return this.blacklistController.whitelistDomain(hostname) + return this.phishingController.bypass(hostname) } /** * Locks MetaMask */ setLocked () { - this.providerApprovalController.setLocked() return this.keyringController.setLocked() } } diff --git a/app/scripts/migrations/024.js b/app/scripts/migrations/024.js index d0b276a79..6239bab13 100644 --- a/app/scripts/migrations/024.js +++ b/app/scripts/migrations/024.js @@ -27,7 +27,7 @@ function transformState (state) { const newState = state if (!newState.TransactionController) return newState const transactions = newState.TransactionController.transactions - newState.TransactionController.transactions = transactions.map((txMeta, _, txList) => { + newState.TransactionController.transactions = transactions.map((txMeta, _) => { if ( txMeta.status === 'unapproved' && txMeta.txParams && diff --git a/app/scripts/migrations/025.js b/app/scripts/migrations/025.js index fc3b20a44..fd4faa782 100644 --- a/app/scripts/migrations/025.js +++ b/app/scripts/migrations/025.js @@ -43,7 +43,7 @@ function normalizeTxParams (txParams) { // functions that handle normalizing of that key in txParams const whiteList = { from: from => ethUtil.addHexPrefix(from).toLowerCase(), - to: to => ethUtil.addHexPrefix(txParams.to).toLowerCase(), + to: () => ethUtil.addHexPrefix(txParams.to).toLowerCase(), nonce: nonce => ethUtil.addHexPrefix(nonce), value: value => ethUtil.addHexPrefix(value), data: data => ethUtil.addHexPrefix(data), diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index 099b0d7ea..0c2d222b8 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -60,20 +60,6 @@ class ExtensionPlatform { } } - addMessageListener (cb) { - extension.runtime.onMessage.addListener(cb) - } - - sendMessage (message, query = {}) { - const id = query.id - delete query.id - extension.tabs.query({ ...query }, tabs => { - tabs.forEach(tab => { - extension.tabs.sendMessage(id || tab.id, message) - }) - }) - } - _showConfirmedTransaction (txMeta) { this._subscribeToNotificationClicked() diff --git a/development/backGroundConnectionModifiers.js b/development/backGroundConnectionModifiers.js index aee68854b..cf1a723d0 100644 --- a/development/backGroundConnectionModifiers.js +++ b/development/backGroundConnectionModifiers.js @@ -1,20 +1,20 @@ module.exports = { 'confirm sig requests': { - signMessage: (msgData, cb) => { + signMessage: (_, cb) => { const stateUpdate = { unapprovedMsgs: {}, unapprovedMsgCount: 0, } return cb(null, stateUpdate) }, - signPersonalMessage: (msgData, cb) => { + signPersonalMessage: (_, cb) => { const stateUpdate = { unapprovedPersonalMsgs: {}, unapprovedPersonalMsgCount: 0, } return cb(null, stateUpdate) }, - signTypedMessage: (msgData, cb) => { + signTypedMessage: (_, cb) => { const stateUpdate = { unapprovedTypedMessages: {}, unapprovedTypedMessagesCount: 0, diff --git a/development/states/conf-tx.json b/development/states/conf-tx.json index d47b26fd4..7b278f331 100644 --- a/development/states/conf-tx.json +++ b/development/states/conf-tx.json @@ -192,7 +192,8 @@ "type": "testnet" }, "shapeShiftTxList": [], - "lostAccounts": [] + "lostAccounts": [], + "frequentRpcListDetail": [] }, "appState": { "menuOpen": false, diff --git a/development/states/confirm-new-ui.json b/development/states/confirm-new-ui.json index 4310ed5b7..c9340fc8f 100644 --- a/development/states/confirm-new-ui.json +++ b/development/states/confirm-new-ui.json @@ -133,7 +133,9 @@ "preferences": { "useNativeCurrencyAsPrimaryCurrency": true, "showFiatInTestnets": true - } + }, + "completedUiMigration": true, + "frequentRpcListDetail": [] }, "appState": { "menuOpen": false, diff --git a/development/states/confirm-sig-requests.json b/development/states/confirm-sig-requests.json index aa3e8dfdf..d531b2ef7 100644 --- a/development/states/confirm-sig-requests.json +++ b/development/states/confirm-sig-requests.json @@ -156,7 +156,9 @@ "currentLocale": "en", "preferences": { "useNativeCurrencyAsPrimaryCurrency": true - } + }, + "completedUiMigration": true, + "frequentRpcListDetail": [] }, "appState": { "menuOpen": false, diff --git a/development/states/currency-localization.json b/development/states/currency-localization.json index 8288b3020..a9a37ecd0 100644 --- a/development/states/currency-localization.json +++ b/development/states/currency-localization.json @@ -115,7 +115,9 @@ "preferences": { "useNativeCurrencyAsPrimaryCurrency": true, "showFiatInTestnets": true - } + }, + "completedUiMigration": true, + "frequentRpcListDetail": [] }, "appState": { "menuOpen": false, diff --git a/development/states/send-edit.json b/development/states/send-edit.json index fda7d1a31..7c7e8f097 100644 --- a/development/states/send-edit.json +++ b/development/states/send-edit.json @@ -137,7 +137,9 @@ "preferences": { "useNativeCurrencyAsPrimaryCurrency": true, "showFiatInTestnets": true - } + }, + "completedUiMigration": true, + "frequentRpcListDetail": [] }, "appState": { "menuOpen": false, diff --git a/development/states/send-new-ui.json b/development/states/send-new-ui.json index b8a3ff128..75982f318 100644 --- a/development/states/send-new-ui.json +++ b/development/states/send-new-ui.json @@ -116,7 +116,9 @@ "preferences": { "useNativeCurrencyAsPrimaryCurrency": true, "showFiatInTestnets": true - } + }, + "completedUiMigration": true, + "frequentRpcListDetail": [] }, "appState": { "menuOpen": false, diff --git a/development/states/send.json b/development/states/send.json index 8ae385564..c71516edc 100644 --- a/development/states/send.json +++ b/development/states/send.json @@ -87,7 +87,8 @@ "type": "testnet" }, "shapeShiftTxList": [], - "lostAccounts": [] + "lostAccounts": [], + "frequentRpcListDetail": [] }, "appState": { "menuOpen": false, diff --git a/development/states/tx-list-items.json b/development/states/tx-list-items.json index d4e3f3860..4190ee149 100644 --- a/development/states/tx-list-items.json +++ b/development/states/tx-list-items.json @@ -1058,7 +1058,9 @@ "currentLocale": "en", "preferences": { "useNativeCurrencyAsPrimaryCurrency": true - } + }, + "completedUiMigration": true, + "frequentRpcListDetail": [] }, "appState": { "menuOpen": false, diff --git a/gentests.js b/gentests.js index 9c591e98c..a84c2079d 100644 --- a/gentests.js +++ b/gentests.js @@ -48,11 +48,11 @@ async function start (fileRegEx, testGenerator) { } */ -async function startContainer (fileRegEx, testGenerator) { +async function startContainer (fileRegEx) { const fileNames = await getAllFileNames('./ui/app') const sFiles = fileNames.filter(name => name.match(fileRegEx)) - async.each(sFiles, async (sFile, cb) => { + async.each(sFiles, async (sFile) => { console.log(`sFile`, sFile) const [, sRootPath, sPath] = sFile.match(/^(.+\/)([^/]+)$/) @@ -91,7 +91,7 @@ async function startContainer (fileRegEx, testGenerator) { const proxyquireObject = ('{\n ' + result .match(/import\s{[\s\S]+?}\sfrom\s.+/g) .map(s => s.replace(/\n/g, '')) - .map((s, i) => { + .map((s) => { const proxyKeys = s.match(/{.+}/)[0].match(/\w+/g) return '\'' + s.match(/'(.+)'/)[1] + '\': { ' + (proxyKeys.length > 1 ? '\n ' + proxyKeys.join(': () => {},\n ') + ': () => {},\n ' diff --git a/gulpfile.js b/gulpfile.js index caddb620a..35c6331e8 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -315,7 +315,7 @@ createTasksForBuildJsExtension({ buildJsFiles, taskPrefix: 'dev:test-extension:j createTasksForBuildJsExtension({ buildJsFiles, taskPrefix: 'build:extension:js' }) createTasksForBuildJsExtension({ buildJsFiles, taskPrefix: 'build:test:extension:js', testing: 'true' }) -function createTasksForBuildJsUIDeps ({ dependenciesToBundle, filename }) { +function createTasksForBuildJsUIDeps ({ filename }) { const destinations = browserPlatforms.map(platform => `./dist/${platform}`) diff --git a/package-lock.json b/package-lock.json index 902dbe004..8844b21a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1746,6 +1746,16 @@ } } }, + "@types/invariant": { + "version": "2.2.29", + "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.29.tgz", + "integrity": "sha512-lRVw09gOvgviOfeUrKc/pmTiRZ7g7oDOU6OAutyuSHpm1/o2RaBQvRhgK8QEdu+FFuw/wnWb29A/iuxv9i8OpQ==" + }, + "@types/lodash": { + "version": "4.14.124", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.124.tgz", + "integrity": "sha512-6bKEUVbHJ8z34jisA7lseJZD2g31SIvee3cGX2KEZCS4XXWNbjPZpmO1/2rGNR9BhGtaYr6iYXPl1EzRrDAFTA==" + }, "@types/node": { "version": "8.5.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-8.5.5.tgz", @@ -1768,6 +1778,14 @@ "@types/react": "*" } }, + "@types/redux": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@types/redux/-/redux-3.6.0.tgz", + "integrity": "sha1-8evh5UEVGAcuT9/KXHbhbnTBOZo=", + "requires": { + "redux": "*" + } + }, "@yarnpkg/lockfile": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", @@ -8281,6 +8299,55 @@ } } }, + "disposables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/disposables/-/disposables-1.0.2.tgz", + "integrity": "sha1-NsamdEdfVaLWkTVnpgFETkh7S24=" + }, + "dnd-core": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-3.0.2.tgz", + "integrity": "sha1-6UdXdiBTHH7jelGM1d3hfQ798PM=", + "requires": { + "@types/invariant": "^2.2.29", + "@types/lodash": "^4.14.107", + "@types/node": "^8.10.11", + "@types/redux": "^3.6.0", + "asap": "^2.0.6", + "invariant": "^2.0.0", + "lodash": "^4.2.0", + "redux": "^4.0.0" + }, + "dependencies": { + "@types/node": { + "version": "8.10.48", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.48.tgz", + "integrity": "sha512-c35YEBTkL4rzXY2ucpSKy+UYHjUBIIkuJbWYbsGIrKLEWU5dgJMmLkkIb3qeC3O3Tpb1ZQCwecscvJTDjDjkRw==" + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "redux": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.1.tgz", + "integrity": "sha512-R7bAtSkk7nY6O/OYMVR9RiBI+XghjF9rlbl5806HJbQph0LJVHZrU5oaO4q70eUKiqMRqm4y07KLTlMZ2BlVmg==", + "requires": { + "loose-envify": "^1.4.0", + "symbol-observable": "^1.2.0" + } + }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + } + } + }, "dnode": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/dnode/-/dnode-1.2.2.tgz", @@ -9759,8 +9826,8 @@ } }, "eth-contract-metadata": { - "version": "github:MetaMask/eth-contract-metadata#41a14e8004bdd37eaba5af5f2bb1fc4f4ff7063f", - "from": "github:MetaMask/eth-contract-metadata#41a14e8004bdd37eaba5af5f2bb1fc4f4ff7063f" + "version": "github:MetaMask/eth-contract-metadata#dc68506221859bc90792bc5e0279a6835f2484d8", + "from": "github:MetaMask/eth-contract-metadata#dc68506221859bc90792bc5e0279a6835f2484d8" }, "eth-ens-namehash": { "version": "2.0.8", @@ -9796,12 +9863,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#d84a96796079c8595a0c78accd1e7709f2277215", + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#8431eab7b3384e65e8126a4602520b78031666fb", "ethereumjs-util": "^5.1.1" }, "dependencies": { "ethereumjs-abi": { - "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#d84a96796079c8595a0c78accd1e7709f2277215", + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#8431eab7b3384e65e8126a4602520b78031666fb", "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "requires": { "bn.js": "^4.11.8", @@ -9859,9 +9926,9 @@ } }, "eth-json-rpc-filters": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/eth-json-rpc-filters/-/eth-json-rpc-filters-3.0.1.tgz", - "integrity": "sha512-F/UbtD47UnZDFILYP5GJLklYQ7witEI9TdCLgw0r4iag8ZLzz5h4Q+9odg2ASVZKkm8E50mrb7PaYCK0thVxfw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/eth-json-rpc-filters/-/eth-json-rpc-filters-3.0.3.tgz", + "integrity": "sha512-rX1EbEmRexMfbzntEUemevRM5qfpjschS/dsSqHYXyWnfAGVOegdSxLbLumiKpRBPMMTnQv6B9l6d/lGneXcaw==", "requires": { "await-semaphore": "^0.1.3", "eth-json-rpc-middleware": "^2.6.0", @@ -9910,50 +9977,32 @@ "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#d84a96796079c8595a0c78accd1e7709f2277215", + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#8431eab7b3384e65e8126a4602520b78031666fb", "ethereumjs-util": "^5.1.1" - }, - "dependencies": { - "ethereumjs-abi": { - "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#d84a96796079c8595a0c78accd1e7709f2277215", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", - "requires": { - "bn.js": "^4.11.8", - "ethereumjs-util": "^6.0.0" - }, - "dependencies": { - "ethereumjs-util": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.1.0.tgz", - "integrity": "sha512-URESKMFbDeJxnAxPppnk2fN6Y3BIatn9fwn76Lm8bQlt+s52TpG8dN9M66MLPuRAiAOIqL3dfwqWJf0sd0fL0Q==", - "requires": { - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "ethjs-util": "0.1.6", - "keccak": "^1.0.2", - "rlp": "^2.0.0", - "safe-buffer": "^5.1.1", - "secp256k1": "^3.0.1" - } - } - } - }, - "ethjs-util": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.6.tgz", - "integrity": "sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==", - "requires": { - "is-hex-prefixed": "1.0.0", - "strip-hex-prefix": "1.0.0" - } - } } }, "ethereumjs-abi": { - "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#d84a96796079c8595a0c78accd1e7709f2277215", + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#8431eab7b3384e65e8126a4602520b78031666fb", "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "requires": { - "bn.js": "^4.11.8" + "bn.js": "^4.11.8", + "ethereumjs-util": "^6.0.0" + }, + "dependencies": { + "ethereumjs-util": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.1.0.tgz", + "integrity": "sha512-URESKMFbDeJxnAxPppnk2fN6Y3BIatn9fwn76Lm8bQlt+s52TpG8dN9M66MLPuRAiAOIqL3dfwqWJf0sd0fL0Q==", + "requires": { + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "ethjs-util": "0.1.6", + "keccak": "^1.0.2", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1", + "secp256k1": "^3.0.1" + } + } } }, "ethereumjs-util": { @@ -9989,6 +10038,15 @@ "promise-to-callback": "^1.0.0" } }, + "ethjs-util": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.6.tgz", + "integrity": "sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==", + "requires": { + "is-hex-prefixed": "1.0.0", + "strip-hex-prefix": "1.0.0" + } + }, "json-rpc-engine": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/json-rpc-engine/-/json-rpc-engine-3.8.0.tgz", @@ -10113,12 +10171,12 @@ "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", "dev": true, "requires": { - "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#d84a96796079c8595a0c78accd1e7709f2277215", + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#8431eab7b3384e65e8126a4602520b78031666fb", "ethereumjs-util": "^5.1.1" }, "dependencies": { "ethereumjs-abi": { - "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#d84a96796079c8595a0c78accd1e7709f2277215", + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#8431eab7b3384e65e8126a4602520b78031666fb", "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "dev": true, "requires": { @@ -10249,16 +10307,41 @@ "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#2863c40e0982acfc0b7163f0285d4c56427c7799", + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#8431eab7b3384e65e8126a4602520b78031666fb", "ethereumjs-util": "^5.1.1" }, "dependencies": { "ethereumjs-abi": { - "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#8431eab7b3384e65e8126a4602520b78031666fb", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", + "requires": { + "bn.js": "^4.11.8", + "ethereumjs-util": "^6.0.0" + }, + "dependencies": { + "ethereumjs-util": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.1.0.tgz", + "integrity": "sha512-URESKMFbDeJxnAxPppnk2fN6Y3BIatn9fwn76Lm8bQlt+s52TpG8dN9M66MLPuRAiAOIqL3dfwqWJf0sd0fL0Q==", + "requires": { + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "ethjs-util": "0.1.6", + "keccak": "^1.0.2", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1", + "secp256k1": "^3.0.1" + } + } + } + }, + "ethjs-util": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.6.tgz", + "integrity": "sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==", "requires": { - "bn.js": "^4.10.0", - "ethereumjs-util": "^5.0.0" + "is-hex-prefixed": "1.0.0", + "strip-hex-prefix": "1.0.0" } } } @@ -10315,8 +10398,34 @@ "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#572d4bafe08a8a231137e1f9daeb0f8a23f197d2", + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#8431eab7b3384e65e8126a4602520b78031666fb", "ethereumjs-util": "^5.1.1" + }, + "dependencies": { + "ethereumjs-abi": { + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#8431eab7b3384e65e8126a4602520b78031666fb", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", + "requires": { + "bn.js": "^4.11.8", + "ethereumjs-util": "^6.0.0" + }, + "dependencies": { + "ethereumjs-util": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.1.0.tgz", + "integrity": "sha512-URESKMFbDeJxnAxPppnk2fN6Y3BIatn9fwn76Lm8bQlt+s52TpG8dN9M66MLPuRAiAOIqL3dfwqWJf0sd0fL0Q==", + "requires": { + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "ethjs-util": "0.1.6", + "keccak": "^1.0.2", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1", + "secp256k1": "^3.0.1" + } + } + } + } } }, "ethereum-common": { @@ -10558,16 +10667,41 @@ "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#2863c40e0982acfc0b7163f0285d4c56427c7799", + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#8431eab7b3384e65e8126a4602520b78031666fb", "ethereumjs-util": "^5.1.1" }, "dependencies": { "ethereumjs-abi": { - "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", - "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799", + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#8431eab7b3384e65e8126a4602520b78031666fb", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "requires": { - "bn.js": "^4.10.0", - "ethereumjs-util": "^5.0.0" + "bn.js": "^4.11.8", + "ethereumjs-util": "^6.0.0" + }, + "dependencies": { + "ethereumjs-util": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.1.0.tgz", + "integrity": "sha512-URESKMFbDeJxnAxPppnk2fN6Y3BIatn9fwn76Lm8bQlt+s52TpG8dN9M66MLPuRAiAOIqL3dfwqWJf0sd0fL0Q==", + "requires": { + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "ethjs-util": "0.1.6", + "keccak": "^1.0.2", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1", + "secp256k1": "^3.0.1" + } + } + } + }, + "ethjs-util": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.6.tgz", + "integrity": "sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==", + "requires": { + "is-hex-prefixed": "1.0.0", + "strip-hex-prefix": "1.0.0" } } } @@ -10766,8 +10900,34 @@ "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#572d4bafe08a8a231137e1f9daeb0f8a23f197d2", + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#8431eab7b3384e65e8126a4602520b78031666fb", "ethereumjs-util": "^5.1.1" + }, + "dependencies": { + "ethereumjs-abi": { + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#8431eab7b3384e65e8126a4602520b78031666fb", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", + "requires": { + "bn.js": "^4.11.8", + "ethereumjs-util": "^6.0.0" + }, + "dependencies": { + "ethereumjs-util": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.1.0.tgz", + "integrity": "sha512-URESKMFbDeJxnAxPppnk2fN6Y3BIatn9fwn76Lm8bQlt+s52TpG8dN9M66MLPuRAiAOIqL3dfwqWJf0sd0fL0Q==", + "requires": { + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "ethjs-util": "0.1.6", + "keccak": "^1.0.2", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1", + "secp256k1": "^3.0.1" + } + } + } + } } }, "ethereum-common": { @@ -11045,8 +11205,76 @@ "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#572d4bafe08a8a231137e1f9daeb0f8a23f197d2", + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#8431eab7b3384e65e8126a4602520b78031666fb", "ethereumjs-util": "^5.1.1" + }, + "dependencies": { + "ethereumjs-abi": { + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#8431eab7b3384e65e8126a4602520b78031666fb", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", + "requires": { + "bn.js": "^4.11.8", + "ethereumjs-util": "^6.0.0" + }, + "dependencies": { + "ethereumjs-util": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.1.0.tgz", + "integrity": "sha512-URESKMFbDeJxnAxPppnk2fN6Y3BIatn9fwn76Lm8bQlt+s52TpG8dN9M66MLPuRAiAOIqL3dfwqWJf0sd0fL0Q==", + "requires": { + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "ethjs-util": "0.1.6", + "keccak": "^1.0.2", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1", + "secp256k1": "^3.0.1" + } + } + } + }, + "ethjs-util": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.6.tgz", + "integrity": "sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==", + "requires": { + "is-hex-prefixed": "1.0.0", + "strip-hex-prefix": "1.0.0" + } + } + } + }, + "ethereumjs-abi": { + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#572d4bafe08a8a231137e1f9daeb0f8a23f197d2", + "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#572d4bafe08a8a231137e1f9daeb0f8a23f197d2", + "requires": { + "bn.js": "^4.11.8", + "ethereumjs-util": "^6.0.0" + }, + "dependencies": { + "ethereumjs-util": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.1.0.tgz", + "integrity": "sha512-URESKMFbDeJxnAxPppnk2fN6Y3BIatn9fwn76Lm8bQlt+s52TpG8dN9M66MLPuRAiAOIqL3dfwqWJf0sd0fL0Q==", + "requires": { + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "ethjs-util": "0.1.6", + "keccak": "^1.0.2", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1", + "secp256k1": "^3.0.1" + } + }, + "ethjs-util": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.6.tgz", + "integrity": "sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==", + "requires": { + "is-hex-prefixed": "1.0.0", + "strip-hex-prefix": "1.0.0" + } + } } }, "ethereumjs-util": { @@ -13098,15 +13326,16 @@ "integrity": "sha1-8ESOgGmFW/Kj5oPNwdMg5+KgfvQ=" }, "gaba": { - "version": "1.0.0-beta.65", - "resolved": "https://registry.npmjs.org/gaba/-/gaba-1.0.0-beta.65.tgz", - "integrity": "sha512-pX9hMd4RR5AXe7bwIamQEXLJe26fNvjOf7PjkHGKlRjKzBYmxZ03Y/Pa9nklNlG2Shc9sSgB6GXZpYlXNlJRIg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gaba/-/gaba-1.0.1.tgz", + "integrity": "sha512-67Zoaq6wnaBASIXGfu2L+jzx8m+l1tfn6FAEIZI/pMvn/ymk4V9raeqz73QQKq1fF4WcRy2H1Ru1r45J1tDQoQ==", "dev": true, "requires": { "await-semaphore": "^0.1.3", "eth-contract-metadata": "github:MetaMask/eth-contract-metadata#faa4f56fb17b3ae8579df68708be59d617732f31", "eth-json-rpc-infura": "^3.1.2", "eth-keyring-controller": "^4.0.0", + "eth-method-registry": "1.1.0", "eth-phishing-detect": "^1.1.13", "eth-query": "^2.1.2", "eth-sig-util": "^2.1.0", @@ -13183,12 +13412,12 @@ "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", "dev": true, "requires": { - "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#572d4bafe08a8a231137e1f9daeb0f8a23f197d2", + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#8431eab7b3384e65e8126a4602520b78031666fb", "ethereumjs-util": "^5.1.1" } }, "ethereumjs-abi": { - "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#572d4bafe08a8a231137e1f9daeb0f8a23f197d2", + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#8431eab7b3384e65e8126a4602520b78031666fb", "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "dev": true, "requires": { @@ -13225,6 +13454,15 @@ } } }, + "eth-method-registry": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eth-method-registry/-/eth-method-registry-1.1.0.tgz", + "integrity": "sha512-jGbbGYd19XJCtoGFtUD2qJYWefKCCbFcu7F/AQ5sJXvqTIVAHnFn3paaV2zhN5t7iyKYp1qxc+ugOky+72xcbg==", + "dev": true, + "requires": { + "ethjs": "^0.3.0" + } + }, "eth-phishing-detect": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/eth-phishing-detect/-/eth-phishing-detect-1.1.13.tgz", @@ -13277,6 +13515,119 @@ "secp256k1": "^3.0.1" } }, + "ethjs": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/ethjs/-/ethjs-0.3.9.tgz", + "integrity": "sha512-gOQzA3tDUjoLpNONSOALJ/rUFtHi5tXl2mholHasF1cvXhoddqi06yU4OJFJu9AGd6n9v9ywzHlYeIKg1t1hdw==", + "dev": true, + "requires": { + "bn.js": "4.11.6", + "ethjs-abi": "0.2.1", + "ethjs-contract": "0.2.2", + "ethjs-filter": "0.1.8", + "ethjs-provider-http": "0.1.6", + "ethjs-query": "0.3.7", + "ethjs-unit": "0.1.6", + "ethjs-util": "0.1.3", + "js-sha3": "0.5.5", + "number-to-bn": "1.7.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha1-UzRK2xRhehP26N0s4okF0cC6MhU=", + "dev": true + }, + "ethjs-query": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/ethjs-query/-/ethjs-query-0.3.7.tgz", + "integrity": "sha512-TZnKUwfkWjy0SowFdPLtmsytCorHi0i4vvkQn7Jg8rZt33cRzKhuzOwKr/G3vdigCc+ePXOhUGMcJSAPlOG44A==", + "dev": true, + "requires": { + "ethjs-format": "0.2.7", + "ethjs-rpc": "0.2.0", + "promise-to-callback": "^1.0.0" + } + }, + "ethjs-util": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.3.tgz", + "integrity": "sha1-39XqSkANxeQhqInK9H4IGtp4u1U=", + "dev": true, + "requires": { + "is-hex-prefixed": "1.0.0", + "strip-hex-prefix": "1.0.0" + } + } + } + }, + "ethjs-abi": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ethjs-abi/-/ethjs-abi-0.2.1.tgz", + "integrity": "sha1-4KepOn6BFjqUR3utVu3lJKtt5TM=", + "dev": true, + "requires": { + "bn.js": "4.11.6", + "js-sha3": "0.5.5", + "number-to-bn": "1.7.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha1-UzRK2xRhehP26N0s4okF0cC6MhU=", + "dev": true + } + } + }, + "ethjs-contract": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ethjs-contract/-/ethjs-contract-0.2.2.tgz", + "integrity": "sha512-xxPqEjsULQ/QNWuvX6Ako0PGs5RxALA8N/H3+boLvnaXDFZVGpD7H63H1gBCRTZyYqCldPpVlVHuw/rD45vazw==", + "dev": true, + "requires": { + "ethjs-abi": "0.2.0", + "ethjs-filter": "0.1.8", + "ethjs-util": "0.1.3", + "js-sha3": "0.5.5" + }, + "dependencies": { + "bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha1-UzRK2xRhehP26N0s4okF0cC6MhU=", + "dev": true + }, + "ethjs-abi": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ethjs-abi/-/ethjs-abi-0.2.0.tgz", + "integrity": "sha1-0+LCIQEVIPxJm3FoIDbBT8wvWyU=", + "dev": true, + "requires": { + "bn.js": "4.11.6", + "js-sha3": "0.5.5", + "number-to-bn": "1.7.0" + } + }, + "ethjs-util": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.3.tgz", + "integrity": "sha1-39XqSkANxeQhqInK9H4IGtp4u1U=", + "dev": true, + "requires": { + "is-hex-prefixed": "1.0.0", + "strip-hex-prefix": "1.0.0" + } + } + } + }, + "ethjs-filter": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/ethjs-filter/-/ethjs-filter-0.1.8.tgz", + "integrity": "sha512-qTDPskDL2UadHwjvM8A+WG9HwM4/FoSY3p3rMJORkHltYcAuiQZd2otzOYKcL5w2Q3sbAkW/E3yt/FPFL/AVXA==", + "dev": true + }, "ethjs-query": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/ethjs-query/-/ethjs-query-0.3.8.tgz", @@ -13298,6 +13649,12 @@ "promise-to-callback": "^1.0.0" } }, + "js-sha3": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.5.tgz", + "integrity": "sha1-uvDA6MVK1ZA0R9+Wreekobynmko=", + "dev": true + }, "obs-store": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/obs-store/-/obs-store-2.4.1.tgz", @@ -19504,6 +19861,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, + "optional": true, "requires": { "ms": "2.0.0" } @@ -29849,16 +30207,6 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" }, - "ping-pong-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ping-pong-stream/-/ping-pong-stream-1.0.0.tgz", - "integrity": "sha1-TF6wm6atsCGInawNyr+45XcGhUo=", - "requires": { - "end-of-stream": "^1.1.0", - "readable-stream": "^2.1.5", - "tape": "^4.6.2" - } - }, "pinkie": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", @@ -32546,6 +32894,84 @@ } } }, + "react-dnd": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-3.0.2.tgz", + "integrity": "sha1-sMI9jYKWn1t740y8T4T6H/xcfdw=", + "requires": { + "disposables": "^1.0.1", + "dnd-core": "^3.0.2", + "hoist-non-react-statics": "^2.5.0", + "invariant": "^2.1.0", + "lodash": "^4.2.0", + "prop-types": "^15.5.10", + "shallowequal": "^1.0.2" + }, + "dependencies": { + "hoist-non-react-statics": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz", + "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==" + }, + "shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + } + } + }, + "react-dnd-html5-backend": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-7.4.4.tgz", + "integrity": "sha512-X/lP92ateY0glHan8mU0JzjBuZL6VHv2Gc/H9OBBxaf/ZCN1oC16MLKdesqG4x1f/NWFTNtuG3W4B99r5gPVog==", + "requires": { + "dnd-core": "^7.4.4" + }, + "dependencies": { + "dnd-core": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-7.4.4.tgz", + "integrity": "sha512-xR8SINDCJG9AmKSjXUMJ1PEl8ih1+xSHH8x4DgBtzScXnEtpCnV1ibDZNV0uyps9VgkXTTbYYzJdF04y0v0e3Q==", + "requires": { + "asap": "^2.0.6", + "invariant": "^2.2.4", + "redux": "^4.0.1" + } + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "requires": { + "loose-envify": "^1.0.0" + } + }, + "redux": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.1.tgz", + "integrity": "sha512-R7bAtSkk7nY6O/OYMVR9RiBI+XghjF9rlbl5806HJbQph0LJVHZrU5oaO4q70eUKiqMRqm4y07KLTlMZ2BlVmg==", + "requires": { + "loose-envify": "^1.4.0", + "symbol-observable": "^1.2.0" + }, + "dependencies": { + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + } + } + }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + } + } + }, "react-docgen": { "version": "3.0.0-beta9", "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-3.0.0-beta9.tgz", @@ -32666,6 +33092,11 @@ "react-icon-base": "2.1.0" } }, + "react-idle-timer": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/react-idle-timer/-/react-idle-timer-4.2.5.tgz", + "integrity": "sha512-8B/OwjG8E/DTx1fHYKTpZ4cnCbL9+LOc5I9t8SYe8tbEkP14KChiYg0xPIuyRpO33wUZHcgmQl93CVePaDhmRA==" + }, "react-input-autosize": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.1.2.tgz", @@ -38915,12 +39346,12 @@ "integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=", "dev": true, "requires": { - "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#d84a96796079c8595a0c78accd1e7709f2277215", + "ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#8431eab7b3384e65e8126a4602520b78031666fb", "ethereumjs-util": "^5.1.1" } }, "ethereumjs-abi": { - "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#d84a96796079c8595a0c78accd1e7709f2277215", + "version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#8431eab7b3384e65e8126a4602520b78031666fb", "from": "git+https://github.com/ethereumjs/ethereumjs-abi.git", "dev": true, "requires": { diff --git a/package.json b/package.json index 2a02954d2..0ae402600 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "start:test": "gulp dev:test", "build:test": "gulp build:test", "test": "npm run test:unit && npm run test:integration && npm run lint", + "dapp": "static-server test/e2e/beta/contract-test --port 8080", + "dapp-chain": "shell-parallel -s 'npm run ganache:start -- -b 2' -x 'sleep 5 && static-server test/e2e/beta/contract-test --port 8080'", "watch:test:unit": "nodemon --exec \"npm run test:unit\" ./test ./app ./ui", "test:unit": "cross-env METAMASK_ENV=test mocha --exit --require test/setup.js --recursive \"test/unit/**/*.js\" \"ui/app/**/*.test.js\"", "test:single": "cross-env METAMASK_ENV=test mocha --require test/helper.js", @@ -84,10 +86,10 @@ "ensnare": "^1.0.0", "eth-bin-to-ops": "^1.0.1", "eth-block-tracker": "^4.1.0", - "eth-contract-metadata": "github:MetaMask/eth-contract-metadata#41a14e8004bdd37eaba5af5f2bb1fc4f4ff7063f", + "eth-contract-metadata": "github:MetaMask/eth-contract-metadata#dc68506221859bc90792bc5e0279a6835f2484d8", "eth-ens-namehash": "^2.0.8", "eth-hd-keyring": "^1.2.2", - "eth-json-rpc-filters": "^3.0.1", + "eth-json-rpc-filters": "^3.0.3", "eth-json-rpc-infura": "^3.0.0", "eth-keyring-controller": "^3.3.1", "eth-ledger-bridge-keyring": "^0.2.0", @@ -136,7 +138,6 @@ "obs-store": "^3.0.2", "percentile": "^1.2.0", "pify": "^3.0.0", - "ping-pong-stream": "^1.0.0", "pojo-migrator": "^2.1.0", "polyfill-crypto.getrandomvalues": "^1.0.0", "post-message-stream": "^3.0.0", @@ -150,8 +151,11 @@ "ramda": "^0.24.1", "react": "^15.6.2", "react-addons-css-transition-group": "^15.6.0", + "react-dnd": "^3.0.2", + "react-dnd-html5-backend": "^7.4.4", "react-dom": "^15.6.2", "react-hyperscript": "^3.0.0", + "react-idle-timer": "^4.2.5", "react-inspector": "^2.3.0", "react-markdown": "^3.0.0", "react-media": "^1.8.0", @@ -224,7 +228,7 @@ "file-loader": "^1.1.11", "fs-extra": "^6.0.1", "fs-promise": "^2.0.3", - "gaba": "1.0.0-beta.65", + "gaba": "^1.0.1", "ganache-cli": "^6.1.0", "ganache-core": "^2.5.3", "geckodriver": "^1.14.1", diff --git a/test/e2e/beta/contract-test/contract.js b/test/e2e/beta/contract-test/contract.js index 65fb9377f..e1f886c58 100644 --- a/test/e2e/beta/contract-test/contract.js +++ b/test/e2e/beta/contract-test/contract.js @@ -37,8 +37,10 @@ web3.currentProvider.enable().then(() => { const createToken = document.getElementById('createToken') const transferTokens = document.getElementById('transferTokens') const approveTokens = document.getElementById('approveTokens') + const transferTokensWithoutGas = document.getElementById('transferTokensWithoutGas') + const approveTokensWithoutGas = document.getElementById('approveTokensWithoutGas') - deployButton.addEventListener('click', async function (event) { + deployButton.addEventListener('click', async function () { document.getElementById('contractStatus').innerHTML = 'Deploying' var piggybank = await piggybankContract.new( @@ -55,7 +57,7 @@ web3.currentProvider.enable().then(() => { document.getElementById('contractStatus').innerHTML = 'Deployed' - depositButton.addEventListener('click', function (event) { + depositButton.addEventListener('click', function () { document.getElementById('contractStatus').innerHTML = 'Deposit initiated' contract.deposit({ from: web3.eth.accounts[0], value: '0x3782dace9d900000' }, function (result) { console.log(result) @@ -63,7 +65,7 @@ web3.currentProvider.enable().then(() => { }) }) - withdrawButton.addEventListener('click', function (event) { + withdrawButton.addEventListener('click', function () { contract.withdraw('0xde0b6b3a7640000', { from: web3.eth.accounts[0] }, function (result) { console.log(result) document.getElementById('contractStatus').innerHTML = 'Withdrawn' @@ -75,7 +77,7 @@ web3.currentProvider.enable().then(() => { console.log(piggybank) }) - sendButton.addEventListener('click', function (event) { + sendButton.addEventListener('click', function () { web3.eth.sendTransaction({ from: web3.eth.accounts[0], to: '0x2f318C334780961FB129D2a6c30D0763d9a5C970', @@ -88,7 +90,7 @@ web3.currentProvider.enable().then(() => { }) - createToken.addEventListener('click', async function (event) { + createToken.addEventListener('click', async function () { var _initialAmount = 100 var _tokenName = 'TST' var _decimalUnits = 0 @@ -124,7 +126,7 @@ web3.currentProvider.enable().then(() => { }) }) - approveTokens.addEventListener('click', function (event) { + approveTokens.addEventListener('click', function () { contract.approve('0x2f318C334780961FB129D2a6c30D0763d9a5C970', '7', { from: web3.eth.accounts[0], to: contract.address, @@ -135,6 +137,29 @@ web3.currentProvider.enable().then(() => { console.log(result) }) }) + + transferTokensWithoutGas.addEventListener('click', function (event) { + console.log(`event`, event) + contract.transfer('0x2f318C334780961FB129D2a6c30D0763d9a5C970', '7', { + from: web3.eth.accounts[0], + to: contract.address, + data: '0xa9059cbb0000000000000000000000002f318C334780961FB129D2a6c30D0763d9a5C970000000000000000000000000000000000000000000000000000000000000000a', + gasPrice: '20000000000', + }, function (result) { + console.log('result', result) + }) + }) + + approveTokensWithoutGas.addEventListener('click', function () { + contract.approve('0x2f318C334780961FB129D2a6c30D0763d9a5C970', '7', { + from: web3.eth.accounts[0], + to: contract.address, + data: '0x095ea7b30000000000000000000000002f318C334780961FB129D2a6c30D0763d9a5C9700000000000000000000000000000000000000000000000000000000000000005', + gasPrice: '20000000000', + }, function (result) { + console.log(result) + }) + }) } }) diff --git a/test/e2e/beta/contract-test/index.html b/test/e2e/beta/contract-test/index.html index 0d422ef20..6e134dc36 100644 --- a/test/e2e/beta/contract-test/index.html +++ b/test/e2e/beta/contract-test/index.html @@ -27,6 +27,8 @@ <button id="createToken">Create Token</button> <button id="transferTokens">Transfer Tokens</button> <button id="approveTokens">Approve Tokens</button> + <button id="transferTokensWithoutGas">Transfer Tokens Without Gas</button> + <button id="approveTokensWithoutGas">Approve Tokens Without Gas</button> </div> </div> diff --git a/test/e2e/beta/metamask-beta-ui.spec.js b/test/e2e/beta/metamask-beta-ui.spec.js index 05ad84f24..06778ab99 100644 --- a/test/e2e/beta/metamask-beta-ui.spec.js +++ b/test/e2e/beta/metamask-beta-ui.spec.js @@ -812,10 +812,31 @@ describe('MetaMask', function () { await delay(regularDelayMs) const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-tab__gas-edit-row__input')) - await gasPriceInput.clear() + await gasPriceInput.sendKeys(Key.chord(Key.CONTROL, 'a')) + await delay(50) + + await gasPriceInput.sendKeys(Key.BACK_SPACE) + await delay(50) + await gasPriceInput.sendKeys(Key.BACK_SPACE) + await delay(50) await gasPriceInput.sendKeys('10') - await gasLimitInput.clear() + await delay(50) + await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'a')) + await delay(50) + await gasLimitInput.sendKeys(Key.BACK_SPACE) + await delay(50) + await gasLimitInput.sendKeys(Key.BACK_SPACE) + await delay(50) + await gasLimitInput.sendKeys(Key.BACK_SPACE) + await delay(50) + await gasLimitInput.sendKeys(Key.BACK_SPACE) + await delay(50) + await gasLimitInput.sendKeys(Key.BACK_SPACE) + await delay(50) await gasLimitInput.sendKeys('60001') + await delay(50) + await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'e')) + await delay(50) const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`)) await save.click() @@ -1175,7 +1196,7 @@ describe('MetaMask', function () { const transferTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Approve Tokens')]`)) await transferTokens.click() - await closeAllWindowHandlesExcept(driver, extension) + await closeAllWindowHandlesExcept(driver, [extension, dapp]) await driver.switchTo().window(extension) await delay(regularDelayMs) @@ -1228,21 +1249,31 @@ describe('MetaMask', function () { await delay(regularDelayMs) const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-tab__gas-edit-row__input')) - await gasPriceInput.clear() - await delay(tinyDelayMs) + await gasPriceInput.sendKeys(Key.chord(Key.CONTROL, 'a')) + await delay(50) + + await gasPriceInput.sendKeys(Key.BACK_SPACE) + await delay(50) + await gasPriceInput.sendKeys(Key.BACK_SPACE) + await delay(50) await gasPriceInput.sendKeys('10') - await delay(tinyDelayMs) - await gasLimitInput.clear() - await delay(tinyDelayMs) + await delay(50) await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'a')) - await gasLimitInput.sendKeys('60000') + await delay(50) + await gasLimitInput.sendKeys(Key.BACK_SPACE) + await delay(50) + await gasLimitInput.sendKeys(Key.BACK_SPACE) + await delay(50) + await gasLimitInput.sendKeys(Key.BACK_SPACE) + await delay(50) + await gasLimitInput.sendKeys(Key.BACK_SPACE) + await delay(50) + await gasLimitInput.sendKeys(Key.BACK_SPACE) + await delay(50) + await gasLimitInput.sendKeys('60001') + await delay(50) await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'e')) - - // Needed for different behaviour of input in different versions of firefox - const gasLimitInputValue = await gasLimitInput.getAttribute('value') - if (gasLimitInputValue === '600001') { - await gasLimitInput.sendKeys(Key.BACK_SPACE) - } + await delay(50) const save = await findElement(driver, By.css('.page-container__footer-button')) await save.click() @@ -1271,6 +1302,105 @@ describe('MetaMask', function () { }) }) + describe('Tranfers a custom token from dapp when no gas value is specified', () => { + it('transfers an already created token, without specifying gas', async () => { + const windowHandles = await driver.getAllWindowHandles() + const extension = windowHandles[0] + const dapp = await switchToWindowWithTitle(driver, 'E2E Test Dapp', windowHandles) + await closeAllWindowHandlesExcept(driver, [extension, dapp]) + await delay(regularDelayMs) + + await driver.switchTo().window(dapp) + + const transferTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Transfer Tokens Without Gas')]`)) + await transferTokens.click() + + await closeAllWindowHandlesExcept(driver, [extension, dapp]) + await driver.switchTo().window(extension) + await delay(regularDelayMs) + + await driver.wait(async () => { + const pendingTxes = await findElements(driver, By.css('.transaction-list__pending-transactions .transaction-list-item')) + return pendingTxes.length === 1 + }, 10000) + + const [txListItem] = await findElements(driver, By.css('.transaction-list-item')) + const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--primary')) + await driver.wait(until.elementTextMatches(txListValue, /-7\s*TST/)) + await txListItem.click() + await delay(regularDelayMs) + }) + + it('submits the transaction', async function () { + await delay(regularDelayMs) + const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) + await confirmButton.click() + await delay(regularDelayMs) + }) + + it('finds the transaction in the transactions list', async function () { + await driver.wait(async () => { + const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) + return confirmedTxes.length === 4 + }, 10000) + + const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary')) + await driver.wait(until.elementTextMatches(txValues[0], /-7\s*TST/)) + const txStatuses = await findElements(driver, By.css('.transaction-list-item__action')) + await driver.wait(until.elementTextMatches(txStatuses[0], /Sent Tokens/)) + }) + }) + + describe('Approves a custom token from dapp when no gas value is specified', () => { + it('approves an already created token', async () => { + const windowHandles = await driver.getAllWindowHandles() + const extension = windowHandles[0] + const dapp = await switchToWindowWithTitle(driver, 'E2E Test Dapp', windowHandles) + await closeAllWindowHandlesExcept(driver, [extension, dapp]) + await delay(regularDelayMs) + + await driver.switchTo().window(dapp) + await delay(tinyDelayMs) + + const transferTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Approve Tokens Without Gas')]`)) + await transferTokens.click() + + await closeAllWindowHandlesExcept(driver, extension) + await driver.switchTo().window(extension) + await delay(regularDelayMs) + + await driver.wait(async () => { + const pendingTxes = await findElements(driver, By.css('.transaction-list__pending-transactions .transaction-list-item')) + return pendingTxes.length === 1 + }, 10000) + + const [txListItem] = await findElements(driver, By.css('.transaction-list-item')) + const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--primary')) + await driver.wait(until.elementTextMatches(txListValue, /-7\s*TST/)) + await txListItem.click() + await delay(regularDelayMs) + }) + + it('submits the transaction', async function () { + await delay(regularDelayMs) + const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) + await confirmButton.click() + await delay(regularDelayMs) + }) + + it('finds the transaction in the transactions list', async function () { + await driver.wait(async () => { + const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) + return confirmedTxes.length === 5 + }, 10000) + + const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary')) + await driver.wait(until.elementTextMatches(txValues[0], /-7\s*TST/)) + const txStatuses = await findElements(driver, By.css('.transaction-list-item__action')) + await driver.wait(until.elementTextMatches(txStatuses[0], /Approve/)) + }) + }) + describe('Hide token', () => { it('hides the token when clicked', async () => { const [hideTokenEllipsis] = await findElements(driver, By.css('.token-list-item__ellipsis')) @@ -1341,11 +1471,14 @@ describe('MetaMask', function () { await customRpcButton.click() await delay(regularDelayMs) - const customRpcInput = await findElement(driver, By.css('input[placeholder="New RPC URL"]')) + await findElement(driver, By.css('.settings-page__sub-header-text')) + + const customRpcInputs = await findElements(driver, By.css('input[type="text"]')) + const customRpcInput = customRpcInputs[1] await customRpcInput.clear() await customRpcInput.sendKeys(customRpcUrl) - const customRpcSave = await findElement(driver, By.css('.settings-tab__rpc-save-button')) + const customRpcSave = await findElement(driver, By.css('.page-container__footer-button')) await customRpcSave.click() await delay(largeDelayMs * 2) }) diff --git a/test/integration/lib/confirm-sig-requests.js b/test/integration/lib/confirm-sig-requests.js index c3b0dfcff..699527609 100644 --- a/test/integration/lib/confirm-sig-requests.js +++ b/test/integration/lib/confirm-sig-requests.js @@ -15,16 +15,25 @@ QUnit.test('successful confirmation of sig requests', (assert) => { }) }) -async function runConfirmSigRequestsTest (assert, done) { +global.ethQuery = global.ethQuery || {} + +async function runConfirmSigRequestsTest (assert) { const selectState = await queryAsync($, 'select') selectState.val('confirm sig requests') reactTriggerChange(selectState[0]) + const realFetch = window.fetch.bind(window) global.fetch = (...args) => { - if (args[0].match(/chromeextensionmm/)) { + if (args[0] === 'https://ethgasstation.info/json/ethgasAPI.json') { + return Promise.resolve({ json: () => Promise.resolve(JSON.parse(fetchMockResponses.ethGasBasic)) }) + } else if (args[0] === 'https://ethgasstation.info/json/predictTable.json') { + return Promise.resolve({ json: () => Promise.resolve(JSON.parse(fetchMockResponses.ethGasPredictTable)) }) + } else if (args[0] === 'https://dev.blockscale.net/api/gasexpress.json') { + return Promise.resolve({ json: () => Promise.resolve(JSON.parse(fetchMockResponses.gasExpress)) }) + } else if (args[0].match(/chromeextensionmm/)) { return Promise.resolve({ json: () => Promise.resolve(JSON.parse(fetchMockResponses.metametrics)) }) } - return window.fetch(...args) + return realFetch.fetch(...args) } const pendingRequestItem = $.find('.transaction-list-item .transaction-list-item__grid') diff --git a/test/integration/lib/currency-localization.js b/test/integration/lib/currency-localization.js index cd10efa30..24c3a1a2d 100644 --- a/test/integration/lib/currency-localization.js +++ b/test/integration/lib/currency-localization.js @@ -16,16 +16,23 @@ QUnit.test('renders localized currency', (assert) => { }) }) -async function runCurrencyLocalizationTest (assert, done) { +async function runCurrencyLocalizationTest (assert) { console.log('*** start runCurrencyLocalizationTest') const selectState = await queryAsync($, 'select') selectState.val('currency localization') + const realFetch = window.fetch.bind(window) global.fetch = (...args) => { - if (args[0].match(/chromeextensionmm/)) { + if (args[0] === 'https://ethgasstation.info/json/ethgasAPI.json') { + return Promise.resolve({ json: () => Promise.resolve(JSON.parse(fetchMockResponses.ethGasBasic)) }) + } else if (args[0] === 'https://ethgasstation.info/json/predictTable.json') { + return Promise.resolve({ json: () => Promise.resolve(JSON.parse(fetchMockResponses.ethGasPredictTable)) }) + } else if (args[0] === 'https://dev.blockscale.net/api/gasexpress.json') { + return Promise.resolve({ json: () => Promise.resolve(JSON.parse(fetchMockResponses.gasExpress)) }) + } else if (args[0].match(/chromeextensionmm/)) { return Promise.resolve({ json: () => Promise.resolve(JSON.parse(fetchMockResponses.metametrics)) }) } - return window.fetch(...args) + return realFetch.fetch(...args) } await timeout(1000) diff --git a/test/integration/lib/send-new-ui.js b/test/integration/lib/send-new-ui.js index 6a58611d1..78014feef 100644 --- a/test/integration/lib/send-new-ui.js +++ b/test/integration/lib/send-new-ui.js @@ -22,9 +22,10 @@ global.ethQuery = { global.ethereumProvider = {} -async function runSendFlowTest (assert, done) { +async function runSendFlowTest (assert) { const tempFetch = global.fetch + const realFetch = window.fetch.bind(window) global.fetch = (...args) => { if (args[0] === 'https://ethgasstation.info/json/ethgasAPI.json') { return Promise.resolve({ json: () => Promise.resolve(JSON.parse(fetchMockResponses.ethGasBasic)) }) @@ -35,7 +36,7 @@ async function runSendFlowTest (assert, done) { } else if (args[0].match(/chromeextensionmm/)) { return Promise.resolve({ json: () => Promise.resolve(JSON.parse(fetchMockResponses.metametrics)) }) } - return window.fetch(...args) + return realFetch.fetch(...args) } console.log('*** start runSendFlowTest') diff --git a/test/integration/lib/tx-list-items.js b/test/integration/lib/tx-list-items.js index c0056dd22..e4478614e 100644 --- a/test/integration/lib/tx-list-items.js +++ b/test/integration/lib/tx-list-items.js @@ -20,17 +20,24 @@ global.ethQuery.getTransactionCount = (_, cb) => { cb(null, '0x4') } -async function runTxListItemsTest (assert, done) { +async function runTxListItemsTest (assert) { console.log('*** start runTxListItemsTest') const selectState = await queryAsync($, 'select') selectState.val('tx list items') reactTriggerChange(selectState[0]) + const realFetch = window.fetch.bind(window) global.fetch = (...args) => { - if (args[0].match(/chromeextensionmm/)) { + if (args[0] === 'https://ethgasstation.info/json/ethgasAPI.json') { + return Promise.resolve({ json: () => Promise.resolve(JSON.parse(fetchMockResponses.ethGasBasic)) }) + } else if (args[0] === 'https://ethgasstation.info/json/predictTable.json') { + return Promise.resolve({ json: () => Promise.resolve(JSON.parse(fetchMockResponses.ethGasPredictTable)) }) + } else if (args[0] === 'https://dev.blockscale.net/api/gasexpress.json') { + return Promise.resolve({ json: () => Promise.resolve(JSON.parse(fetchMockResponses.gasExpress)) }) + } else if (args[0].match(/chromeextensionmm/)) { return Promise.resolve({ json: () => Promise.resolve(JSON.parse(fetchMockResponses.metametrics)) }) } - return window.fetch(...args) + return realFetch.fetch(...args) } const metamaskLogo = await queryAsync($, '.app-header__logo-container') diff --git a/test/lib/mock-encryptor.js b/test/lib/mock-encryptor.js index 852c536c2..23ab2404f 100644 --- a/test/lib/mock-encryptor.js +++ b/test/lib/mock-encryptor.js @@ -4,12 +4,12 @@ let cacheVal module.exports = { - encrypt (password, dataObj) { + encrypt (_, dataObj) { cacheVal = dataObj return Promise.resolve(mockHex) }, - decrypt (password, text) { + decrypt () { return Promise.resolve(cacheVal || {}) }, @@ -21,7 +21,7 @@ module.exports = { return this.decrypt(key, text) }, - keyFromPassword (password) { + keyFromPassword () { return Promise.resolve(mockKey) }, diff --git a/test/lib/util.js b/test/lib/util.js index 858565bb9..4c5d789d1 100644 --- a/test/lib/util.js +++ b/test/lib/util.js @@ -6,7 +6,7 @@ module.exports = { } function timeout (time) { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { setTimeout(resolve, time || 1500) }) } diff --git a/test/unit/actions/tx_test.js b/test/unit/actions/tx_test.js index 8c64d844f..f2f8f1d1c 100644 --- a/test/unit/actions/tx_test.js +++ b/test/unit/actions/tx_test.js @@ -33,8 +33,8 @@ describe('tx confirmation screen', function () { describe('cancelTx', function () { before(function (done) { actions._setBackgroundConnection({ - approveTransaction (txId, cb) { cb('An error!') }, - cancelTransaction (txId, cb) { cb() }, + approveTransaction (_, cb) { cb('An error!') }, + cancelTransaction (_, cb) { cb() }, clearSeedWordCache (cb) { cb() }, getState (cb) { cb() }, }) diff --git a/test/unit/app/controllers/blacklist-controller-test.js b/test/unit/app/controllers/blacklist-controller-test.js deleted file mode 100644 index 7a14c02cc..000000000 --- a/test/unit/app/controllers/blacklist-controller-test.js +++ /dev/null @@ -1,56 +0,0 @@ -const assert = require('assert') -const BlacklistController = require('../../../../app/scripts/controllers/blacklist') - -describe('blacklist controller', function () { - let blacklistController - - before(() => { - blacklistController = new BlacklistController() - }) - - describe('whitelistDomain', function () { - it('should add hostname to the runtime whitelist', function () { - blacklistController.whitelistDomain('foo.com') - assert.deepEqual(blacklistController.store.getState().whitelist, ['foo.com']) - - blacklistController.whitelistDomain('bar.com') - assert.deepEqual(blacklistController.store.getState().whitelist, ['bar.com', 'foo.com']) - }) - }) - - describe('checkForPhishing', function () { - it('should not flag whitelisted values', function () { - const result = blacklistController.checkForPhishing('www.metamask.io') - assert.equal(result, false) - }) - it('should flag explicit values', function () { - const result = blacklistController.checkForPhishing('metamask.com') - assert.equal(result, true) - }) - it('should flag levenshtein values', function () { - const result = blacklistController.checkForPhishing('metmask.io') - assert.equal(result, true) - }) - it('should not flag not-even-close values', function () { - const result = blacklistController.checkForPhishing('example.com') - assert.equal(result, false) - }) - it('should not flag the ropsten faucet domains', function () { - const result = blacklistController.checkForPhishing('faucet.metamask.io') - assert.equal(result, false) - }) - it('should not flag the mascara domain', function () { - const result = blacklistController.checkForPhishing('zero.metamask.io') - assert.equal(result, false) - }) - it('should not flag the mascara-faucet domain', function () { - const result = blacklistController.checkForPhishing('zero-faucet.metamask.io') - assert.equal(result, false) - }) - it('should not flag whitelisted domain', function () { - blacklistController.whitelistDomain('metamask.com') - const result = blacklistController.checkForPhishing('metamask.com') - assert.equal(result, false) - }) - }) -}) diff --git a/test/unit/app/controllers/currency-controller-test.js b/test/unit/app/controllers/currency-controller-test.js index 7c4644d9f..8b6fbb719 100644 --- a/test/unit/app/controllers/currency-controller-test.js +++ b/test/unit/app/controllers/currency-controller-test.js @@ -59,7 +59,7 @@ describe('currency-controller', function () { var promise = new Promise( - function (resolve, reject) { + function (resolve) { currencyController.setCurrentCurrency('jpy') currencyController.updateConversionRate().then(function () { resolve() diff --git a/test/unit/app/controllers/metamask-controller-test.js b/test/unit/app/controllers/metamask-controller-test.js index 1ed6a95fb..a56b8adbd 100644 --- a/test/unit/app/controllers/metamask-controller-test.js +++ b/test/unit/app/controllers/metamask-controller-test.js @@ -49,7 +49,7 @@ describe('MetaMaskController', function () { showUnapprovedTx: noop, showUnconfirmedMessage: noop, encryptor: { - encrypt: function (password, object) { + encrypt: function (_, object) { this.object = object return Promise.resolve('mock-encrypted') }, @@ -144,7 +144,7 @@ describe('MetaMaskController', function () { 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.slice(0, -1)).catch(() => null) await metamaskController.createNewVaultAndRestore(password, TEST_SEED) assert(metamaskController.keyringController.createNewVaultAndRestore.calledTwice) @@ -207,7 +207,7 @@ describe('MetaMaskController', function () { const accounts = {} const balance = '0x14ced5122ce0a000' const ethQuery = new EthQuery() - sinon.stub(ethQuery, 'getBalance').callsFake((account, callback) => { + sinon.stub(ethQuery, 'getBalance').callsFake((_, callback) => { callback(undefined, balance) }) @@ -295,7 +295,7 @@ describe('MetaMaskController', function () { it('should add the Trezor Hardware keyring', async function () { sinon.spy(metamaskController.keyringController, 'addNewKeyring') - await metamaskController.connectHardware('trezor', 0).catch((e) => null) + await metamaskController.connectHardware('trezor', 0).catch(() => null) const keyrings = await metamaskController.keyringController.getKeyringsByType( 'Trezor Hardware' ) @@ -305,7 +305,7 @@ describe('MetaMaskController', function () { it('should add the Ledger Hardware keyring', async function () { sinon.spy(metamaskController.keyringController, 'addNewKeyring') - await metamaskController.connectHardware('ledger', 0).catch((e) => null) + await metamaskController.connectHardware('ledger', 0).catch(() => null) const keyrings = await metamaskController.keyringController.getKeyringsByType( 'Ledger Hardware' ) @@ -325,7 +325,7 @@ describe('MetaMaskController', function () { }) it('should be locked by default', async function () { - await metamaskController.connectHardware('trezor', 0).catch((e) => null) + await metamaskController.connectHardware('trezor', 0).catch(() => null) const status = await metamaskController.checkHardwareStatus('trezor') assert.equal(status, false) }) @@ -341,7 +341,7 @@ describe('MetaMaskController', function () { }) it('should wipe all the keyring info', async function () { - await metamaskController.connectHardware('trezor', 0).catch((e) => null) + await metamaskController.connectHardware('trezor', 0).catch(() => null) await metamaskController.forgetDevice('trezor') const keyrings = await metamaskController.keyringController.getKeyringsByType( 'Trezor Hardware' @@ -376,7 +376,7 @@ describe('MetaMaskController', function () { sinon.spy(metamaskController.preferencesController, 'setAddresses') sinon.spy(metamaskController.preferencesController, 'setSelectedAddress') sinon.spy(metamaskController.preferencesController, 'setAccountLabel') - await metamaskController.connectHardware('trezor', 0, `m/44/0'/0'`).catch((e) => null) + await metamaskController.connectHardware('trezor', 0, `m/44/0'/0'`).catch(() => null) await metamaskController.unlockHardwareWalletAccount(accountToUnlock, 'trezor', `m/44/0'/0'`) }) @@ -464,7 +464,7 @@ describe('MetaMaskController', function () { depositAddress = '3EevLFfB4H4XMWQwYCgjLie1qCAGpd2WBc' depositType = 'ETH' - shapeShiftTxList = metamaskController.shapeshiftController.store.getState().shapeShiftTxList + shapeShiftTxList = metamaskController.shapeshiftController.state.shapeShiftTxList }) it('creates a shapeshift tx', async function () { @@ -752,12 +752,11 @@ describe('MetaMaskController', function () { }) it('sets up phishing stream for untrusted communication ', async () => { - await metamaskController.blacklistController.updatePhishingList() - console.log(blacklistJSON.blacklist.includes(phishingUrl)) + await metamaskController.phishingController.updatePhishingLists() const { promise, resolve } = deferredPromise() - streamTest = createThoughStream((chunk, enc, cb) => { + streamTest = createThoughStream((chunk, _, cb) => { if (chunk.name !== 'phishing') return cb() assert.equal(chunk.data.hostname, phishingUrl) resolve() @@ -777,7 +776,7 @@ describe('MetaMaskController', function () { }) it('sets up controller dnode api for trusted communication', function (done) { - streamTest = createThoughStream((chunk, enc, cb) => { + streamTest = createThoughStream((chunk, _, cb) => { assert.equal(chunk.name, 'controller') cb() done() diff --git a/test/unit/app/controllers/preferences-controller-test.js b/test/unit/app/controllers/preferences-controller-test.js index 558597ae7..81b152f3d 100644 --- a/test/unit/app/controllers/preferences-controller-test.js +++ b/test/unit/app/controllers/preferences-controller-test.js @@ -527,14 +527,14 @@ describe('preferences controller', function () { it('should add custom RPC url to state', function () { preferencesController.addToFrequentRpcList('rpc_url', 1) preferencesController.addToFrequentRpcList('http://localhost:8545', 1) - assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: 1, ticker: 'ETH', nickname: '' }]) + assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: 1, ticker: 'ETH', nickname: '', rpcPrefs: {} }]) preferencesController.addToFrequentRpcList('rpc_url', 1) - assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: 1, ticker: 'ETH', nickname: '' }]) + assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: 1, ticker: 'ETH', nickname: '', rpcPrefs: {} }]) }) it('should remove custom RPC url from state', function () { preferencesController.addToFrequentRpcList('rpc_url', 1) - assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: 1, ticker: 'ETH', nickname: '' }]) + assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: 1, ticker: 'ETH', nickname: '', rpcPrefs: {} }]) preferencesController.removeFromFrequentRpcList('other_rpc_url') preferencesController.removeFromFrequentRpcList('http://localhost:8545') preferencesController.removeFromFrequentRpcList('rpc_url') diff --git a/test/unit/app/controllers/transactions/pending-tx-test.js b/test/unit/app/controllers/transactions/pending-tx-test.js index 2988bf61f..1c5f59f5a 100644 --- a/test/unit/app/controllers/transactions/pending-tx-test.js +++ b/test/unit/app/controllers/transactions/pending-tx-test.js @@ -100,7 +100,7 @@ describe('PendingTransactionTracker', function () { describe('#_checkPendingTx', function () { it('should emit \'tx:failed\' if the txMeta does not have a hash', function (done) { - pendingTxTracker.once('tx:failed', (txId, err) => { + pendingTxTracker.once('tx:failed', (txId) => { assert(txId, txMetaNoHash.id, 'should pass txId') done() }) @@ -128,7 +128,7 @@ describe('PendingTransactionTracker', function () { pendingTxTracker.getPendingTransactions = () => txList pendingTxTracker._checkPendingTx = (tx) => { tx.resolve(tx) } Promise.all(txList.map((tx) => tx.processed)) - .then((txCompletedList) => done()) + .then(() => done()) .catch(done) pendingTxTracker.updatePendingTxs() @@ -152,7 +152,7 @@ describe('PendingTransactionTracker', function () { pendingTxTracker.getPendingTransactions = () => txList pendingTxTracker._resubmitTx = async (tx) => { tx.resolve(tx) } Promise.all(txList.map((tx) => tx.processed)) - .then((txCompletedList) => done()) + .then(() => done()) .catch(done) pendingTxTracker.resubmitPendingTxs(blockNumberStub) }) @@ -178,7 +178,7 @@ describe('PendingTransactionTracker', function () { throw new Error(knownErrors.pop()) } Promise.all(txList.map((tx) => tx.processed)) - .then((txCompletedList) => done()) + .then(() => done()) .catch(done) pendingTxTracker.resubmitPendingTxs(blockNumberStub) @@ -194,9 +194,9 @@ describe('PendingTransactionTracker', function () { }) pendingTxTracker.getPendingTransactions = () => txList - pendingTxTracker._resubmitTx = async (tx) => { throw new TypeError('im some real error') } + pendingTxTracker._resubmitTx = async () => { throw new TypeError('im some real error') } Promise.all(txList.map((tx) => tx.processed)) - .then((txCompletedList) => done()) + .then(() => done()) .catch(done) pendingTxTracker.resubmitPendingTxs(blockNumberStub) diff --git a/test/unit/app/controllers/transactions/tx-controller-test.js b/test/unit/app/controllers/transactions/tx-controller-test.js index 9000cd364..8ff409207 100644 --- a/test/unit/app/controllers/transactions/tx-controller-test.js +++ b/test/unit/app/controllers/transactions/tx-controller-test.js @@ -8,6 +8,13 @@ const TransactionController = require('../../../../../app/scripts/controllers/tr const { TRANSACTION_TYPE_RETRY, } = require('../../../../../app/scripts/controllers/transactions/enums') +const { + TOKEN_METHOD_APPROVE, + TOKEN_METHOD_TRANSFER, + SEND_ETHER_ACTION_KEY, + DEPLOY_CONTRACT_ACTION_KEY, + CONTRACT_INTERACTION_KEY, +} = require('../../../../../ui/app/helpers/constants/transactions.js') const { createTestProviderTools, getTestAccounts } = require('../../../../stub/provider') const noop = () => true @@ -537,6 +544,86 @@ describe('Transaction Controller', function () { }) }) + describe('#_determineTransactionCategory', function () { + it('should return a simple send transactionCategory when to is truthy but data is falsey', async function () { + const result = await txController._determineTransactionCategory({ + to: '0xabc', + data: '', + }) + assert.deepEqual(result, { transactionCategory: SEND_ETHER_ACTION_KEY, getCodeResponse: undefined }) + }) + + it('should return a token transfer transactionCategory when data is for the respective method call', async function () { + const result = await txController._determineTransactionCategory({ + to: '0xabc', + data: '0xa9059cbb0000000000000000000000002f318C334780961FB129D2a6c30D0763d9a5C970000000000000000000000000000000000000000000000000000000000000000a', + }) + assert.deepEqual(result, { transactionCategory: TOKEN_METHOD_TRANSFER, getCodeResponse: undefined }) + }) + + it('should return a token approve transactionCategory when data is for the respective method call', async function () { + const result = await txController._determineTransactionCategory({ + to: '0xabc', + data: '0x095ea7b30000000000000000000000002f318C334780961FB129D2a6c30D0763d9a5C9700000000000000000000000000000000000000000000000000000000000000005', + }) + assert.deepEqual(result, { transactionCategory: TOKEN_METHOD_APPROVE, getCodeResponse: undefined }) + }) + + it('should return a contract deployment transactionCategory when to is falsey and there is data', async function () { + const result = await txController._determineTransactionCategory({ + to: '', + data: '0xabd', + }) + assert.deepEqual(result, { transactionCategory: DEPLOY_CONTRACT_ACTION_KEY, getCodeResponse: undefined }) + }) + + it('should return a simple send transactionCategory with a 0x getCodeResponse when there is data and but the to address is not a contract address', async function () { + const result = await txController._determineTransactionCategory({ + to: '0x9e673399f795D01116e9A8B2dD2F156705131ee9', + data: '0xabd', + }) + assert.deepEqual(result, { transactionCategory: SEND_ETHER_ACTION_KEY, getCodeResponse: '0x' }) + }) + + it('should return a simple send transactionCategory with a null getCodeResponse when to is truthy and there is data and but getCode returns an error', async function () { + const result = await txController._determineTransactionCategory({ + to: '0xabc', + data: '0xabd', + }) + assert.deepEqual(result, { transactionCategory: SEND_ETHER_ACTION_KEY, getCodeResponse: null }) + }) + + it('should return a contract interaction transactionCategory with the correct getCodeResponse when to is truthy and there is data and it is not a token transaction', async function () { + const _providerResultStub = { + // 1 gwei + eth_gasPrice: '0x0de0b6b3a7640000', + // by default, all accounts are external accounts (not contracts) + eth_getCode: '0xa', + } + const _provider = createTestProviderTools({ scaffold: _providerResultStub }).provider + const _fromAccount = getTestAccounts()[0] + const _blockTrackerStub = new EventEmitter() + _blockTrackerStub.getCurrentBlock = noop + _blockTrackerStub.getLatestBlock = noop + const _txController = new TransactionController({ + provider: _provider, + getGasPrice: function () { return '0xee6b2800' }, + networkStore: new ObservableStore(currentNetworkId), + txHistoryLimit: 10, + blockTracker: _blockTrackerStub, + signTransaction: (ethTx) => new Promise((resolve) => { + ethTx.sign(_fromAccount.key) + resolve() + }), + }) + const result = await _txController._determineTransactionCategory({ + to: '0x9e673399f795D01116e9A8B2dD2F156705131ee9', + data: 'abd', + }) + assert.deepEqual(result, { transactionCategory: CONTRACT_INTERACTION_KEY, getCodeResponse: '0x0a' }) + }) + }) + describe('#getPendingTransactions', function () { beforeEach(function () { txController.txStateManager._saveTxList([ diff --git a/test/unit/app/controllers/transactions/tx-gas-util-test.js b/test/unit/app/controllers/transactions/tx-gas-util-test.js index 31defd6ed..f92d95507 100644 --- a/test/unit/app/controllers/transactions/tx-gas-util-test.js +++ b/test/unit/app/controllers/transactions/tx-gas-util-test.js @@ -11,7 +11,7 @@ describe('txUtils', function () { before(function () { txUtils = new TxUtils(new Proxy({}, { - get: (obj, name) => { + get: () => { return () => {} }, })) diff --git a/test/unit/app/controllers/transactions/tx-state-history-helper-test.js b/test/unit/app/controllers/transactions/tx-state-history-helper-test.js index fba0e7fda..328c2ac6f 100644 --- a/test/unit/app/controllers/transactions/tx-state-history-helper-test.js +++ b/test/unit/app/controllers/transactions/tx-state-history-helper-test.js @@ -29,7 +29,7 @@ describe('Transaction state history helper', function () { describe('#migrateFromSnapshotsToDiffs', function () { it('migrates history to diffs and can recover original values', function () { - testVault.data.TransactionController.transactions.forEach((tx, index) => { + testVault.data.TransactionController.transactions.forEach((tx) => { const newHistory = txStateHistoryHelper.migrateFromSnapshotsToDiffs(tx.history) newHistory.forEach((newEntry, index) => { if (index === 0) { diff --git a/test/unit/app/controllers/transactions/tx-state-manager-test.js b/test/unit/app/controllers/transactions/tx-state-manager-test.js index 88bdaa60e..4ccade2aa 100644 --- a/test/unit/app/controllers/transactions/tx-state-manager-test.js +++ b/test/unit/app/controllers/transactions/tx-state-manager-test.js @@ -55,7 +55,7 @@ describe('TransactionStateManager', function () { it('should emit a rejected event to signal the exciton of callback', (done) => { const tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } txStateManager.addTx(tx) - const noop = function (err, txId) { + const noop = function (err) { if (err) { console.log('Error: ', err) } diff --git a/test/unit/app/edge-encryptor-test.js b/test/unit/app/edge-encryptor-test.js index 1a6255b36..52817cd09 100644 --- a/test/unit/app/edge-encryptor-test.js +++ b/test/unit/app/edge-encryptor-test.js @@ -83,7 +83,7 @@ describe('EdgeEncryptor', function () { edgeEncryptor.encrypt(password, data) .then(function (encryptedData) { edgeEncryptor.decrypt('wrong password', encryptedData) - .then(function (decryptedData) { + .then(function () { assert.fail('could decrypt with wrong password') done() }) diff --git a/test/unit/migrations/migrator-test.js b/test/unit/migrations/migrator-test.js index a9374dff1..693c5830d 100644 --- a/test/unit/migrations/migrator-test.js +++ b/test/unit/migrations/migrator-test.js @@ -61,7 +61,7 @@ describe('Migrator', () => { const migrator = new Migrator({ migrations: [{ version: 1, migrate: async () => { throw new Error('test') } } ] }) migrator.on('error', () => done()) migrator.migrateData({ meta: {version: 0} }) - .then((migratedData) => { + .then(() => { }).catch(done) }) diff --git a/test/unit/ui/app/actions.spec.js b/test/unit/ui/app/actions.spec.js index 86c3f8aff..34dd6a39b 100644 --- a/test/unit/ui/app/actions.spec.js +++ b/test/unit/ui/app/actions.spec.js @@ -44,7 +44,7 @@ describe('Actions', () => { showUnapprovedTx: noop, showUnconfirmedMessage: noop, encryptor: { - encrypt: function (password, object) { + encrypt: function (_, object) { this.object = object return Promise.resolve('mock-encrypted') }, @@ -103,7 +103,7 @@ describe('Actions', () => { submitPasswordSpy = sinon.stub(background, 'submitPassword') - submitPasswordSpy.callsFake((password, callback) => { + submitPasswordSpy.callsFake((_, callback) => { callback(new Error('error in submitPassword')) }) @@ -235,7 +235,7 @@ describe('Actions', () => { createNewVaultAndRestoreSpy = sinon.stub(background, 'createNewVaultAndRestore') - createNewVaultAndRestoreSpy.callsFake((password, seed, callback) => { + createNewVaultAndRestoreSpy.callsFake((_, __, callback) => { callback(new Error('error')) }) @@ -279,7 +279,7 @@ describe('Actions', () => { ] createNewVaultAndKeychainSpy = sinon.stub(background, 'createNewVaultAndKeychain') - createNewVaultAndKeychainSpy.callsFake((password, callback) => { + createNewVaultAndKeychainSpy.callsFake((_, callback) => { callback(new Error('error')) }) @@ -342,7 +342,7 @@ describe('Actions', () => { ] submitPasswordSpy = sinon.stub(background, 'submitPassword') - submitPasswordSpy.callsFake((password, callback) => { + submitPasswordSpy.callsFake((_, callback) => { callback(new Error('error')) }) @@ -414,7 +414,7 @@ describe('Actions', () => { it('displays warning error message when submitPassword in background errors', () => { submitPasswordSpy = sinon.stub(background, 'submitPassword') - submitPasswordSpy.callsFake((password, callback) => { + submitPasswordSpy.callsFake((_, callback) => { callback(new Error('error')) }) @@ -483,7 +483,7 @@ describe('Actions', () => { { type: 'DISPLAY_WARNING', value: 'error' }, ] removeAccountSpy = sinon.stub(background, 'removeAccount') - removeAccountSpy.callsFake((address, callback) => { + removeAccountSpy.callsFake((_, callback) => { callback(new Error('error')) }) @@ -522,7 +522,7 @@ describe('Actions', () => { { type: 'DISPLAY_WARNING', value: 'error' }, ] - addNewKeyringSpy.callsFake((type, opts, callback) => { + addNewKeyringSpy.callsFake((_, __, callback) => { callback(new Error('error')) }) @@ -611,7 +611,7 @@ describe('Actions', () => { ] importAccountWithStrategySpy = sinon.stub(background, 'importAccountWithStrategy') - importAccountWithStrategySpy.callsFake((strategy, args, callback) => { + importAccountWithStrategySpy.callsFake((_, __, callback) => { callback(new Error('error')) }) @@ -668,7 +668,7 @@ describe('Actions', () => { { type: 'HIDE_LOADING_INDICATION' }, { type: 'DISPLAY_WARNING', value: 'error' }, ] - setCurrentCurrencySpy.callsFake((currencyCode, callback) => { + setCurrentCurrencySpy.callsFake((_, callback) => { callback(new Error('error')) }) @@ -720,7 +720,7 @@ describe('Actions', () => { ] signMessageSpy = sinon.stub(background, 'signMessage') - signMessageSpy.callsFake((msgData, callback) => { + signMessageSpy.callsFake((_, callback) => { callback(new Error('error')) }) @@ -775,7 +775,7 @@ describe('Actions', () => { ] signPersonalMessageSpy = sinon.stub(background, 'signPersonalMessage') - signPersonalMessageSpy.callsFake((msgData, callback) => { + signPersonalMessageSpy.callsFake((_, callback) => { callback(new Error('error')) }) @@ -812,7 +812,7 @@ describe('Actions', () => { { type: 'DISPLAY_WARNING', value: 'error' }, { type: 'SHOW_CONF_TX_PAGE', transForward: true, id: undefined }, ] - sendTransactionSpy.callsFake((txData, callback) => { + sendTransactionSpy.callsFake((_, callback) => { callback(new Error('error')) }) @@ -906,7 +906,7 @@ describe('Actions', () => { { type: 'DISPLAY_WARNING', value: 'error' }, ] - setSelectedAddressSpy.callsFake((address, callback) => { + setSelectedAddressSpy.callsFake((_, callback) => { callback(new Error('error')) }) @@ -941,7 +941,7 @@ describe('Actions', () => { { type: 'HIDE_LOADING_INDICATION' }, { type: 'DISPLAY_WARNING', value: 'error' }, ] - setSelectedAddressSpy.callsFake((address, callback) => { + setSelectedAddressSpy.callsFake((_, callback) => { callback(new Error('error')) }) @@ -980,7 +980,7 @@ describe('Actions', () => { { type: 'UPDATE_TOKENS', newTokens: undefined }, ] - addTokenSpy.callsFake((address, symbol, decimals, image, callback) => { + addTokenSpy.callsFake((_, __, ___, ____, callback) => { callback(new Error('error')) }) @@ -1020,7 +1020,7 @@ describe('Actions', () => { { type: 'UPDATE_TOKENS', newTokens: undefined }, ] - removeTokenSpy.callsFake((address, callback) => { + removeTokenSpy.callsFake((_, callback) => { callback(new Error('error')) }) @@ -1054,7 +1054,7 @@ describe('Actions', () => { { type: 'DISPLAY_WARNING', value: 'Had a problem changing networks!' }, ] - setProviderTypeSpy.callsFake((type, callback) => { + setProviderTypeSpy.callsFake((_, callback) => { callback(new Error('error')) }) @@ -1087,7 +1087,7 @@ describe('Actions', () => { { type: 'DISPLAY_WARNING', value: 'Had a problem changing networks!' }, ] - setRpcTargetSpy.callsFake((newRpc, chainId, ticker, nickname, callback) => { + setRpcTargetSpy.callsFake((_, __, ___, ____, callback) => { callback(new Error('error')) }) @@ -1134,7 +1134,7 @@ describe('Actions', () => { exportAccountSpy = sinon.spy(background, 'exportAccount') return store.dispatch(actions.exportAccount(password, '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc')) - .then((result) => { + .then(() => { assert(submitPasswordSpy.calledOnce) assert(exportAccountSpy.calledOnce) assert.deepEqual(store.getActions(), expectedActions) @@ -1150,7 +1150,7 @@ describe('Actions', () => { ] submitPasswordSpy = sinon.stub(background, 'submitPassword') - submitPasswordSpy.callsFake((password, callback) => { + submitPasswordSpy.callsFake((_, callback) => { callback(new Error('error')) }) @@ -1169,7 +1169,7 @@ describe('Actions', () => { ] exportAccountSpy = sinon.stub(background, 'exportAccount') - exportAccountSpy.callsFake((address, callback) => { + exportAccountSpy.callsFake((_, callback) => { callback(new Error('error')) }) @@ -1196,7 +1196,6 @@ describe('Actions', () => { describe('#pairUpdate', () => { beforeEach(() => { - nock('https://shapeshift.io') .defaultReplyHeaders({ 'access-control-allow-origin': '*' }) .get('/marketinfo/btc_eth') @@ -1206,10 +1205,6 @@ describe('Actions', () => { .defaultReplyHeaders({ 'access-control-allow-origin': '*' }) .get('/coins') .reply(200) - }) - - afterEach(() => { - nock.restore() }) it('', () => { @@ -1251,7 +1246,7 @@ describe('Actions', () => { { type: 'DISPLAY_WARNING', value: 'error' }, ] - setFeatureFlagSpy.callsFake((feature, activated, callback) => { + setFeatureFlagSpy.callsFake((_, __, callback) => { callback(new Error('error')) }) @@ -1305,7 +1300,7 @@ describe('Actions', () => { ] getTransactionCountSpy = sinon.stub(global.ethQuery, 'getTransactionCount') - getTransactionCountSpy.callsFake((address, callback) => { + getTransactionCountSpy.callsFake((_, callback) => { callback(new Error('error')) }) @@ -1343,7 +1338,7 @@ describe('Actions', () => { { type: 'SET_USE_BLOCKIE', value: undefined }, ] - setUseBlockieSpy.callsFake((val, callback) => { + setUseBlockieSpy.callsFake((_, callback) => { callback(new Error('error')) }) @@ -1390,7 +1385,7 @@ describe('Actions', () => { { type: 'DISPLAY_WARNING', value: 'error' }, ] setCurrentLocaleSpy = sinon.stub(background, 'setCurrentLocale') - setCurrentLocaleSpy.callsFake((key, callback) => { + setCurrentLocaleSpy.callsFake((_, callback) => { callback(new Error('error')) }) diff --git a/test/web3/web3.js b/test/web3/web3.js index 5c2de078d..0f7a4c3cd 100644 --- a/test/web3/web3.js +++ b/test/web3/web3.js @@ -12,7 +12,7 @@ web3.currentProvider.enable().then(() => { Object.keys(methodGroup).forEach(methodKey => { const methodButton = document.getElementById(methodKey) - methodButton.addEventListener('click', function (event) { + methodButton.addEventListener('click', function () { window.ethereum.sendAsync({ method: methodKey, diff --git a/ui/app/components/app/account-panel.js b/ui/app/components/app/account-panel.js index 79882f34a..e61cb8ad6 100644 --- a/ui/app/components/app/account-panel.js +++ b/ui/app/components/app/account-panel.js @@ -69,18 +69,9 @@ AccountPanel.prototype.render = function () { ) } -function balanceOrFaucetingIndication (account, isFauceting) { - // Temporarily deactivating isFauceting indication - // because it shows fauceting for empty restored accounts. - if (/* isFauceting*/ false) { - return { - key: 'Account is auto-funding.', - value: 'Please wait.', - } - } else { - return { - key: 'BALANCE', - value: formatBalance(account.balance), - } +function balanceOrFaucetingIndication (account) { + return { + key: 'BALANCE', + value: formatBalance(account.balance), } } diff --git a/ui/app/components/app/bn-as-decimal-input.js b/ui/app/components/app/bn-as-decimal-input.js index 9a033f893..834bab0a4 100644 --- a/ui/app/components/app/bn-as-decimal-input.js +++ b/ui/app/components/app/bn-as-decimal-input.js @@ -116,7 +116,7 @@ BnAsDecimalInput.prototype.render = function () { ) } -BnAsDecimalInput.prototype.setValid = function (message) { +BnAsDecimalInput.prototype.setValid = function () { this.setState({ invalid: null }) } diff --git a/ui/app/components/app/dropdowns/account-details-dropdown.js b/ui/app/components/app/dropdowns/account-details-dropdown.js index 3d4598946..cbeccdd81 100644 --- a/ui/app/components/app/dropdowns/account-details-dropdown.js +++ b/ui/app/components/app/dropdowns/account-details-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('../../../store/actions') -const { getSelectedIdentity } = require('../../../selectors/selectors') +const { getSelectedIdentity, getRpcPrefsForCurrentProvider } = require('../../../selectors/selectors') const genAccountLink = require('../../../../lib/account-link.js') const { Menu, Item, CloseArea } = require('./components/menu') @@ -20,6 +20,7 @@ function mapStateToProps (state) { selectedIdentity: getSelectedIdentity(state), network: state.metamask.network, keyrings: state.metamask.keyrings, + rpcPrefs: getRpcPrefsForCurrentProvider(state), } } @@ -28,8 +29,8 @@ function mapDispatchToProps (dispatch) { showAccountDetailModal: () => { dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' })) }, - viewOnEtherscan: (address, network) => { - global.platform.openWindow({ url: genAccountLink(address, network) }) + viewOnEtherscan: (address, network, rpcPrefs) => { + global.platform.openWindow({ url: genAccountLink(address, network, rpcPrefs) }) }, showRemoveAccountConfirmationModal: (identity) => { return dispatch(actions.showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', identity })) @@ -56,7 +57,9 @@ AccountDetailsDropdown.prototype.render = function () { keyrings, showAccountDetailModal, viewOnEtherscan, - showRemoveAccountConfirmationModal } = this.props + showRemoveAccountConfirmationModal, + rpcPrefs, + } = this.props const address = selectedIdentity.address @@ -112,10 +115,12 @@ AccountDetailsDropdown.prototype.render = function () { name: 'Clicked View on Etherscan', }, }) - viewOnEtherscan(address, network) + viewOnEtherscan(address, network, rpcPrefs) this.props.onClose() }, - text: this.context.t('viewOnEtherscan'), + text: (rpcPrefs.blockExplorerUrl + ? this.context.t('blockExplorerView', [rpcPrefs.blockExplorerUrl.match(/^https?:\/\/(.+)/)[1]]) + : this.context.t('viewOnEtherscan')), icon: h(`img`, { src: 'images/open-etherscan.svg', style: { height: '15px' } }), }), isRemovable ? h(Item, { diff --git a/ui/app/components/app/dropdowns/network-dropdown.js b/ui/app/components/app/dropdowns/network-dropdown.js index dbe3f1bc8..378ad3ba6 100644 --- a/ui/app/components/app/dropdowns/network-dropdown.js +++ b/ui/app/components/app/dropdowns/network-dropdown.js @@ -10,7 +10,7 @@ const Dropdown = require('./components/dropdown').Dropdown const DropdownMenuItem = require('./components/dropdown').DropdownMenuItem const NetworkDropdownIcon = require('./components/network-dropdown-icon') const R = require('ramda') -const { ADVANCED_ROUTE } = require('../../../helpers/constants/routes') +const { NETWORKS_ROUTE } = require('../../../helpers/constants/routes') // classes from nodes of the toggle element. const notToggleElementClassnames = [ @@ -49,6 +49,7 @@ function mapDispatchToProps (dispatch) { }, showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()), hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()), + setNetworksTabAddMode: isInAddMode => dispatch(actions.setNetworksTabAddMode(isInAddMode)), } } @@ -72,7 +73,7 @@ module.exports = compose( // TODO: specify default props and proptypes NetworkDropdown.prototype.render = function () { const props = this.props - const { provider: { type: providerType, rpcTarget: activeNetwork } } = props + const { provider: { type: providerType, rpcTarget: activeNetwork }, setNetworksTabAddMode } = props const rpcListDetail = props.frequentRpcListDetail const isOpen = this.props.networkDropdownOpen const dropdownMenuItemStyle = { @@ -255,7 +256,10 @@ NetworkDropdown.prototype.render = function () { DropdownMenuItem, { closeMenu: () => this.props.hideNetworkDropdown(), - onClick: () => this.props.history.push(ADVANCED_ROUTE), + onClick: () => { + setNetworksTabAddMode(true) + this.props.history.push(NETWORKS_ROUTE) + }, style: dropdownMenuItemStyle, }, [ diff --git a/ui/app/components/app/ens-input.js b/ui/app/components/app/ens-input.js index 424c5061e..5eea0dd90 100644 --- a/ui/app/components/app/ens-input.js +++ b/ui/app/components/app/ens-input.js @@ -144,7 +144,7 @@ EnsInput.prototype.ensIcon = function (recipient) { }, this.ensIconContents(recipient)) } -EnsInput.prototype.ensIconContents = function (recipient) { +EnsInput.prototype.ensIconContents = function () { const { loadingEns, ensFailure, ensResolution, toError } = this.state || { ensResolution: ZERO_ADDRESS } if (toError) return diff --git a/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js index 95894140c..d6c259033 100644 --- a/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js +++ b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js @@ -58,7 +58,7 @@ export default class AdvancedTabContent extends Component { } } - gasInput ({ labelKey, value, onChange, insufficientBalance, showGWEI, customPriceIsSafe, isSpeedUp }) { + gasInput ({ labelKey, value, onChange, insufficientBalance, customPriceIsSafe, isSpeedUp }) { const { isInError, errorText, diff --git a/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js index 90fef1a1b..73bc13481 100644 --- a/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js +++ b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js @@ -17,8 +17,8 @@ function convertGasLimitForInputs (gasLimitInHexWEI) { const mapDispatchToProps = dispatch => { return { - showGasPriceInfoModal: modalName => dispatch(showModal({ name: 'GAS_PRICE_INFO_MODAL' })), - showGasLimitInfoModal: modalName => dispatch(showModal({ name: 'GAS_LIMIT_INFO_MODAL' })), + showGasPriceInfoModal: () => dispatch(showModal({ name: 'GAS_PRICE_INFO_MODAL' })), + showGasLimitInfoModal: () => dispatch(showModal({ name: 'GAS_LIMIT_INFO_MODAL' })), } } diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js index ad8628621..eab3434df 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js @@ -67,7 +67,7 @@ export default class AdvancedTabContent extends Component { } } - gasInput ({ labelKey, value, onChange, insufficientBalance, showGWEI, customPriceIsSafe, isSpeedUp }) { + gasInput ({ labelKey, value, onChange, insufficientBalance, customPriceIsSafe, isSpeedUp }) { const { isInError, errorText, @@ -148,7 +148,6 @@ export default class AdvancedTabContent extends Component { customGasPrice, updateCustomGasPrice, customGasLimit, - updateCustomGasLimit, insufficientBalance, customPriceIsSafe, isSpeedUp, diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js index 8aaccafd5..e18c1067e 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js @@ -122,8 +122,6 @@ export default class GasModalPageContainer extends Component { } renderTabs ({ - originalTotalFiat, - originalTotalEth, newTotalFiat, newTotalEth, sendAmount, diff --git a/ui/app/components/app/gas-customization/gas-price-button-group/gas-price-button-group.component.js b/ui/app/components/app/gas-customization/gas-price-button-group/gas-price-button-group.component.js index 0456f5262..14952a49a 100644 --- a/ui/app/components/app/gas-customization/gas-price-button-group/gas-price-button-group.component.js +++ b/ui/app/components/app/gas-customization/gas-price-button-group/gas-price-button-group.component.js @@ -49,7 +49,7 @@ export default class GasPriceButtonGroup extends Component { priceInHexWei, ...renderableGasInfo }, { - buttonDataLoading, + buttonDataLoading: _, handleGasPriceSelection, ...buttonContentPropsAndFlags }, index) { diff --git a/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.utils.js b/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.utils.js index f19dafcc1..55512ce09 100644 --- a/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.utils.js +++ b/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.utils.js @@ -68,7 +68,7 @@ export function handleChartUpdate ({ chart, gasPrices, newPrice, cssId }) { export function getAdjacentGasPrices ({ gasPrices, priceToPosition }) { const closestLowerValueIndex = gasPrices.findIndex((e, i, a) => e <= priceToPosition && a[i + 1] >= priceToPosition) - const closestHigherValueIndex = gasPrices.findIndex((e, i, a) => e > priceToPosition) + const closestHigherValueIndex = gasPrices.findIndex((e) => e > priceToPosition) return { closestLowerValueIndex, closestHigherValueIndex, @@ -133,7 +133,7 @@ export function setTickPosition (axis, n, newPosition, secondNewPosition) { d3.select('#chart') .select(`.c3-axis-${axis}`) .selectAll('.tick') - .filter((d, i) => i === n) + .filter((_, i) => i === n) .select('text') .attr(positionToShift, 0) .select('tspan') @@ -284,7 +284,7 @@ export function generateChart (gasPrices, estimatedTimes, gasPricesMax, estimate }) return text + '</table>' + "<div class='tooltip-arrow'></div>" }, - position: function (data) { + position: function () { if (d3.select('#overlayed-circle').empty()) { return { top: -100, left: -100 } } diff --git a/ui/app/components/app/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js b/ui/app/components/app/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js index 7dec7a85f..c960f49a7 100644 --- a/ui/app/components/app/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js +++ b/ui/app/components/app/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js @@ -6,7 +6,7 @@ import shallow from '../../../../../../lib/shallow-with-context' import * as d3 from 'd3' function timeout (time) { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { setTimeout(resolve, time) }) } diff --git a/ui/app/components/app/modals/account-details-modal.js b/ui/app/components/app/modals/account-details-modal.js index 1b1ca6b8e..6cffc918b 100644 --- a/ui/app/components/app/modals/account-details-modal.js +++ b/ui/app/components/app/modals/account-details-modal.js @@ -5,7 +5,7 @@ const inherits = require('util').inherits const connect = require('react-redux').connect const actions = require('../../../store/actions') const AccountModalContainer = require('./account-modal-container') -const { getSelectedIdentity } = require('../../../selectors/selectors') +const { getSelectedIdentity, getRpcPrefsForCurrentProvider } = require('../../../selectors/selectors') const genAccountLink = require('../../../../lib/account-link.js') const QrView = require('../../ui/qr-code') const EditableLabel = require('../../ui/editable-label') @@ -17,6 +17,7 @@ function mapStateToProps (state) { network: state.metamask.network, selectedIdentity: getSelectedIdentity(state), keyrings: state.metamask.keyrings, + rpcPrefs: getRpcPrefsForCurrentProvider(state), } } @@ -54,6 +55,7 @@ AccountDetailsModal.prototype.render = function () { showExportPrivateKeyModal, setAccountLabel, keyrings, + rpcPrefs, } = this.props const { name, address } = selectedIdentity @@ -86,8 +88,12 @@ AccountDetailsModal.prototype.render = function () { h(Button, { type: 'secondary', className: 'account-modal__button', - onClick: () => global.platform.openWindow({ url: genAccountLink(address, network) }), - }, this.context.t('etherscanView')), + onClick: () => { + global.platform.openWindow({ url: genAccountLink(address, network, rpcPrefs) }) + }, + }, (rpcPrefs.blockExplorerUrl + ? this.context.t('blockExplorerView', [rpcPrefs.blockExplorerUrl.match(/^https?:\/\/(.+)/)[1]]) + : this.context.t('viewOnEtherscan'))), // Holding on redesign for Export Private Key functionality diff --git a/ui/app/components/app/modals/deposit-ether-modal.js b/ui/app/components/app/modals/deposit-ether-modal.js index 8f7ef792c..f56069d65 100644 --- a/ui/app/components/app/modals/deposit-ether-modal.js +++ b/ui/app/components/app/modals/deposit-ether-modal.js @@ -48,7 +48,7 @@ function mapDispatchToProps (dispatch) { } inherits(DepositEtherModal, Component) -function DepositEtherModal (props, context) { +function DepositEtherModal (_, context) { Component.call(this) // need to set after i18n locale has loaded diff --git a/ui/app/components/app/modals/export-private-key-modal.js b/ui/app/components/app/modals/export-private-key-modal.js index 70987330a..c3098a16c 100644 --- a/ui/app/components/app/modals/export-private-key-modal.js +++ b/ui/app/components/app/modals/export-private-key-modal.js @@ -98,7 +98,7 @@ ExportPrivateKeyModal.prototype.renderPasswordInput = function (privateKey) { }) } -ExportPrivateKeyModal.prototype.renderButtons = function (privateKey, password, address, hideModal) { +ExportPrivateKeyModal.prototype.renderButtons = function (privateKey, address, hideModal) { return h('div.export-private-key-buttons', {}, [ !privateKey && h(Button, { type: 'default', @@ -171,7 +171,7 @@ ExportPrivateKeyModal.prototype.render = function () { h('div.private-key-password-warning', this.context.t('privateKeyWarning')), - this.renderButtons(privateKey, this.state.password, address, hideModal), + this.renderButtons(privateKey, address, hideModal), ]) } diff --git a/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.container.js b/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.container.js index 83595281f..ea7d71a73 100644 --- a/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.container.js +++ b/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.container.js @@ -4,7 +4,7 @@ import MetaMetricsOptInModal from './metametrics-opt-in-modal.component' import withModalProps from '../../../../helpers/higher-order-components/with-modal-props' import { setParticipateInMetaMetrics } from '../../../../store/actions' -const mapStateToProps = (state, ownProps) => { +const mapStateToProps = (_, ownProps) => { const { unapprovedTxCount } = ownProps return { diff --git a/ui/app/components/app/modals/qr-scanner/qr-scanner.component.js b/ui/app/components/app/modals/qr-scanner/qr-scanner.component.js index 20915b5f9..a83ba8f8e 100644 --- a/ui/app/components/app/modals/qr-scanner/qr-scanner.component.js +++ b/ui/app/components/app/modals/qr-scanner/qr-scanner.component.js @@ -71,7 +71,7 @@ export default class QrScanner extends Component { initCamera () { this.codeReader = new BrowserQRCodeReader() this.codeReader.getVideoInputDevices() - .then(videoInputDevices => { + .then(() => { clearTimeout(this.permissionChecker) this.checkPermisisions() this.codeReader.decodeFromInputVideoDevice(undefined, 'video') diff --git a/ui/app/components/app/modals/reject-transactions/reject-transactions.container.js b/ui/app/components/app/modals/reject-transactions/reject-transactions.container.js index d2af05573..aa74fd800 100644 --- a/ui/app/components/app/modals/reject-transactions/reject-transactions.container.js +++ b/ui/app/components/app/modals/reject-transactions/reject-transactions.container.js @@ -3,7 +3,7 @@ import { compose } from 'recompose' import RejectTransactionsModal from './reject-transactions.component' import withModalProps from '../../../../helpers/higher-order-components/with-modal-props' -const mapStateToProps = (state, ownProps) => { +const mapStateToProps = (_, ownProps) => { const { unapprovedTxCount } = ownProps return { diff --git a/ui/app/components/app/provider-page-container/provider-page-container.component.js b/ui/app/components/app/provider-page-container/provider-page-container.component.js index 910def2a3..1c655d404 100644 --- a/ui/app/components/app/provider-page-container/provider-page-container.component.js +++ b/ui/app/components/app/provider-page-container/provider-page-container.component.js @@ -5,12 +5,11 @@ import { PageContainerFooter } from '../../ui/page-container' export default class ProviderPageContainer extends PureComponent { static propTypes = { - approveProviderRequest: PropTypes.func.isRequired, + approveProviderRequestByOrigin: PropTypes.func.isRequired, + rejectProviderRequestByOrigin: PropTypes.func.isRequired, origin: PropTypes.string.isRequired, - rejectProviderRequest: PropTypes.func.isRequired, siteImage: PropTypes.string, siteTitle: PropTypes.string.isRequired, - tabID: PropTypes.string.isRequired, }; static contextTypes = { @@ -29,7 +28,7 @@ export default class ProviderPageContainer extends PureComponent { } onCancel = () => { - const { tabID, rejectProviderRequest } = this.props + const { origin, rejectProviderRequestByOrigin } = this.props this.context.metricsEvent({ eventOpts: { category: 'Auth', @@ -37,11 +36,11 @@ export default class ProviderPageContainer extends PureComponent { name: 'Canceled', }, }) - rejectProviderRequest(tabID) + rejectProviderRequestByOrigin(origin) } onSubmit = () => { - const { approveProviderRequest, tabID } = this.props + const { approveProviderRequestByOrigin, origin } = this.props this.context.metricsEvent({ eventOpts: { category: 'Auth', @@ -49,7 +48,7 @@ export default class ProviderPageContainer extends PureComponent { name: 'Confirmed', }, }) - approveProviderRequest(tabID) + approveProviderRequestByOrigin(origin) } render () { diff --git a/ui/app/components/app/token-cell.js b/ui/app/components/app/token-cell.js index cef809e8a..495b9502b 100644 --- a/ui/app/components/app/token-cell.js +++ b/ui/app/components/app/token-cell.js @@ -155,7 +155,7 @@ TokenCell.prototype.send = function (address, event) { } } -TokenCell.prototype.view = function (address, userAddress, network, event) { +TokenCell.prototype.view = function (address, userAddress, network) { const url = etherscanLinkFor(address, userAddress, network) if (url) { navigateTo(url) diff --git a/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js b/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js index 4a3b04998..72ca784e2 100644 --- a/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js +++ b/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js @@ -1,13 +1,15 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import copyToClipboard from 'copy-to-clipboard' +import { + getBlockExplorerUrlForTx, +} from '../../../helpers/utils/transactions.util' import SenderToRecipient from '../../ui/sender-to-recipient' import { FLAT_VARIANT } from '../../ui/sender-to-recipient/sender-to-recipient.constants' import TransactionActivityLog from '../transaction-activity-log' import TransactionBreakdown from '../transaction-breakdown' import Button from '../../ui/button' import Tooltip from '../../ui/tooltip' -import prefixForNetwork from '../../../../lib/etherscan-prefix-for-network' export default class TransactionListItemDetails extends PureComponent { static contextTypes = { @@ -22,6 +24,7 @@ export default class TransactionListItemDetails extends PureComponent { showRetry: PropTypes.bool, cancelDisabled: PropTypes.bool, transactionGroup: PropTypes.object, + rpcPrefs: PropTypes.object, } state = { @@ -30,12 +33,9 @@ export default class TransactionListItemDetails extends PureComponent { } handleEtherscanClick = () => { - const { transactionGroup: { primaryTransaction } } = this.props + const { transactionGroup: { primaryTransaction }, rpcPrefs } = this.props const { hash, metamaskNetworkId } = primaryTransaction - const prefix = prefixForNetwork(metamaskNetworkId) - const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}` - this.context.metricsEvent({ eventOpts: { category: 'Navigation', @@ -44,7 +44,7 @@ export default class TransactionListItemDetails extends PureComponent { }, }) - global.platform.openWindow({ url: etherscanUrl }) + global.platform.openWindow({ url: getBlockExplorerUrlForTx(metamaskNetworkId, hash, rpcPrefs) }) } handleCancel = event => { @@ -125,6 +125,7 @@ export default class TransactionListItemDetails extends PureComponent { showRetry, onCancel, onRetry, + rpcPrefs: { blockExplorerUrl } = {}, } = this.props const { primaryTransaction: transaction } = transactionGroup const { txParams: { to, from } = {} } = transaction @@ -158,7 +159,7 @@ export default class TransactionListItemDetails extends PureComponent { /> </Button> </Tooltip> - <Tooltip title={t('viewOnEtherscan')}> + <Tooltip title={blockExplorerUrl ? t('viewOnCustomBlockExplorer', [blockExplorerUrl]) : t('viewOnEtherscan')}> <Button type="raised" onClick={this.handleEtherscanClick} diff --git a/ui/app/components/app/transaction-list-item/transaction-list-item.component.js b/ui/app/components/app/transaction-list-item/transaction-list-item.component.js index c7d9dd7c7..0d4127b4f 100644 --- a/ui/app/components/app/transaction-list-item/transaction-list-item.component.js +++ b/ui/app/components/app/transaction-list-item/transaction-list-item.component.js @@ -33,6 +33,7 @@ export default class TransactionListItem extends PureComponent { value: PropTypes.string, fetchBasicGasAndTimeEstimates: PropTypes.func, fetchGasEstimates: PropTypes.func, + rpcPrefs: PropTypes.object, } static defaultProps = { @@ -161,6 +162,7 @@ export default class TransactionListItem extends PureComponent { showRetry, tokenData, transactionGroup, + rpcPrefs, } = this.props const { txParams = {} } = transaction const { showTransactionDetails } = this.state @@ -216,6 +218,7 @@ export default class TransactionListItem extends PureComponent { onCancel={this.handleCancel} showCancel={showCancel} cancelDisabled={!hasEnoughCancelGas} + rpcPrefs={rpcPrefs} /> </div> ) diff --git a/ui/app/components/app/transaction-list-item/transaction-list-item.container.js b/ui/app/components/app/transaction-list-item/transaction-list-item.container.js index a8fb8c246..5e88a2937 100644 --- a/ui/app/components/app/transaction-list-item/transaction-list-item.container.js +++ b/ui/app/components/app/transaction-list-item/transaction-list-item.container.js @@ -18,12 +18,14 @@ import { getIsMainnet, preferencesSelector, getSelectedAddress, conversionRateSe import { isBalanceSufficient } from '../../../pages/send/send.utils' const mapStateToProps = (state, ownProps) => { - const { metamask: { knownMethodData, accounts } } = state + const { metamask: { knownMethodData, accounts, provider, frequentRpcListDetail } } = state const { showFiatInTestnets } = preferencesSelector(state) const isMainnet = getIsMainnet(state) const { transactionGroup: { primaryTransaction } = {} } = ownProps const { txParams: { gas: gasLimit, gasPrice } = {} } = primaryTransaction const selectedAccountBalance = accounts[getSelectedAddress(state)].balance + const selectRpcInfo = frequentRpcListDetail.find(rpcInfo => rpcInfo.rpcUrl === provider.rpcTarget) + const { rpcPrefs } = selectRpcInfo || {} const hasEnoughCancelGas = primaryTransaction.txParams && isBalanceSufficient({ amount: '0x0', @@ -40,6 +42,7 @@ const mapStateToProps = (state, ownProps) => { showFiat: (isMainnet || !!showFiatInTestnets), selectedAccountBalance, hasEnoughCancelGas, + rpcPrefs, } } diff --git a/ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js b/ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js index 88d63baae..4ecc0dabb 100644 --- a/ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js +++ b/ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js @@ -5,7 +5,7 @@ let mapStateToProps, mergeProps proxyquire('../user-preferenced-currency-display.container.js', { 'react-redux': { - connect: (ms, md, mp) => { + connect: (ms, _, mp) => { mapStateToProps = ms mergeProps = mp return () => ({}) diff --git a/ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.container.js b/ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.container.js index 42d156f92..2a4635955 100644 --- a/ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.container.js +++ b/ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.container.js @@ -3,7 +3,7 @@ import UserPreferencedCurrencyDisplay from './user-preferenced-currency-display. import { preferencesSelector, getIsMainnet } from '../../../selectors/selectors' import { ETH, PRIMARY, SECONDARY } from '../../../helpers/constants/common' -const mapStateToProps = (state, ownProps) => { +const mapStateToProps = (state) => { const { useNativeCurrencyAsPrimaryCurrency, showFiatInTestnets, diff --git a/ui/app/components/ui/alert/index.js b/ui/app/components/ui/alert/index.js index 5620d847a..b1229f502 100644 --- a/ui/app/components/ui/alert/index.js +++ b/ui/app/components/ui/alert/index.js @@ -18,7 +18,7 @@ class Alert extends Component { if (!this.props.visible && nextProps.visible) { this.animateIn(nextProps) } else if (this.props.visible && !nextProps.visible) { - this.animateOut(nextProps) + this.animateOut() } } @@ -30,7 +30,7 @@ class Alert extends Component { }) } - animateOut (props) { + animateOut () { this.setState({ msg: null, className: '.hidden', diff --git a/ui/app/components/ui/currency-display/currency-display.component.js b/ui/app/components/ui/currency-display/currency-display.component.js index 04dd89892..c15668da3 100644 --- a/ui/app/components/ui/currency-display/currency-display.component.js +++ b/ui/app/components/ui/currency-display/currency-display.component.js @@ -23,7 +23,7 @@ export default class CurrencyDisplay extends PureComponent { render () { const { className, displayValue, prefix, prefixComponent, style, suffix, hideTitle } = this.props const text = `${prefix || ''}${displayValue}` - const title = `${text} ${suffix}` + const title = suffix ? `${text} ${suffix}` : text return ( <div diff --git a/ui/app/components/ui/currency-display/tests/currency-display.container.test.js b/ui/app/components/ui/currency-display/tests/currency-display.container.test.js index 9888c366e..182524e59 100644 --- a/ui/app/components/ui/currency-display/tests/currency-display.container.test.js +++ b/ui/app/components/ui/currency-display/tests/currency-display.container.test.js @@ -5,7 +5,7 @@ let mapStateToProps, mergeProps proxyquire('../currency-display.container.js', { 'react-redux': { - connect: (ms, md, mp) => { + connect: (ms, _, mp) => { mapStateToProps = ms mergeProps = mp return () => ({}) diff --git a/ui/app/components/ui/currency-input/tests/currency-input.container.test.js b/ui/app/components/ui/currency-input/tests/currency-input.container.test.js index 6109d29b6..259fe594a 100644 --- a/ui/app/components/ui/currency-input/tests/currency-input.container.test.js +++ b/ui/app/components/ui/currency-input/tests/currency-input.container.test.js @@ -5,7 +5,7 @@ let mapStateToProps, mergeProps proxyquire('../currency-input.container.js', { 'react-redux': { - connect: (ms, md, mp) => { + connect: (ms, _, mp) => { mapStateToProps = ms mergeProps = mp return () => ({}) diff --git a/ui/app/components/ui/text-field/text-field.component.js b/ui/app/components/ui/text-field/text-field.component.js index 2c72d8124..1153a595b 100644 --- a/ui/app/components/ui/text-field/text-field.component.js +++ b/ui/app/components/ui/text-field/text-field.component.js @@ -41,11 +41,11 @@ const styles = { inputFocused: {}, inputRoot: { 'label + &': { - marginTop: '8px', + marginTop: '9px', }, - border: '1px solid #d2d8dd', + border: '2px solid #BBC0C5', height: '48px', - borderRadius: '4px', + borderRadius: '6px', padding: '0 16px', display: 'flex', alignItems: 'center', diff --git a/ui/app/components/ui/token-input/tests/token-input.container.test.js b/ui/app/components/ui/token-input/tests/token-input.container.test.js index 2b1c102c8..6f87e64a5 100644 --- a/ui/app/components/ui/token-input/tests/token-input.container.test.js +++ b/ui/app/components/ui/token-input/tests/token-input.container.test.js @@ -5,7 +5,7 @@ let mapStateToProps, mergeProps proxyquire('../token-input.container.js', { 'react-redux': { - connect: (ms, md, mp) => { + connect: (ms, _, mp) => { mapStateToProps = ms mergeProps = mp return () => ({}) diff --git a/ui/app/components/ui/unit-input/unit-input.component.js b/ui/app/components/ui/unit-input/unit-input.component.js index c5f8350a6..6a53f4c6f 100644 --- a/ui/app/components/ui/unit-input/unit-input.component.js +++ b/ui/app/components/ui/unit-input/unit-input.component.js @@ -58,7 +58,7 @@ export default class UnitInput extends PureComponent { this.props.onChange(value) } - handleBlur = event => { + handleBlur = () => { const { onBlur } = this.props typeof onBlur === 'function' && onBlur(this.state.value) } diff --git a/ui/app/ducks/app/app.js b/ui/app/ducks/app/app.js index 295507d70..b181092c1 100644 --- a/ui/app/ducks/app/app.js +++ b/ui/app/ducks/app/app.js @@ -77,6 +77,8 @@ function reduceApp (state, action) { ledger: `m/44'/60'/0'/0/0`, }, lastSelectedProvider: null, + networksTabSelectedRpcUrl: '', + networksTabIsInAddMode: false, }, state.appState) switch (action.type) { @@ -751,6 +753,16 @@ function reduceApp (state, action) { lastSelectedProvider: action.value, }) + case actions.SET_SELECTED_SETTINGS_RPC_URL: + return extend(appState, { + networksTabSelectedRpcUrl: action.value, + }) + + case actions.SET_NETWORKS_TAB_ADD_MODE: + return extend(appState, { + networksTabIsInAddMode: action.value, + }) + default: return appState } diff --git a/ui/app/ducks/confirm-transaction/confirm-transaction.duck.js b/ui/app/ducks/confirm-transaction/confirm-transaction.duck.js index 169c9d543..58b0ec8e8 100644 --- a/ui/app/ducks/confirm-transaction/confirm-transaction.duck.js +++ b/ui/app/ducks/confirm-transaction/confirm-transaction.duck.js @@ -375,7 +375,7 @@ export function setTransactionToConfirm (transactionId) { dispatch(updateMethodData(methodData)) try { - const toSmartContract = await isSmartContractAddress(to) + const toSmartContract = await isSmartContractAddress(to || '') dispatch(updateToSmartContract(toSmartContract)) } catch (error) { log.error(error) diff --git a/ui/app/ducks/confirm-transaction/confirm-transaction.duck.test.js b/ui/app/ducks/confirm-transaction/confirm-transaction.duck.test.js index 483f2f56d..d2e344663 100644 --- a/ui/app/ducks/confirm-transaction/confirm-transaction.duck.test.js +++ b/ui/app/ducks/confirm-transaction/confirm-transaction.duck.test.js @@ -494,7 +494,7 @@ describe('Confirm Transaction Duck', () => { }) }) - describe('Thunk actions', done => { + describe('Thunk actions', () => { beforeEach(() => { global.eth = { getCode: sinon.stub().callsFake( diff --git a/ui/app/ducks/gas/gas-duck.test.js b/ui/app/ducks/gas/gas-duck.test.js index c0152c74f..b7e83a81c 100644 --- a/ui/app/ducks/gas/gas-duck.test.js +++ b/ui/app/ducks/gas/gas-duck.test.js @@ -461,8 +461,8 @@ describe('Gas Duck', () => { assert.equal(thirdDispatchCallType, SET_PRICE_AND_TIME_ESTIMATES) assert(priceAndTimeEstimateResult.length < mockPredictTableResponse.length * 3 - 2) assert(!priceAndTimeEstimateResult.find(d => d.expectedTime > 100)) - assert(!priceAndTimeEstimateResult.find((d, i, a) => a[a + 1] && d.expectedTime > a[a + 1].expectedTime)) - assert(!priceAndTimeEstimateResult.find((d, i, a) => a[a + 1] && d.gasprice > a[a + 1].gasprice)) + assert(!priceAndTimeEstimateResult.find((d, _, a) => a[a + 1] && d.expectedTime > a[a + 1].expectedTime)) + assert(!priceAndTimeEstimateResult.find((d, _, a) => a[a + 1] && d.gasprice > a[a + 1].gasprice)) assert.deepEqual( mockDistpatch.getCall(3).args, diff --git a/ui/app/helpers/constants/routes.js b/ui/app/helpers/constants/routes.js index df35112d1..d906fc8e6 100644 --- a/ui/app/helpers/constants/routes.js +++ b/ui/app/helpers/constants/routes.js @@ -8,6 +8,7 @@ const ADVANCED_ROUTE = '/settings/advanced' const SECURITY_ROUTE = '/settings/security' const COMPANY_ROUTE = '/settings/company' const ABOUT_US_ROUTE = '/settings/about-us' +const NETWORKS_ROUTE = '/settings/networks' const REVEAL_SEED_ROUTE = '/seed' const MOBILE_SYNC_ROUTE = '/mobile-sync' const CONFIRM_SEED_ROUTE = '/confirm-seed' @@ -86,4 +87,5 @@ module.exports = { COMPANY_ROUTE, GENERAL_ROUTE, ABOUT_US_ROUTE, + NETWORKS_ROUTE, } diff --git a/ui/app/helpers/higher-order-components/i18n-provider.js b/ui/app/helpers/higher-order-components/i18n-provider.js index 0e34e17e0..5a6650147 100644 --- a/ui/app/helpers/higher-order-components/i18n-provider.js +++ b/ui/app/helpers/higher-order-components/i18n-provider.js @@ -15,11 +15,21 @@ class I18nProvider extends Component { const { localeMessages } = this.props const { current, en } = localeMessages return { + /** + * Returns a localized message for the given key + * @param {string} key The message key + * @param {string[]} args A list of message substitution replacements + * @return {string|undefined|null} The localized message if available + */ t (key, ...args) { + if (key === undefined || key === null) { + return key + } + return t(current, key, ...args) || t(en, key, ...args) || `[${key}]` }, tOrDefault: this.tOrDefault, - tOrKey (key, ...args) { + tOrKey: (key, ...args) => { return this.tOrDefault(key, key, ...args) }, } diff --git a/ui/app/helpers/higher-order-components/metametrics/metametrics.provider.js b/ui/app/helpers/higher-order-components/metametrics/metametrics.provider.js index 6086e03fb..6281ddcc6 100644 --- a/ui/app/helpers/higher-order-components/metametrics/metametrics.provider.js +++ b/ui/app/helpers/higher-order-components/metametrics/metametrics.provider.js @@ -42,7 +42,7 @@ class MetaMetricsProvider extends Component { currentPath: window.location.href, } - props.history.listen(locationObj => { + props.history.listen(() => { this.setState({ previousPath: this.state.currentPath, currentPath: window.location.href, diff --git a/ui/app/helpers/higher-order-components/with-modal-props/tests/with-modal-props.test.js b/ui/app/helpers/higher-order-components/with-modal-props/tests/with-modal-props.test.js index 654e7062a..81a3512d1 100644 --- a/ui/app/helpers/higher-order-components/with-modal-props/tests/with-modal-props.test.js +++ b/ui/app/helpers/higher-order-components/with-modal-props/tests/with-modal-props.test.js @@ -21,7 +21,7 @@ const mockState = { describe('withModalProps', () => { it('should return a component wrapped with modal state props', () => { - const TestComponent = props => ( + const TestComponent = () => ( <div className="test">Testing</div> ) const WrappedComponent = withModalProps(TestComponent) diff --git a/ui/app/helpers/utils/conversion-util.js b/ui/app/helpers/utils/conversion-util.js index 8cc531773..affddade7 100644 --- a/ui/app/helpers/utils/conversion-util.js +++ b/ui/app/helpers/utils/conversion-util.js @@ -42,7 +42,7 @@ const convert = R.invoker(1, 'times') const round = R.invoker(2, 'round')(R.__, BigNumber.ROUND_HALF_DOWN) const roundDown = R.invoker(2, 'round')(R.__, BigNumber.ROUND_DOWN) const invertConversionRate = conversionRate => () => new BigNumber(1.0).div(conversionRate) -const decToBigNumberViaString = n => R.pipe(String, toBigNumber['dec']) +const decToBigNumberViaString = () => R.pipe(String, toBigNumber['dec']) // Setter Maps const toBigNumber = { diff --git a/ui/app/helpers/utils/metametrics.util.js b/ui/app/helpers/utils/metametrics.util.js index 5ae3e8937..cafbd5c07 100644 --- a/ui/app/helpers/utils/metametrics.util.js +++ b/ui/app/helpers/utils/metametrics.util.js @@ -84,7 +84,7 @@ function composeParamAddition (paramValue, paramName) { : `&${paramName}=${paramValue}` } -function composeUrl (config, permissionPreferences = {}) { +function composeUrl (config) { const { eventOpts = {}, customVariables = '', @@ -124,10 +124,10 @@ function composeUrl (config, permissionPreferences = {}) { numberOfTokens: customVariables && customVariables.numberOfTokens || numberOfTokens, numberOfAccounts: customVariables && customVariables.numberOfAccounts || numberOfAccounts, }) : '' - const url = configUrl || `&url=${encodeURIComponent(currentPath.replace(/chrome-extension:\/\/\w+/, METAMETRICS_TRACKING_URL))}` + const url = configUrl || currentPath ? `&url=${encodeURIComponent(currentPath.replace(/chrome-extension:\/\/\w+/, METAMETRICS_TRACKING_URL))}` : '' const _id = metaMetricsId && !excludeMetaMetricsId ? `&_id=${metaMetricsId.slice(2, 18)}` : '' const rand = `&rand=${String(Math.random()).slice(2)}` - const pv_id = `&pv_id=${ethUtil.bufferToHex(ethUtil.sha3(url || currentPath.match(/chrome-extension:\/\/\w+\/(.+)/)[0])).slice(2, 8)}` + const pv_id = (url || currentPath) && `&pv_id=${ethUtil.bufferToHex(ethUtil.sha3(url || currentPath.match(/chrome-extension:\/\/\w+\/(.+)/)[0])).slice(2, 8)}` || '' const uid = metaMetricsId && !excludeMetaMetricsId ? `&uid=${metaMetricsId.slice(2, 18)}` : excludeMetaMetricsId diff --git a/ui/app/helpers/utils/transactions.util.js b/ui/app/helpers/utils/transactions.util.js index cb6c9536c..99ccc3478 100644 --- a/ui/app/helpers/utils/transactions.util.js +++ b/ui/app/helpers/utils/transactions.util.js @@ -6,6 +6,8 @@ import { TRANSACTION_TYPE_CANCEL, TRANSACTION_STATUS_CONFIRMED, } from '../../../../app/scripts/controllers/transactions/enums' +import prefixForNetwork from '../../../lib/etherscan-prefix-for-network' + import { TOKEN_METHOD_TRANSFER, @@ -188,3 +190,17 @@ export function getStatusKey (transaction) { return transaction.status } + +/** + * Returns an external block explorer URL at which a transaction can be viewed. + * @param {number} networkId + * @param {string} hash + * @param {Object} rpcPrefs + */ +export function getBlockExplorerUrlForTx (networkId, hash, rpcPrefs = {}) { + if (rpcPrefs.blockExplorerUrl) { + return `${rpcPrefs.blockExplorerUrl}/tx/${hash}` + } + const prefix = prefixForNetwork(networkId) + return `https://${prefix}etherscan.io/tx/${hash}` +} diff --git a/ui/app/helpers/utils/util.js b/ui/app/helpers/utils/util.js index c50d7cbe5..94fa9ad42 100644 --- a/ui/app/helpers/utils/util.js +++ b/ui/app/helpers/utils/util.js @@ -92,7 +92,7 @@ function miniAddressSummary (address) { return checked ? checked.slice(0, 4) + '...' + checked.slice(-4) : '...' } -function isValidAddress (address, network) { +function isValidAddress (address) { var prefixed = ethUtil.addHexPrefix(address) if (address === '0x0000000000000000000000000000000000000000') return false return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed) @@ -268,7 +268,7 @@ function bnMultiplyByFraction (targetBN, numerator, denominator) { return targetBN.mul(numBN).div(denomBN) } -function getTxFeeBn (gas, gasPrice = MIN_GAS_PRICE_BN.toString(16), blockGasLimit) { +function getTxFeeBn (gas, gasPrice = MIN_GAS_PRICE_BN.toString(16)) { const gasBn = hexToBn(gas) const gasPriceBn = hexToBn(gasPrice) const txFeeBn = gasBn.mul(gasPriceBn) @@ -297,7 +297,7 @@ function exportAsFile (filename, data, type = 'text/csv') { } function allNull (obj) { - return Object.entries(obj).every(([key, value]) => value === null) + return Object.entries(obj).every(([_, value]) => value === null) } function getTokenAddressFromTokenObject (token) { @@ -308,11 +308,10 @@ function getTokenAddressFromTokenObject (token) { * Safely checksumms a potentially-null address * * @param {String} [address] - address to checksum - * @param {String} [network] - network id * @returns {String} - checksummed address * */ -function checksumAddress (address, network) { +function checksumAddress (address) { const checksummed = address ? ethUtil.toChecksumAddress(address) : '' return checksummed } diff --git a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js index 1cbe5951d..3c4e6dcac 100644 --- a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -18,6 +18,7 @@ import AdvancedGasInputs from '../../components/app/gas-customization/advanced-g export default class ConfirmTransactionBase extends Component { static contextTypes = { t: PropTypes.func, + tOrKey: PropTypes.func.isRequired, metricsEvent: PropTypes.func, } @@ -99,15 +100,18 @@ export default class ConfirmTransactionBase extends Component { submitError: null, } - componentDidUpdate () { + componentDidUpdate (prevProps) { const { transactionStatus, showTransactionConfirmedModal, history, clearConfirmTransaction, } = this.props + const { transactionStatus: prevTxStatus } = prevProps + const statusUpdated = transactionStatus !== prevTxStatus + const txDroppedOrConfirmed = transactionStatus === DROPPED_STATUS || transactionStatus === CONFIRMED_STATUS - if (transactionStatus === DROPPED_STATUS || transactionStatus === CONFIRMED_STATUS) { + if (statusUpdated && txDroppedOrConfirmed) { showTransactionConfirmedModal({ onSubmit: () => { clearConfirmTransaction() @@ -543,7 +547,8 @@ export default class ConfirmTransactionBase extends Component { toName={toName} toAddress={toAddress} showEdit={onEdit && !isTxReprice} - action={this.context.t(actionKey) || getMethodName(name) || this.context.t('contractInteraction')} + // In the event that the key is falsy (and inherently invalid), use a fallback string + action={this.context.tOrKey(actionKey) || getMethodName(name) || this.context.t('contractInteraction')} title={title} titleComponent={this.renderTitleComponent()} subtitle={subtitle} diff --git a/ui/app/pages/create-account/connect-hardware/account-list.js b/ui/app/pages/create-account/connect-hardware/account-list.js index a521c7eaf..247c27a5d 100644 --- a/ui/app/pages/create-account/connect-hardware/account-list.js +++ b/ui/app/pages/create-account/connect-hardware/account-list.js @@ -6,10 +6,6 @@ const Select = require('react-select').default import Button from '../../../components/ui/button' class AccountList extends Component { - constructor (props, context) { - super(props) - } - getHdPaths () { return [ { diff --git a/ui/app/pages/create-account/connect-hardware/connect-screen.js b/ui/app/pages/create-account/connect-hardware/connect-screen.js index f5a83e6cf..a3b8ad246 100644 --- a/ui/app/pages/create-account/connect-hardware/connect-screen.js +++ b/ui/app/pages/create-account/connect-hardware/connect-screen.js @@ -4,7 +4,7 @@ const h = require('react-hyperscript') import Button from '../../../components/ui/button' class ConnectScreen extends Component { - constructor (props, context) { + constructor (props) { super(props) this.state = { selectedDevice: null, @@ -103,7 +103,7 @@ class ConnectScreen extends Component { } - scrollToTutorial = (e) => { + scrollToTutorial = () => { if (this.referenceNode) this.referenceNode.scrollIntoView({behavior: 'smooth'}) } diff --git a/ui/app/pages/create-account/connect-hardware/index.js b/ui/app/pages/create-account/connect-hardware/index.js index 1398fa680..5a91a2725 100644 --- a/ui/app/pages/create-account/connect-hardware/index.js +++ b/ui/app/pages/create-account/connect-hardware/index.js @@ -12,7 +12,7 @@ const { getPlatform } = require('../../../../../app/scripts/lib/util') const { PLATFORM_FIREFOX } = require('../../../../../app/scripts/lib/enums') class ConnectHardwareForm extends Component { - constructor (props, context) { + constructor (props) { super(props) this.state = { error: null, @@ -101,7 +101,7 @@ class ConnectHardwareForm extends Component { const newState = { unlocked: true, device, error: null } // Default to the first account if (this.state.selectedAccount === null) { - accounts.forEach((a, i) => { + accounts.forEach((a) => { if (a.address.toLowerCase() === this.props.address) { newState.selectedAccount = a.index.toString() } diff --git a/ui/app/pages/create-account/import-account/seed.js b/ui/app/pages/create-account/import-account/seed.js index d98909baa..73332f926 100644 --- a/ui/app/pages/create-account/import-account/seed.js +++ b/ui/app/pages/create-account/import-account/seed.js @@ -11,7 +11,7 @@ SeedImportSubview.contextTypes = { module.exports = connect(mapStateToProps)(SeedImportSubview) -function mapStateToProps (state) { +function mapStateToProps () { return {} } diff --git a/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js b/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js index ffaff9acf..6b9d06cf9 100644 --- a/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js +++ b/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js @@ -119,7 +119,7 @@ export default class MetaMetricsOptIn extends Component { hideCancel={false} onSubmit={() => { setParticipateInMetaMetrics(true) - .then(([participateStatus, metaMetricsId]) => { + .then(([_, metaMetricsId]) => { const promise = participateInMetaMetrics !== true ? metricsEvent({ eventOpts: { diff --git a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js index f3bfc3171..4cfc38fdf 100644 --- a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js +++ b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js @@ -8,7 +8,9 @@ import { INITIALIZE_SEED_PHRASE_ROUTE, } from '../../../../helpers/constants/routes' import { exportAsFile } from '../../../../helpers/utils/util' -import { selectSeedWord, deselectSeedWord } from './confirm-seed-phrase.state' +import DraggableSeed from './draggable-seed.component' + +const EMPTY_SEEDS = Array(12).fill(null) export default class ConfirmSeedPhrase extends PureComponent { static contextTypes = { @@ -27,10 +29,32 @@ export default class ConfirmSeedPhrase extends PureComponent { } state = { - selectedSeedWords: [], + selectedSeedIndices: [], shuffledSeedWords: [], - // Hash of shuffledSeedWords index {Number} to selectedSeedWords index {Number} - selectedSeedWordsHash: {}, + pendingSeedIndices: [], + draggingSeedIndex: -1, + hoveringIndex: -1, + isDragging: false, + } + + shouldComponentUpdate (nextProps, nextState) { + const { seedPhrase } = this.props + const { + selectedSeedIndices, + shuffledSeedWords, + pendingSeedIndices, + draggingSeedIndex, + hoveringIndex, + isDragging, + } = this.state + + return seedPhrase !== nextProps.seedPhrase || + draggingSeedIndex !== nextState.draggingSeedIndex || + isDragging !== nextState.isDragging || + hoveringIndex !== nextState.hoveringIndex || + selectedSeedIndices.join(' ') !== nextState.selectedSeedIndices.join(' ') || + shuffledSeedWords.join(' ') !== nextState.shuffledSeedWords.join(' ') || + pendingSeedIndices.join(' ') !== nextState.pendingSeedIndices.join(' ') } componentDidMount () { @@ -39,6 +63,26 @@ export default class ConfirmSeedPhrase extends PureComponent { this.setState({ shuffledSeedWords }) } + setDraggingSeedIndex = draggingSeedIndex => this.setState({ draggingSeedIndex }) + + setHoveringIndex = hoveringIndex => this.setState({ hoveringIndex }) + + onDrop = targetIndex => { + const { + selectedSeedIndices, + draggingSeedIndex, + } = this.state + + const indices = insert(selectedSeedIndices, draggingSeedIndex, targetIndex, true) + + this.setState({ + selectedSeedIndices: indices, + pendingSeedIndices: indices, + draggingSeedIndex: -1, + hoveringIndex: -1, + }) + } + handleExport = () => { exportAsFile('MetaMask Secret Backup Phrase', this.props.seedPhrase, 'text/plain') } @@ -64,24 +108,35 @@ export default class ConfirmSeedPhrase extends PureComponent { } } - handleSelectSeedWord = (word, shuffledIndex) => { - this.setState(selectSeedWord(word, shuffledIndex)) + handleSelectSeedWord = (shuffledIndex) => { + this.setState({ + selectedSeedIndices: [...this.state.selectedSeedIndices, shuffledIndex], + pendingSeedIndices: [...this.state.pendingSeedIndices, shuffledIndex], + }) } handleDeselectSeedWord = shuffledIndex => { - this.setState(deselectSeedWord(shuffledIndex)) + this.setState({ + selectedSeedIndices: this.state.selectedSeedIndices.filter(i => shuffledIndex !== i), + pendingSeedIndices: this.state.pendingSeedIndices.filter(i => shuffledIndex !== i), + }) } isValid () { const { seedPhrase } = this.props - const { selectedSeedWords } = this.state + const { selectedSeedIndices, shuffledSeedWords } = this.state + const selectedSeedWords = selectedSeedIndices.map(i => shuffledSeedWords[i]) return seedPhrase === selectedSeedWords.join(' ') } render () { const { t } = this.context const { history } = this.props - const { selectedSeedWords, shuffledSeedWords, selectedSeedWordsHash } = this.state + const { + selectedSeedIndices, + shuffledSeedWords, + draggingSeedIndex, + } = this.state return ( <div className="confirm-seed-phrase"> @@ -102,41 +157,39 @@ export default class ConfirmSeedPhrase extends PureComponent { <div className="first-time-flow__text-block"> { t('selectEachPhrase') } </div> - <div className="confirm-seed-phrase__selected-seed-words"> - { - selectedSeedWords.map((word, index) => ( - <div - key={index} - className="confirm-seed-phrase__seed-word" - > - { word } - </div> - )) - } + <div + className={classnames('confirm-seed-phrase__selected-seed-words', { + 'confirm-seed-phrase__selected-seed-words--dragging': draggingSeedIndex > -1, + })} + > + { this.renderPendingSeeds() } + { this.renderSelectedSeeds() } </div> <div className="confirm-seed-phrase__shuffled-seed-words"> { shuffledSeedWords.map((word, index) => { - const isSelected = index in selectedSeedWordsHash + const isSelected = selectedSeedIndices.includes(index) return ( - <div + <DraggableSeed key={index} - className={classnames( - 'confirm-seed-phrase__seed-word', - 'confirm-seed-phrase__seed-word--shuffled', - { 'confirm-seed-phrase__seed-word--selected': isSelected } - )} + seedIndex={index} + index={index} + draggingSeedIndex={this.state.draggingSeedIndex} + setDraggingSeedIndex={this.setDraggingSeedIndex} + setHoveringIndex={this.setHoveringIndex} + onDrop={this.onDrop} + className="confirm-seed-phrase__seed-word--shuffled" + selected={isSelected} onClick={() => { if (!isSelected) { - this.handleSelectSeedWord(word, index) + this.handleSelectSeedWord(index) } else { this.handleDeselectSeedWord(index) } }} - > - { word } - </div> + word={word} + /> ) }) } @@ -152,4 +205,80 @@ export default class ConfirmSeedPhrase extends PureComponent { </div> ) } + + renderSelectedSeeds () { + const { shuffledSeedWords, selectedSeedIndices, draggingSeedIndex } = this.state + return EMPTY_SEEDS.map((_, index) => { + const seedIndex = selectedSeedIndices[index] + const word = shuffledSeedWords[seedIndex] + + return ( + <DraggableSeed + key={`selected-${seedIndex}-${index}`} + className="confirm-seed-phrase__selected-seed-words__selected-seed" + index={index} + seedIndex={seedIndex} + word={word} + draggingSeedIndex={draggingSeedIndex} + setDraggingSeedIndex={this.setDraggingSeedIndex} + setHoveringIndex={this.setHoveringIndex} + onDrop={this.onDrop} + draggable + /> + ) + }) + } + + renderPendingSeeds () { + const { + pendingSeedIndices, + shuffledSeedWords, + draggingSeedIndex, + hoveringIndex, + } = this.state + + const indices = insert(pendingSeedIndices, draggingSeedIndex, hoveringIndex) + + return EMPTY_SEEDS.map((_, index) => { + const seedIndex = indices[index] + const word = shuffledSeedWords[seedIndex] + + return ( + <DraggableSeed + key={`pending-${seedIndex}-${index}`} + index={index} + className={classnames('confirm-seed-phrase__selected-seed-words__pending-seed', { + 'confirm-seed-phrase__seed-word--hidden': draggingSeedIndex === seedIndex && index !== hoveringIndex, + })} + seedIndex={seedIndex} + word={word} + draggingSeedIndex={draggingSeedIndex} + setDraggingSeedIndex={this.setDraggingSeedIndex} + setHoveringIndex={this.setHoveringIndex} + onDrop={this.onDrop} + droppable={!!word} + /> + ) + }) + } +} + +function insert (list, value, target, removeOld) { + let nextList = [...list] + + if (typeof list[target] === 'number') { + nextList = [...list.slice(0, target), value, ...list.slice(target)] + } + + if (removeOld) { + nextList = nextList.filter((seed, i) => { + return seed !== value || i === target + }) + } + + if (nextList.length > 12) { + nextList.pop() + } + + return nextList } diff --git a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js deleted file mode 100644 index f2476fc5c..000000000 --- a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js +++ /dev/null @@ -1,41 +0,0 @@ -export function selectSeedWord (word, shuffledIndex) { - return function update (state) { - const { selectedSeedWords, selectedSeedWordsHash } = state - const nextSelectedIndex = selectedSeedWords.length - - return { - selectedSeedWords: [ ...selectedSeedWords, word ], - selectedSeedWordsHash: { ...selectedSeedWordsHash, [shuffledIndex]: nextSelectedIndex }, - } - } -} - -export function deselectSeedWord (shuffledIndex) { - return function update (state) { - const { - selectedSeedWords: prevSelectedSeedWords, - selectedSeedWordsHash: prevSelectedSeedWordsHash, - } = state - - const selectedSeedWords = [...prevSelectedSeedWords] - const indexToRemove = prevSelectedSeedWordsHash[shuffledIndex] - selectedSeedWords.splice(indexToRemove, 1) - const selectedSeedWordsHash = Object.keys(prevSelectedSeedWordsHash).reduce((acc, index) => { - const output = { ...acc } - const selectedSeedWordIndex = prevSelectedSeedWordsHash[index] - - if (selectedSeedWordIndex < indexToRemove) { - output[index] = selectedSeedWordIndex - } else if (selectedSeedWordIndex > indexToRemove) { - output[index] = selectedSeedWordIndex - 1 - } - - return output - }, {}) - - return { - selectedSeedWords, - selectedSeedWordsHash, - } - } -} diff --git a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/draggable-seed.component.js b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/draggable-seed.component.js new file mode 100644 index 000000000..cdb881921 --- /dev/null +++ b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/draggable-seed.component.js @@ -0,0 +1,126 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import { DragSource, DropTarget } from 'react-dnd' + +class DraggableSeed extends Component { + + static propTypes = { + // React DnD Props + connectDragSource: PropTypes.func.isRequired, + connectDropTarget: PropTypes.func.isRequired, + isDragging: PropTypes.bool, + isOver: PropTypes.bool, + canDrop: PropTypes.bool, + // Own Props + onClick: PropTypes.func.isRequired, + setHoveringIndex: PropTypes.func.isRequired, + index: PropTypes.number, + draggingSeedIndex: PropTypes.number, + word: PropTypes.string, + className: PropTypes.string, + selected: PropTypes.bool, + droppable: PropTypes.bool, + } + + static defaultProps = { + className: '', + onClick () {}, + } + + componentWillReceiveProps (nextProps) { + const { isOver, setHoveringIndex } = this.props + if (isOver && !nextProps.isOver) { + setHoveringIndex(-1) + } + } + + render () { + const { + connectDragSource, + connectDropTarget, + isDragging, + index, + word, + selected, + className, + onClick, + isOver, + canDrop, + } = this.props + + return connectDropTarget(connectDragSource( + <div + key={index} + className={classnames('btn-secondary confirm-seed-phrase__seed-word', className, { + 'confirm-seed-phrase__seed-word--selected btn-primary': selected, + 'confirm-seed-phrase__seed-word--dragging': isDragging, + 'confirm-seed-phrase__seed-word--empty': !word, + 'confirm-seed-phrase__seed-word--active-drop': !isOver && canDrop, + 'confirm-seed-phrase__seed-word--drop-hover': isOver && canDrop, + })} + onClick={onClick} + > + { word } + </div> + )) + } +} + +const SEEDWORD = 'SEEDWORD' + +const seedSource = { + beginDrag (props) { + setTimeout(() => props.setDraggingSeedIndex(props.seedIndex), 0) + return { + seedIndex: props.seedIndex, + word: props.word, + } + }, + canDrag (props) { + return props.draggable + }, + endDrag (props, monitor) { + const dropTarget = monitor.getDropResult() + + if (!dropTarget) { + setTimeout(() => props.setDraggingSeedIndex(-1), 0) + return + } + + props.onDrop(dropTarget.targetIndex) + }, +} + +const seedTarget = { + drop (props) { + return { + targetIndex: props.index, + } + }, + canDrop (props) { + return props.droppable + }, + hover (props) { + props.setHoveringIndex(props.index) + }, +} + +const collectDrag = (connect, monitor) => { + return { + connectDragSource: connect.dragSource(), + isDragging: monitor.isDragging(), + } +} + +const collectDrop = (connect, monitor) => { + return { + connectDropTarget: connect.dropTarget(), + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), + } +} + +export default DropTarget(SEEDWORD, seedTarget, collectDrop)(DragSource(SEEDWORD, seedSource, collectDrag)(DraggableSeed)) + + diff --git a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss index 93137618c..f025a503f 100644 --- a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss +++ b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss @@ -3,37 +3,58 @@ margin-bottom: 12px; } - &__selected-seed-words { - min-height: 190px; - max-width: 496px; - border: 1px solid #CDCDCD; - border-radius: 6px; - background-color: $white; - margin: 24px 0 36px; - padding: 12px; - } - &__shuffled-seed-words { - max-width: 496px; + max-width: 575px; } &__seed-word { - display: inline-block; - color: #5B5D67; - background-color: #E7E7E7; + display: inline-flex; + flex-flow: row nowrap; + align-items: center; + justify-content: center; padding: 8px 18px; - min-width: 64px; + width: 128px; + height: 41px; margin: 4px; text-align: center; + border-radius: 4px; + cursor: move; + + &--shuffled { + cursor: pointer; + margin: 6px; + } &--selected { - background-color: #85D1CC; color: $white; } - &--shuffled { - cursor: pointer; - margin: 6px; + &--dragging { + margin: 0; + } + + &--empty { + background-color: transparent; + border-color: transparent; + cursor: default; + + &:hover, + &:active { + background-color: transparent; + border-color: transparent; + cursor: default; + box-shadow: none !important; + } + } + + &--hidden { + display: none !important; + } + + &--drop-hover { + background-color: transparent; + border-color: transparent; + color: transparent; } @media screen and (max-width: 575px) { @@ -42,7 +63,37 @@ } } - button { - margin-top: 0xp; + &__selected-seed-words { + display: flex; + flex-flow: row wrap; + min-height: 161px; + max-width: 575px; + border: 1px solid #CDCDCD; + border-radius: 6px; + background-color: $white; + margin: 24px 0 36px; + padding: 12px; + + &__pending-seed { + display: none; + } + + &__selected-seed { + display: inline-flex; + + &:hover { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25); + } + } + + &--dragging { + .confirm-seed-phrase__selected-seed-words__pending-seed { + display: inline-flex; + } + + .confirm-seed-phrase__selected-seed-words__selected-seed { + display: none; + } + } } } diff --git a/ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js b/ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js index 9a9f84049..0b19af18c 100644 --- a/ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js +++ b/ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js @@ -8,6 +8,8 @@ import { INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE, DEFAULT_ROUTE, } from '../../../helpers/constants/routes' +import HTML5Backend from 'react-dnd-html5-backend' +import {DragDropContextProvider} from 'react-dnd' export default class SeedPhrase extends PureComponent { static propTypes = { @@ -28,43 +30,45 @@ export default class SeedPhrase extends PureComponent { const { seedPhrase } = this.props return ( - <div className="first-time-flow__wrapper"> - <div className="app-header__logo-container"> - <img - className="app-header__metafox-logo app-header__metafox-logo--horizontal" - src="/images/logo/metamask-logo-horizontal.svg" - height={30} - /> - <img - className="app-header__metafox-logo app-header__metafox-logo--icon" - src="/images/logo/metamask-fox.svg" - height={42} - width={42} - /> + <DragDropContextProvider backend={HTML5Backend}> + <div className="first-time-flow__wrapper"> + <div className="app-header__logo-container"> + <img + className="app-header__metafox-logo app-header__metafox-logo--horizontal" + src="/images/logo/metamask-logo-horizontal.svg" + height={30} + /> + <img + className="app-header__metafox-logo app-header__metafox-logo--icon" + src="/images/logo/metamask-fox.svg" + height={42} + width={42} + /> + </div> + <Switch> + <Route + exact + path={INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE} + render={props => ( + <ConfirmSeedPhrase + { ...props } + seedPhrase={seedPhrase} + /> + )} + /> + <Route + exact + path={INITIALIZE_SEED_PHRASE_ROUTE} + render={props => ( + <RevealSeedPhrase + { ...props } + seedPhrase={seedPhrase} + /> + )} + /> + </Switch> </div> - <Switch> - <Route - exact - path={INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE} - render={props => ( - <ConfirmSeedPhrase - { ...props } - seedPhrase={seedPhrase} - /> - )} - /> - <Route - exact - path={INITIALIZE_SEED_PHRASE_ROUTE} - render={props => ( - <RevealSeedPhrase - { ...props } - seedPhrase={seedPhrase} - /> - )} - /> - </Switch> - </div> + </DragDropContextProvider> ) } } diff --git a/ui/app/pages/first-time-flow/seed-phrase/tests/confirm-seed-phrase-component.test.js b/ui/app/pages/first-time-flow/seed-phrase/tests/confirm-seed-phrase-component.test.js new file mode 100644 index 000000000..8339a6f6f --- /dev/null +++ b/ui/app/pages/first-time-flow/seed-phrase/tests/confirm-seed-phrase-component.test.js @@ -0,0 +1,169 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +import ConfirmSeedPhrase from '../confirm-seed-phrase/confirm-seed-phrase.component' + +function shallowRender (props = {}, context = {}) { + return shallow( + <ConfirmSeedPhrase {...props} />, + { + context: { + t: str => str + '_t', + ...context, + }, + } + ) +} + +describe('ConfirmSeedPhrase Component', () => { + it('should render correctly', () => { + const root = shallowRender({ + seedPhrase: '鼠 牛 虎 兔 龍 蛇 馬 羊 猴 雞 狗 豬', + }) + + assert.equal( + root.find('.confirm-seed-phrase__seed-word--shuffled').length, + 12, + 'should render 12 seed phrases' + ) + }) + + it('should add/remove selected on click', () => { + const metricsEventSpy = sinon.spy() + const pushSpy = sinon.spy() + const root = shallowRender( + { + seedPhrase: '鼠 牛 虎 兔 龍 蛇 馬 羊 猴 雞 狗 豬', + history: { push: pushSpy }, + }, + { + metricsEvent: metricsEventSpy, + } + ) + + const seeds = root.find('.confirm-seed-phrase__seed-word--shuffled') + + // Click on 3 seeds to add to selected + seeds.at(0).simulate('click') + seeds.at(1).simulate('click') + seeds.at(2).simulate('click') + + assert.deepEqual( + root.state().selectedSeedIndices, + [0, 1, 2], + 'should add seed phrase to selected on click', + ) + + // Click on a selected seed to remove + root.state() + root.update() + root.state() + root.find('.confirm-seed-phrase__seed-word--shuffled').at(1).simulate('click') + assert.deepEqual( + root.state().selectedSeedIndices, + [0, 2], + 'should remove seed phrase from selected when click again', + ) + }) + + it('should render correctly on hover', () => { + const metricsEventSpy = sinon.spy() + const pushSpy = sinon.spy() + const root = shallowRender( + { + seedPhrase: '鼠 牛 虎 兔 龍 蛇 馬 羊 猴 雞 狗 豬', + history: { push: pushSpy }, + }, + { + metricsEvent: metricsEventSpy, + } + ) + + const seeds = root.find('.confirm-seed-phrase__seed-word--shuffled') + + // Click on 3 seeds to add to selected + seeds.at(0).simulate('click') + seeds.at(1).simulate('click') + seeds.at(2).simulate('click') + + // Dragging Seed # 2 to 0 placeth + root.instance().setDraggingSeedIndex(2) + root.instance().setHoveringIndex(0) + + root.update() + + const pendingSeeds = root.find('.confirm-seed-phrase__selected-seed-words__pending-seed') + + assert.equal(pendingSeeds.at(0).props().seedIndex, 2) + assert.equal(pendingSeeds.at(1).props().seedIndex, 0) + assert.equal(pendingSeeds.at(2).props().seedIndex, 1) + }) + + it('should insert seed in place on drop', () => { + const metricsEventSpy = sinon.spy() + const pushSpy = sinon.spy() + const root = shallowRender( + { + seedPhrase: '鼠 牛 虎 兔 龍 蛇 馬 羊 猴 雞 狗 豬', + history: { push: pushSpy }, + }, + { + metricsEvent: metricsEventSpy, + } + ) + + const seeds = root.find('.confirm-seed-phrase__seed-word--shuffled') + + // Click on 3 seeds to add to selected + seeds.at(0).simulate('click') + seeds.at(1).simulate('click') + seeds.at(2).simulate('click') + + // Drop Seed # 2 to 0 placeth + root.instance().setDraggingSeedIndex(2) + root.instance().setHoveringIndex(0) + root.instance().onDrop(0) + + root.update() + + assert.deepEqual(root.state().selectedSeedIndices, [2, 0, 1]) + assert.deepEqual(root.state().pendingSeedIndices, [2, 0, 1]) + }) + + it('should submit correctly', () => { + const originalSeed = ['鼠', '牛', '虎', '兔', '龍', '蛇', '馬', '羊', '猴', '雞', '狗', '豬'] + const metricsEventSpy = sinon.spy() + const pushSpy = sinon.spy() + const root = shallowRender( + { + seedPhrase: '鼠 牛 虎 兔 龍 蛇 馬 羊 猴 雞 狗 豬', + history: { push: pushSpy }, + }, + { + metricsEvent: metricsEventSpy, + } + ) + + const shuffled = root.state().shuffledSeedWords + const seeds = root.find('.confirm-seed-phrase__seed-word--shuffled') + + + originalSeed.forEach(seed => { + const seedIndex = shuffled.findIndex(s => s === seed) + seeds.at(seedIndex).simulate('click') + }) + + root.update() + + root.find('.first-time-flow__button').simulate('click') + assert.deepEqual(metricsEventSpy.args[0][0], { + eventOpts: { + category: 'Onboarding', + action: 'Seed Phrase Setup', + name: 'Verify Complete', + }, + }) + assert.equal(pushSpy.args[0][0], '/initialize/end-of-flow') + }) +}) diff --git a/ui/app/pages/home/home.component.js b/ui/app/pages/home/home.component.js index 29d93a9fa..4d96c3131 100644 --- a/ui/app/pages/home/home.component.js +++ b/ui/app/pages/home/home.component.js @@ -23,21 +23,27 @@ export default class Home extends PureComponent { providerRequests: PropTypes.array, } + componentWillMount () { + const { + history, + unconfirmedTransactionsCount = 0, + } = this.props + + if (unconfirmedTransactionsCount > 0) { + history.push(CONFIRM_TRANSACTION_ROUTE) + } + } + componentDidMount () { const { history, suggestedTokens = {}, - unconfirmedTransactionsCount = 0, } = this.props // suggested new tokens if (Object.keys(suggestedTokens).length > 0) { history.push(CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE) } - - if (unconfirmedTransactionsCount > 0) { - history.push(CONFIRM_TRANSACTION_ROUTE) - } } render () { @@ -45,6 +51,7 @@ export default class Home extends PureComponent { forgottenPassword, seedWords, providerRequests, + history, } = this.props // seed words @@ -69,7 +76,7 @@ export default class Home extends PureComponent { query="(min-width: 576px)" render={() => <WalletView />} /> - <TransactionView /> + { !history.location.pathname.match(/^\/confirm-transaction/) ? <TransactionView /> : null } </div> </div> ) diff --git a/ui/app/pages/provider-approval/provider-approval.component.js b/ui/app/pages/provider-approval/provider-approval.component.js index 1f1d68da7..70d3d0007 100644 --- a/ui/app/pages/provider-approval/provider-approval.component.js +++ b/ui/app/pages/provider-approval/provider-approval.component.js @@ -4,9 +4,9 @@ import ProviderPageContainer from '../../components/app/provider-page-container' export default class ProviderApproval extends Component { static propTypes = { - approveProviderRequest: PropTypes.func.isRequired, + approveProviderRequestByOrigin: PropTypes.func.isRequired, + rejectProviderRequestByOrigin: PropTypes.func.isRequired, providerRequest: PropTypes.object.isRequired, - rejectProviderRequest: PropTypes.func.isRequired, }; static contextTypes = { @@ -14,13 +14,13 @@ export default class ProviderApproval extends Component { }; render () { - const { approveProviderRequest, providerRequest, rejectProviderRequest } = this.props + const { approveProviderRequestByOrigin, providerRequest, rejectProviderRequestByOrigin } = this.props return ( <ProviderPageContainer - approveProviderRequest={approveProviderRequest} + approveProviderRequestByOrigin={approveProviderRequestByOrigin} + rejectProviderRequestByOrigin={rejectProviderRequestByOrigin} origin={providerRequest.origin} tabID={providerRequest.tabID} - rejectProviderRequest={rejectProviderRequest} siteImage={providerRequest.siteImage} siteTitle={providerRequest.siteTitle} /> diff --git a/ui/app/pages/provider-approval/provider-approval.container.js b/ui/app/pages/provider-approval/provider-approval.container.js index d53c0ae4d..1e167ddb7 100644 --- a/ui/app/pages/provider-approval/provider-approval.container.js +++ b/ui/app/pages/provider-approval/provider-approval.container.js @@ -1,11 +1,11 @@ import { connect } from 'react-redux' import ProviderApproval from './provider-approval.component' -import { approveProviderRequest, rejectProviderRequest } from '../../store/actions' +import { approveProviderRequestByOrigin, rejectProviderRequestByOrigin } from '../../store/actions' function mapDispatchToProps (dispatch) { return { - approveProviderRequest: tabID => dispatch(approveProviderRequest(tabID)), - rejectProviderRequest: tabID => dispatch(rejectProviderRequest(tabID)), + approveProviderRequestByOrigin: origin => dispatch(approveProviderRequestByOrigin(origin)), + rejectProviderRequestByOrigin: origin => dispatch(rejectProviderRequestByOrigin(origin)), } } diff --git a/ui/app/pages/routes/index.js b/ui/app/pages/routes/index.js index e38a6d6ce..9eeac2da2 100644 --- a/ui/app/pages/routes/index.js +++ b/ui/app/pages/routes/index.js @@ -5,7 +5,8 @@ import { Route, Switch, withRouter, matchPath } from 'react-router-dom' import { compose } from 'recompose' import actions from '../../store/actions' import log from 'loglevel' -import { getMetaMaskAccounts, getNetworkIdentifier } from '../../selectors/selectors' +import IdleTimer from 'react-idle-timer' +import {getMetaMaskAccounts, getNetworkIdentifier, preferencesSelector} from '../../selectors/selectors' // init import FirstTimeFlow from '../first-time-flow' @@ -98,7 +99,9 @@ class Routes extends Component { } renderRoutes () { - return ( + const { autoLogoutTimeLimit, setLastActiveTime } = this.props + + const routes = ( <Switch> <Route path={LOCK_ROUTE} component={Lock} exact /> <Route path={INITIALIZE_ROUTE} component={FirstTimeFlow} /> @@ -116,6 +119,16 @@ class Routes extends Component { <Authenticated path={DEFAULT_ROUTE} component={Home} exact /> </Switch> ) + + if (autoLogoutTimeLimit > 0) { + return ( + <IdleTimer onAction={setLastActiveTime} throttle={1000}> + {routes} + </IdleTimer> + ) + } + + return routes } onInitializationUnlockPage () { @@ -322,6 +335,7 @@ Routes.propTypes = { networkDropdownOpen: PropTypes.bool, showNetworkDropdown: PropTypes.func, hideNetworkDropdown: PropTypes.func, + setLastActiveTime: PropTypes.func, history: PropTypes.object, location: PropTypes.object, dispatch: PropTypes.func, @@ -344,6 +358,7 @@ Routes.propTypes = { t: PropTypes.func, providerId: PropTypes.string, providerRequests: PropTypes.array, + autoLogoutTimeLimit: PropTypes.number, } function mapStateToProps (state) { @@ -358,6 +373,7 @@ function mapStateToProps (state) { } = appState const accounts = getMetaMaskAccounts(state) + const { autoLogoutTimeLimit = 0 } = preferencesSelector(state) const { identities, @@ -409,6 +425,7 @@ function mapStateToProps (state) { Qr: state.appState.Qr, welcomeScreenSeen: state.metamask.welcomeScreenSeen, providerId: getNetworkIdentifier(state), + autoLogoutTimeLimit, // state needed to get account dropdown temporarily rendering from app bar identities, @@ -418,7 +435,7 @@ function mapStateToProps (state) { } } -function mapDispatchToProps (dispatch, ownProps) { +function mapDispatchToProps (dispatch) { return { dispatch, hideSidebar: () => dispatch(actions.hideSidebar()), @@ -427,6 +444,7 @@ function mapDispatchToProps (dispatch, ownProps) { setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('usd')), toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()), setMouseUserState: (isMouseUser) => dispatch(actions.setMouseUserState(isMouseUser)), + setLastActiveTime: () => dispatch(actions.setLastActiveTime()), } } diff --git a/ui/app/pages/send/account-list-item/tests/account-list-item-container.test.js b/ui/app/pages/send/account-list-item/tests/account-list-item-container.test.js index 33f932daf..1580fd497 100644 --- a/ui/app/pages/send/account-list-item/tests/account-list-item-container.test.js +++ b/ui/app/pages/send/account-list-item/tests/account-list-item-container.test.js @@ -5,7 +5,7 @@ let mapStateToProps proxyquire('../account-list-item.container.js', { 'react-redux': { - connect: (ms, md) => { + connect: (ms) => { mapStateToProps = ms return () => ({}) }, diff --git a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js index f17137c1e..e256d1442 100644 --- a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js +++ b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js @@ -15,6 +15,7 @@ export default class AmountMaxButton extends Component { static contextTypes = { t: PropTypes.func, + metricsEvent: PropTypes.func, } setMaxAmount () { @@ -35,11 +36,15 @@ export default class AmountMaxButton extends Component { } onMaxClick = (event) => { - const { setMaxModeTo, selectedToken } = this.props + const { setMaxModeTo } = this.props + const { metricsEvent } = this.context - fetch('https://chromeextensionmm.innocraft.cloud/piwik.php?idsite=1&rec=1&e_c=send&e_a=amountMax&e_n=' + (selectedToken ? 'token' : 'eth'), { - 'headers': {}, - 'method': 'GET', + metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Edit Screen', + name: 'Clicked "Amount Max"', + }, }) event.preventDefault() diff --git a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js index b04d3897f..a6cb29d4c 100644 --- a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js +++ b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js @@ -26,7 +26,12 @@ describe('AmountMaxButton Component', function () { setAmountToMax={propsMethodSpies.setAmountToMax} setMaxModeTo={propsMethodSpies.setMaxModeTo} tokenBalance={'mockTokenBalance'} - />, { context: { t: str => str + '_t' } }) + />, { + context: { + t: str => str + '_t', + metricsEvent: () => {}, + }, + }) instance = wrapper.instance() }) diff --git a/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js b/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js index eecff165d..2013e3200 100644 --- a/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js +++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js @@ -5,7 +5,7 @@ let mapStateToProps proxyquire('../send-row-error-message.container.js', { 'react-redux': { - connect: (ms, md) => { + connect: (ms) => { mapStateToProps = ms return () => ({}) }, diff --git a/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js index 225bf056c..6c0739f0e 100644 --- a/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js +++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js @@ -5,7 +5,7 @@ let mapStateToProps proxyquire('../send-row-warning-message.container.js', { 'react-redux': { - connect: (ms, md) => { + connect: (ms) => { mapStateToProps = ms return () => ({}) }, diff --git a/ui/app/pages/send/send-content/send-to-row/send-to-row.utils.js b/ui/app/pages/send/send-content/send-to-row/send-to-row.utils.js index d0a43f086..b3b0d2da3 100644 --- a/ui/app/pages/send/send-content/send-to-row/send-to-row.utils.js +++ b/ui/app/pages/send/send-content/send-to-row/send-to-row.utils.js @@ -10,16 +10,15 @@ import { checkExistingAddresses } from '../../../add-token/util' const ethUtil = require('ethereumjs-util') const contractMap = require('eth-contract-metadata') -function getToErrorObject (to, toError = null, hasHexData = false, tokens = [], selectedToken = null, network) { +function getToErrorObject (to, toError = null, hasHexData = false, _, __, network) { if (!to) { if (!hasHexData) { toError = REQUIRED_ERROR } } else if (!isValidAddress(to, network) && !toError) { toError = isEthNetwork(network) ? INVALID_RECIPIENT_ADDRESS_ERROR : INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR - } else if (selectedToken && (ethUtil.toChecksumAddress(to) in contractMap || checkExistingAddresses(to, tokens))) { - toError = KNOWN_RECIPIENT_ADDRESS_ERROR } + return { to: toError } } diff --git a/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-utils.test.js b/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-utils.test.js index f29f5efec..f8a6dd96f 100644 --- a/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-utils.test.js +++ b/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-utils.test.js @@ -55,9 +55,9 @@ describe('send-to-row utils', () => { }) }) - it('should return a known address recipient if to is truthy but part of state tokens', () => { + it('should return null if to is truthy but part of state tokens', () => { assert.deepEqual(getToErrorObject('0xabc123', undefined, false, [{'address': '0xabc123'}], {'address': '0xabc123'}), { - to: KNOWN_RECIPIENT_ADDRESS_ERROR, + to: null, }) }) @@ -67,14 +67,14 @@ describe('send-to-row utils', () => { }) }) - it('should return a known address recipient if to is truthy but part of contract metadata', () => { + it('should return null if to is truthy but part of contract metadata', () => { assert.deepEqual(getToErrorObject('0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', undefined, false, [{'address': '0xabc123'}], {'address': '0xabc123'}), { - to: KNOWN_RECIPIENT_ADDRESS_ERROR, + to: null, }) }) it('should null if to is truthy part of contract metadata but selectedToken falsy', () => { assert.deepEqual(getToErrorObject('0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', undefined, false, [{'address': '0xabc123'}], {'address': '0xabc123'}), { - to: KNOWN_RECIPIENT_ADDRESS_ERROR, + to: null, }) }) }) diff --git a/ui/app/pages/send/tests/send-container.test.js b/ui/app/pages/send/tests/send-container.test.js index b3e202030..131c42f59 100644 --- a/ui/app/pages/send/tests/send-container.test.js +++ b/ui/app/pages/send/tests/send-container.test.js @@ -24,7 +24,7 @@ proxyquire('../send.container.js', { }, }, 'react-router-dom': { withRouter: () => {} }, - 'recompose': { compose: (arg1, arg2) => () => arg2() }, + 'recompose': { compose: (_, arg2) => () => arg2() }, './send.selectors': { getAmountConversionRate: (s) => `mockAmountConversionRate:${s}`, getBlockGasLimit: (s) => `mockBlockGasLimit:${s}`, diff --git a/ui/app/pages/send/tests/send-utils.test.js b/ui/app/pages/send/tests/send-utils.test.js index b19535b9e..bf9cba14a 100644 --- a/ui/app/pages/send/tests/send-utils.test.js +++ b/ui/app/pages/send/tests/send-utils.test.js @@ -17,12 +17,12 @@ const { } = require('../send.constants') const stubs = { - addCurrencies: sinon.stub().callsFake((a, b, obj) => { + addCurrencies: sinon.stub().callsFake((a, b) => { if (String(a).match(/^0x.+/)) a = Number(String(a).slice(2)) if (String(b).match(/^0x.+/)) b = Number(String(b).slice(2)) return a + b }), - conversionUtil: sinon.stub().callsFake((val, obj) => parseInt(val, 16)), + conversionUtil: sinon.stub().callsFake((val) => parseInt(val, 16)), conversionGTE: sinon.stub().callsFake((obj1, obj2) => obj1.value >= obj2.value), multiplyCurrencies: sinon.stub().callsFake((a, b) => `${a}x${b}`), calcTokenAmount: sinon.stub().callsFake((a, d) => 'calc:' + a + d), diff --git a/ui/app/pages/send/to-autocomplete/to-autocomplete.js b/ui/app/pages/send/to-autocomplete/to-autocomplete.js index b246413fb..328a5b62b 100644 --- a/ui/app/pages/send/to-autocomplete/to-autocomplete.js +++ b/ui/app/pages/send/to-autocomplete/to-autocomplete.js @@ -84,7 +84,7 @@ ToAutoComplete.prototype.handleInputEvent = function (event = {}, cb) { cb && cb(event.target.value) } -ToAutoComplete.prototype.componentDidUpdate = function (nextProps, nextState) { +ToAutoComplete.prototype.componentDidUpdate = function (nextProps) { if (this.props.to !== nextProps.to) { this.handleInputEvent() } diff --git a/ui/app/pages/settings/advanced-tab/advanced-tab.component.js b/ui/app/pages/settings/advanced-tab/advanced-tab.component.js index 14b9daae6..3d27fe349 100644 --- a/ui/app/pages/settings/advanced-tab/advanced-tab.component.js +++ b/ui/app/pages/settings/advanced-tab/advanced-tab.component.js @@ -24,6 +24,8 @@ export default class AdvancedTab extends PureComponent { setAdvancedInlineGasFeatureFlag: PropTypes.func, advancedInlineGas: PropTypes.bool, showFiatInTestnets: PropTypes.bool, + autoLogoutTimeLimit: PropTypes.number, + setAutoLogoutTimeLimit: PropTypes.func.isRequired, setShowFiatConversionOnTestnetsPreference: PropTypes.func.isRequired, } @@ -49,7 +51,7 @@ export default class AdvancedTab extends PureComponent { <TextField type="text" id="new-rpc" - placeholder={t('rpcURL')} + placeholder={t('rpcUrl')} value={newRpc} onChange={e => this.setState({ newRpc: e.target.value })} onKeyPress={e => { @@ -355,6 +357,48 @@ export default class AdvancedTab extends PureComponent { ) } + renderAutoLogoutTimeLimit () { + const { t } = this.context + const { + autoLogoutTimeLimit, + setAutoLogoutTimeLimit, + } = this.props + + return ( + <div className="settings-page__content-row"> + <div className="settings-page__content-item"> + <span>{ t('autoLogoutTimeLimit') }</span> + <div className="settings-page__content-description"> + { t('autoLogoutTimeLimitDescription') } + </div> + </div> + <div className="settings-page__content-item"> + <div className="settings-page__content-item-col"> + <TextField + type="number" + id="autoTimeout" + placeholder="5" + value={this.state.autoLogoutTimeLimit} + defaultValue={autoLogoutTimeLimit} + onChange={e => this.setState({ autoLogoutTimeLimit: Math.max(Number(e.target.value), 0) })} + fullWidth + margin="dense" + min={0} + /> + <button + className="button btn-primary settings-tab__rpc-save-button" + onClick={() => { + setAutoLogoutTimeLimit(this.state.autoLogoutTimeLimit) + }} + > + { t('save') } + </button> + </div> + </div> + </div> + ) + } + renderContent () { const { warning } = this.props @@ -368,6 +412,7 @@ export default class AdvancedTab extends PureComponent { { this.renderAdvancedGasInputInline() } { this.renderHexDataOptIn() } { this.renderShowConversionInTestnets() } + { this.renderAutoLogoutTimeLimit() } </div> ) } diff --git a/ui/app/pages/settings/advanced-tab/advanced-tab.container.js b/ui/app/pages/settings/advanced-tab/advanced-tab.container.js index 69d7e07e6..bcac55f5e 100644 --- a/ui/app/pages/settings/advanced-tab/advanced-tab.container.js +++ b/ui/app/pages/settings/advanced-tab/advanced-tab.container.js @@ -8,10 +8,11 @@ import { setFeatureFlag, showModal, setShowFiatConversionOnTestnetsPreference, + setAutoLogoutTimeLimit, } from '../../../store/actions' import {preferencesSelector} from '../../../selectors/selectors' -const mapStateToProps = state => { +export const mapStateToProps = state => { const { appState: { warning }, metamask } = state const { featureFlags: { @@ -19,17 +20,18 @@ const mapStateToProps = state => { advancedInlineGas, } = {}, } = metamask - const { showFiatInTestnets } = preferencesSelector(state) + const { showFiatInTestnets, autoLogoutTimeLimit } = preferencesSelector(state) return { warning, sendHexData, advancedInlineGas, showFiatInTestnets, + autoLogoutTimeLimit, } } -const mapDispatchToProps = dispatch => { +export const mapDispatchToProps = dispatch => { return { setHexDataFeatureFlag: shouldShow => dispatch(setFeatureFlag('sendHexData', shouldShow)), setRpcTarget: (newRpc, chainId, ticker, nickname) => dispatch(updateAndSetCustomRpc(newRpc, chainId, ticker, nickname)), @@ -39,6 +41,9 @@ const mapDispatchToProps = dispatch => { setShowFiatConversionOnTestnetsPreference: value => { return dispatch(setShowFiatConversionOnTestnetsPreference(value)) }, + setAutoLogoutTimeLimit: value => { + return dispatch(setAutoLogoutTimeLimit(value)) + }, } } diff --git a/ui/app/pages/settings/advanced-tab/tests/advanced-tab-component.test.js b/ui/app/pages/settings/advanced-tab/tests/advanced-tab-component.test.js new file mode 100644 index 000000000..f81329533 --- /dev/null +++ b/ui/app/pages/settings/advanced-tab/tests/advanced-tab-component.test.js @@ -0,0 +1,44 @@ +import React from 'react' +import assert from 'assert' +import sinon from 'sinon' +import { shallow } from 'enzyme' +import AdvancedTab from '../advanced-tab.component' +import TextField from '../../../../components/ui/text-field' + +describe('AdvancedTab Component', () => { + it('should render correctly', () => { + const root = shallow( + <AdvancedTab />, + { + context: { + t: s => `_${s}`, + }, + } + ) + + assert.equal(root.find('.settings-page__content-row').length, 8) + }) + + it('should update autoLogoutTimeLimit', () => { + const setAutoLogoutTimeLimitSpy = sinon.spy() + const root = shallow( + <AdvancedTab + setAutoLogoutTimeLimit={setAutoLogoutTimeLimitSpy} + />, + { + context: { + t: s => `_${s}`, + }, + } + ) + + const autoTimeout = root.find('.settings-page__content-row').last() + const textField = autoTimeout.find(TextField) + + textField.props().onChange({ target: { value: 1440 } }) + assert.equal(root.state().autoLogoutTimeLimit, 1440) + + autoTimeout.find('button').simulate('click') + assert.equal(setAutoLogoutTimeLimitSpy.args[0][0], 1440) + }) +}) diff --git a/ui/app/pages/settings/advanced-tab/tests/advanced-tab-container.test.js b/ui/app/pages/settings/advanced-tab/tests/advanced-tab-container.test.js new file mode 100644 index 000000000..62122073d --- /dev/null +++ b/ui/app/pages/settings/advanced-tab/tests/advanced-tab-container.test.js @@ -0,0 +1,46 @@ +import assert from 'assert' +import { mapStateToProps, mapDispatchToProps } from '../advanced-tab.container' + +const defaultState = { + appState: { + warning: null, + }, + metamask: { + featureFlags: { + sendHexData: false, + advancedInlineGas: false, + }, + preferences: { + autoLogoutTimeLimit: 0, + showFiatInTestnets: false, + useNativeCurrencyAsPrimaryCurrency: true, + }, + }, +} + +describe('AdvancedTab Container', () => { + it('should map state to props correctly', () => { + const props = mapStateToProps(defaultState) + const expected = { + warning: null, + sendHexData: false, + advancedInlineGas: false, + showFiatInTestnets: false, + autoLogoutTimeLimit: 0, + } + + assert.deepEqual(props, expected) + }) + + it('should map dispatch to props correctly', () => { + const props = mapDispatchToProps(() => 'mockDispatch') + + assert.ok(typeof props.setHexDataFeatureFlag === 'function') + assert.ok(typeof props.setRpcTarget === 'function') + assert.ok(typeof props.displayWarning === 'function') + assert.ok(typeof props.showResetAccountConfirmationModal === 'function') + assert.ok(typeof props.setAdvancedInlineGasFeatureFlag === 'function') + assert.ok(typeof props.setShowFiatConversionOnTestnetsPreference === 'function') + assert.ok(typeof props.setAutoLogoutTimeLimit === 'function') + }) +}) diff --git a/ui/app/pages/settings/index.scss b/ui/app/pages/settings/index.scss index 52208dc85..66959ba93 100644 --- a/ui/app/pages/settings/index.scss +++ b/ui/app/pages/settings/index.scss @@ -1,5 +1,7 @@ @import 'info-tab/index'; +@import 'networks-tab/index'; + @import 'settings-tab/index'; .settings-page { @@ -13,7 +15,6 @@ flex-flow: row nowrap; padding: 12px 24px; align-items: center; - border-bottom: 1px solid $alto; flex: 0 0 auto; &__title { @@ -22,6 +23,45 @@ } } + &__subheader { + padding: 16px 4px; + font-size: 20px; + border-bottom: 1px solid $alto; + margin-right: 24px; + + @media screen and (max-width: 575px) { + display: none; + } + } + + &__sub-header { + height: 72px; + border-bottom: 1px solid #D8D8D8; + display: flex; + justify-content: space-between; + align-items: center; + + @media screen and (max-width: 575px) { + height: 69px; + position: relative; + text-align: center; + } + } + + &__sub-header-text { + font-family: Roboto; + font-style: normal; + font-weight: normal; + font-size: 24px; + line-height: 24px; + color: black; + + @media screen and (max-width: 575px) { + font-size: 16px; + width: 100%; + } + } + &__back-button { display: none; @@ -49,8 +89,9 @@ &__content { display: flex; flex-flow: row nowrap; - height: auto; + height: 100%; overflow: auto; + border-top: 1px solid #D8D8D8; &__tabs { display: flex; @@ -58,9 +99,15 @@ flex: 1 1 auto; @media screen and (min-width: 576px) { - flex: 0 0 32%; + flex: 0 0 40%; max-width: 210px; - border-right: 1px solid $alto; + padding-top: 8px; + } + + .tab-bar__tab { + @media screen and (min-width: 576px) { + padding: 16px 24px 0; + } } } @@ -76,6 +123,10 @@ &__body { padding: 12px 24px; + + @media screen and (min-width: 576px) { + padding: 12px; + } } &__content-row { @@ -89,7 +140,6 @@ min-width: 0; display: flex; flex-direction: column; - padding: 0 5px; min-height: 71px; @media screen and (max-width: 575px) { diff --git a/ui/app/pages/settings/networks-tab/index.js b/ui/app/pages/settings/networks-tab/index.js new file mode 100644 index 000000000..362004498 --- /dev/null +++ b/ui/app/pages/settings/networks-tab/index.js @@ -0,0 +1 @@ +export { default } from './networks-tab.container' diff --git a/ui/app/pages/settings/networks-tab/index.scss b/ui/app/pages/settings/networks-tab/index.scss new file mode 100644 index 000000000..b0020437d --- /dev/null +++ b/ui/app/pages/settings/networks-tab/index.scss @@ -0,0 +1,200 @@ +.networks-tab { + &__content { + margin-top: 24px; + display: flex; + height: 100%; + max-width: 739px; + justify-content: space-between; + + @media screen and (max-width: 575px) { + margin-top: 0px; + } + } + + &__body { + padding: 12px 24px; + height: 100%; + display: flex; + flex-direction: column; + + @media screen and (max-width: 575px) { + padding: 0; + } + } + + &__back-button { + display: none; + + @media screen and (max-width: 575px) { + display: block; + background-image: url('/images/caret-left-black.svg'); + width: 18px; + height: 18px; + opacity: .5; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + margin-right: 16px; + cursor: pointer; + position: absolute; + margin-left: 10px; + } + } + + &__network-form { + flex: 0.5 0 auto; + max-width: 343px; + max-height: 465px; + display: flex; + flex-direction: column; + justify-content: space-between; + + .page-container__footer { + border-top: none; + + @media screen and (max-width: 575px) { + width: 93%; + } + + header { + padding: 10px 0px; + } + } + + @media screen and (max-width: 575px) { + display: flex; + flex: auto; + max-width: 100%; + max-height: 100%; + align-items: center; + width: 100%; + margin-top: 10px; + } + } + + &__network-form-row { + @media screen and (max-width: 575px) { + display: flex; + flex-direction: column; + width: 93%; + } + } + + &__network-form-label { + font-family: Roboto; + font-style: normal; + font-weight: normal; + font-size: 14px; + line-height: 20px; + color: #000000; + } + + &__networks-list { + flex: 0.5 0 auto; + max-width: 343px; + + @media screen and (max-width: 575px) { + max-width: 100vw; + width: 100vw; + overflow-y: scroll; + } + } + + &__add-network-button-wrapper { + display: none; + + @media screen and (max-width: 575px) { + display: flex; + padding-top: 19px; + padding-bottom: 23px; + justify-content: center; + align-items: center; + border-top: 1px solid #D8D8D8; + + .button { + width: 178px; + } + } + } + + &__add-network-header-button-wrapper { + padding-top: 15px; + padding-bottom: 21px; + justify-content: center; + + .button { + width: 178px; + } + + @media screen and (max-width: 575px) { + display: none; + } + } + + &__networks-list--selection { + @media screen and (max-width: 575px) { + display: none; + } + } + + &__networks-list-item { + display: flex; + padding: 13px 0px 13px 17px; + position: relative; + + .menu-icon-circle { + &:hover { + cursor: pointer; + } + } + + @media screen and (max-width: 575px) { + padding: 20px 23px 21px 17px; + border-bottom: 1px solid #D8D8D8; + } + } + + &__networks-list-item:last-of-type { + @media screen and (max-width: 575px) { + border-bottom: none; + } + } + + &__networks-list-name { + margin-left: 11px; + font-family: Roboto; + font-style: normal; + font-weight: normal; + font-size: 16px; + line-height: 23px; + color: #6A737D; + + &:hover { + cursor: pointer; + } + } + + &__networks-list-arrow { + display: none; + + @media screen and (max-width: 575px) { + display: block; + background-image: url('/images/caret-right.svg'); + width: 24px; + height: 24px; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + right: 10px; + cursor: pointer; + position: absolute; + width: 24px; + height: 24px; + } + } + + &__networks-list-name--selected { + font-weight: bold; + color: #000000; + } +}
\ No newline at end of file diff --git a/ui/app/pages/settings/networks-tab/network-form/index.js b/ui/app/pages/settings/networks-tab/network-form/index.js new file mode 100644 index 000000000..89d9de42b --- /dev/null +++ b/ui/app/pages/settings/networks-tab/network-form/index.js @@ -0,0 +1 @@ +export { default } from './network-form.component' diff --git a/ui/app/pages/settings/networks-tab/network-form/network-form.component.js b/ui/app/pages/settings/networks-tab/network-form/network-form.component.js new file mode 100644 index 000000000..5e455b65e --- /dev/null +++ b/ui/app/pages/settings/networks-tab/network-form/network-form.component.js @@ -0,0 +1,225 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import validUrl from 'valid-url' +import PageContainerFooter from '../../../../components/ui/page-container/page-container-footer' +import TextField from '../../../../components/ui/text-field' + +export default class NetworksTab extends PureComponent { + static contextTypes = { + t: PropTypes.func.isRequired, + metricsEvent: PropTypes.func.isRequired, + } + + static propTypes = { + editRpc: PropTypes.func.isRequired, + rpcUrl: PropTypes.string, + chainId: PropTypes.string, + ticker: PropTypes.string, + viewOnly: PropTypes.bool, + networkName: PropTypes.string, + onClear: PropTypes.func.isRequired, + setRpcTarget: PropTypes.func.isRequired, + networksTabIsInAddMode: PropTypes.bool, + blockExplorerUrl: PropTypes.string, + rpcPrefs: PropTypes.object, + } + + state = { + rpcUrl: this.props.rpcUrl, + chainId: this.props.chainId, + ticker: this.props.ticker, + networkName: this.props.networkName, + blockExplorerUrl: this.props.blockExplorerUrl, + errors: {}, + } + + componentDidUpdate (prevProps) { + const { rpcUrl: prevRpcUrl, networksTabIsInAddMode: prevAddMode } = prevProps + const { + rpcUrl, + chainId, + ticker, + networkName, + networksTabIsInAddMode, + blockExplorerUrl, + } = this.props + + if (!prevAddMode && networksTabIsInAddMode) { + this.setState({ + rpcUrl: '', + chainId: '', + ticker: '', + networkName: '', + blockExplorerUrl: '', + errors: {}, + }) + } else if (prevRpcUrl !== rpcUrl) { + this.setState({ rpcUrl, chainId, ticker, networkName, blockExplorerUrl, errors: {} }) + } + } + + componentWillUnmount () { + this.props.onClear() + this.setState({ + rpcUrl: '', + chainId: '', + ticker: '', + networkName: '', + blockExplorerUrl: '', + errors: {}, + }) + } + + stateIsUnchanged () { + const { + rpcUrl, + chainId, + ticker, + networkName, + blockExplorerUrl, + } = this.props + + const { + rpcUrl: stateRpcUrl, + chainId: stateChainId, + ticker: stateTicker, + networkName: stateNetworkName, + blockExplorerUrl: stateBlockExplorerUrl, + } = this.state + + return ( + stateRpcUrl === rpcUrl && + stateChainId === chainId && + stateTicker === ticker && + stateNetworkName === networkName && + stateBlockExplorerUrl === blockExplorerUrl + ) + } + + renderFormTextField (fieldKey, textFieldId, onChange, value, optionalTextFieldKey) { + const { errors } = this.state + const { viewOnly } = this.props + + return ( + <div className="networks-tab__network-form-row"> + <div className="networks-tab__network-form-label">{this.context.t(optionalTextFieldKey || fieldKey)}</div> + <TextField + type="text" + id={textFieldId} + onChange={onChange} + fullWidth + margin="dense" + value={value} + disabled={viewOnly} + error={errors[fieldKey]} + /> + </div> + ) + } + + setStateWithValue = (stateKey, validator) => { + return (e) => { + validator && validator(e.target.value, stateKey) + this.setState({ [stateKey]: e.target.value }) + } + } + + setErrorTo = (errorKey, errorVal) => { + this.setState({ + errors: { + ...this.state.errors, + [errorKey]: errorVal, + }, + }) + } + + validateChainId = (chainId) => { + this.setErrorTo('chainId', !!chainId && Number.isNaN(parseInt(chainId)) + ? `${this.context.t('invalidInput')} chainId` + : '' + ) + } + + validateUrl = (url, stateKey) => { + if (validUrl.isWebUri(url)) { + this.setErrorTo(stateKey, '') + } else { + const appendedRpc = `http://${url}` + const validWhenAppended = validUrl.isWebUri(appendedRpc) && !url.match(/^https?:\/\/$/) + + this.setErrorTo(stateKey, this.context.t(validWhenAppended ? 'uriErrorMsg' : 'invalidRPC')) + } + } + + render () { + const { setRpcTarget, viewOnly, rpcUrl: propsRpcUrl, editRpc, rpcPrefs = {} } = this.props + const { + networkName, + rpcUrl, + chainId, + ticker, + blockExplorerUrl, + errors, + } = this.state + + + return ( + <div className="networks-tab__network-form"> + {this.renderFormTextField( + 'networkName', + 'network-name', + this.setStateWithValue('networkName'), + networkName, + )} + {this.renderFormTextField( + 'rpcUrl', + 'rpc-url', + this.setStateWithValue('rpcUrl', this.validateUrl), + rpcUrl, + )} + {this.renderFormTextField( + 'chainId', + 'chainId', + this.setStateWithValue('chainId', this.validateChainId), + chainId, + 'optionalChainId', + )} + {this.renderFormTextField( + 'symbol', + 'network-ticker', + this.setStateWithValue('ticker'), + ticker, + 'optionalSymbol', + )} + {this.renderFormTextField( + 'blockExplorerUrl', + 'block-explorer-url', + this.setStateWithValue('blockExplorerUrl', this.validateUrl), + blockExplorerUrl, + 'optionalBlockExplorerUrl', + )} + <PageContainerFooter + cancelText={this.context.t('cancel')} + hideCancel={true} + onSubmit={() => { + if (propsRpcUrl && rpcUrl !== propsRpcUrl) { + editRpc(propsRpcUrl, rpcUrl, chainId, ticker, networkName, { + blockExplorerUrl: blockExplorerUrl || rpcPrefs.blockExplorerUrl, + ...rpcPrefs, + }) + } else { + setRpcTarget(rpcUrl, chainId, ticker, networkName, { + blockExplorerUrl: blockExplorerUrl || rpcPrefs.blockExplorerUrl, + ...rpcPrefs, + }) + } + }} + submitText={this.context.t('save')} + submitButtonType={'confirm'} + disabled={viewOnly || this.stateIsUnchanged() || Object.values(errors).some(x => x) || !rpcUrl} + /> + </div> + ) + } + +} diff --git a/ui/app/pages/settings/networks-tab/networks-tab.component.js b/ui/app/pages/settings/networks-tab/networks-tab.component.js new file mode 100644 index 000000000..2f921a892 --- /dev/null +++ b/ui/app/pages/settings/networks-tab/networks-tab.component.js @@ -0,0 +1,214 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import { SETTINGS_ROUTE } from '../../../helpers/constants/routes' +import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums' +import { getEnvironmentType } from '../../../../../app/scripts/lib/util' +import classnames from 'classnames' +import Button from '../../../components/ui/button' +import NetworkForm from './network-form' +import NetworkDropdownIcon from '../../../components/app/dropdowns/components/network-dropdown-icon' + +export default class NetworksTab extends PureComponent { + static contextTypes = { + t: PropTypes.func.isRequired, + metricsEvent: PropTypes.func.isRequired, + } + + static propTypes = { + editRpc: PropTypes.func.isRequired, + history: PropTypes.object.isRequired, + location: PropTypes.object.isRequired, + networkIsSelected: PropTypes.bool, + networksTabIsInAddMode: PropTypes.bool, + networksToRender: PropTypes.array.isRequired, + selectedNetwork: PropTypes.object, + setNetworksTabAddMode: PropTypes.func.isRequired, + setRpcTarget: PropTypes.func.isRequired, + setSelectedSettingsRpcUrl: PropTypes.func.isRequired, + providerUrl: PropTypes.string, + providerType: PropTypes.string, + networkDefaultedToProvider: PropTypes.bool, + } + + componentWillMount () { + this.props.setSelectedSettingsRpcUrl(null) + } + + isCurrentPath (pathname) { + return this.props.location.pathname === pathname + } + + renderSubHeader () { + const { + networkIsSelected, + setSelectedSettingsRpcUrl, + setNetworksTabAddMode, + networksTabIsInAddMode, + networkDefaultedToProvider, + } = this.props + + return ( + <div className="settings-page__sub-header"> + <div + className="networks-tab__back-button" + onClick={(networkIsSelected && !networkDefaultedToProvider) || networksTabIsInAddMode + ? () => { + setNetworksTabAddMode(false) + setSelectedSettingsRpcUrl(null) + } + : () => this.props.history.push(SETTINGS_ROUTE) + } + /> + <span className="settings-page__sub-header-text">{ this.context.t('networks') }</span> + <div className="networks-tab__add-network-header-button-wrapper"> + <Button + type="primary" + onClick={event => { + event.preventDefault() + setSelectedSettingsRpcUrl(null) + setNetworksTabAddMode(true) + }} + > + { this.context.t('addNetwork') } + </Button> + </div> + </div> + ) + } + + renderNetworkListItem (network, selectRpcUrl) { + const { + setSelectedSettingsRpcUrl, + setNetworksTabAddMode, + networkIsSelected, + providerUrl, + providerType, + networksTabIsInAddMode, + } = this.props + const { + border, + iconColor, + label, + labelKey, + rpcUrl, + providerType: currentProviderType, + } = network + + const listItemNetworkIsSelected = selectRpcUrl && selectRpcUrl === rpcUrl + const listItemUrlIsProviderUrl = rpcUrl === providerUrl + const listItemTypeIsProviderNonRpcType = providerType !== 'rpc' && currentProviderType === providerType + const listItemNetworkIsCurrentProvider = !networkIsSelected && !networksTabIsInAddMode && (listItemUrlIsProviderUrl || listItemTypeIsProviderNonRpcType) + const displayNetworkListItemAsSelected = listItemNetworkIsSelected || listItemNetworkIsCurrentProvider + + return ( + <div + key={'settings-network-list-item:' + rpcUrl} + className="networks-tab__networks-list-item" + onClick={ () => { + setNetworksTabAddMode(false) + setSelectedSettingsRpcUrl(rpcUrl) + }} + > + <NetworkDropdownIcon + backgroundColor={iconColor || 'white'} + innerBorder={border} + /> + <div className={ classnames('networks-tab__networks-list-name', { + 'networks-tab__networks-list-name--selected': displayNetworkListItemAsSelected, + }) }> + { label || this.context.t(labelKey) } + </div> + <div className="networks-tab__networks-list-arrow" /> + </div> + ) + } + + renderNetworksList () { + const { networksToRender, selectedNetwork, networkIsSelected, networksTabIsInAddMode, networkDefaultedToProvider } = this.props + + return ( + <div className={classnames('networks-tab__networks-list', { + 'networks-tab__networks-list--selection': (networkIsSelected && !networkDefaultedToProvider) || networksTabIsInAddMode, + })}> + { networksToRender.map(network => this.renderNetworkListItem(network, selectedNetwork.rpcUrl)) } + </div> + ) + } + + renderNetworksTabContent () { + const { + setRpcTarget, + setSelectedSettingsRpcUrl, + setNetworksTabAddMode, + selectedNetwork: { + labelKey, + label, + rpcUrl, + chainId, + ticker, + viewOnly, + rpcPrefs, + blockExplorerUrl, + }, + networksTabIsInAddMode, + editRpc, + networkDefaultedToProvider, + } = this.props + const envIsPopup = getEnvironmentType() === ENVIRONMENT_TYPE_POPUP + + return ( + <div className="networks-tab__content"> + {this.renderNetworksList()} + {networksTabIsInAddMode || !envIsPopup || (envIsPopup && !networkDefaultedToProvider) + ? <NetworkForm + setRpcTarget={setRpcTarget} + editRpc={editRpc} + networkName={label || labelKey && this.context.t(labelKey) || ''} + rpcUrl={rpcUrl} + chainId={chainId} + ticker={ticker} + onClear={() => { + setNetworksTabAddMode(false) + setSelectedSettingsRpcUrl(null) + }} + viewOnly={viewOnly} + networksTabIsInAddMode={networksTabIsInAddMode} + rpcPrefs={rpcPrefs} + blockExplorerUrl={blockExplorerUrl} + /> + : null + } + </div> + ) + } + + renderContent () { + const { setNetworksTabAddMode, setSelectedSettingsRpcUrl, networkIsSelected, networksTabIsInAddMode } = this.props + + return ( + <div className="networks-tab__body"> + {this.renderSubHeader()} + {this.renderNetworksTabContent()} + {!networkIsSelected && !networksTabIsInAddMode + ? <div className="networks-tab__add-network-button-wrapper"> + <Button + type="primary" + onClick={event => { + event.preventDefault() + setSelectedSettingsRpcUrl(null) + setNetworksTabAddMode(true) + }} + > + { this.context.t('addNetwork') } + </Button> + </div> + : null + } + </div> + ) + } + + render () { + return this.renderContent() + } +} diff --git a/ui/app/pages/settings/networks-tab/networks-tab.constants.js b/ui/app/pages/settings/networks-tab/networks-tab.constants.js new file mode 100644 index 000000000..d3d1a01cc --- /dev/null +++ b/ui/app/pages/settings/networks-tab/networks-tab.constants.js @@ -0,0 +1,50 @@ +const defaultNetworksData = [ + { + labelKey: 'mainnet', + iconColor: '#29B6AF', + providerType: 'mainnet', + rpcUrl: 'https://api.infura.io/v1/jsonrpc/mainnet', + chainId: '1', + ticker: 'ETH', + blockExplorerUrl: 'https://etherscan.io', + }, + { + labelKey: 'ropsten', + iconColor: '#FF4A8D', + providerType: 'ropsten', + rpcUrl: 'https://api.infura.io/v1/jsonrpc/ropsten', + chainId: '3', + ticker: 'ETH', + blockExplorerUrl: 'https://ropsten.etherscan.io', + }, + { + labelKey: 'kovan', + iconColor: '#9064FF', + providerType: 'kovan', + rpcUrl: 'https://api.infura.io/v1/jsonrpc/kovan', + chainId: '4', + ticker: 'ETH', + blockExplorerUrl: 'https://etherscan.io', + }, + { + labelKey: 'rinkeby', + iconColor: '#F6C343', + providerType: 'rinkeby', + rpcUrl: 'https://api.infura.io/v1/jsonrpc/rinkeby', + chainId: '42', + ticker: 'ETH', + blockExplorerUrl: 'https://rinkeby.etherscan.io', + }, + { + labelKey: 'localhost', + iconColor: 'white', + border: '1px solid #6A737D', + providerType: 'localhost', + rpcUrl: 'http://localhost:8545/', + blockExplorerUrl: 'https://etherscan.io', + }, +] + +export { + defaultNetworksData, +} diff --git a/ui/app/pages/settings/networks-tab/networks-tab.container.js b/ui/app/pages/settings/networks-tab/networks-tab.container.js new file mode 100644 index 000000000..a5d71f714 --- /dev/null +++ b/ui/app/pages/settings/networks-tab/networks-tab.container.js @@ -0,0 +1,77 @@ +import NetworksTab from './networks-tab.component' +import { compose } from 'recompose' +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { + setSelectedSettingsRpcUrl, + updateAndSetCustomRpc, + displayWarning, + setNetworksTabAddMode, + editRpc, +} from '../../../store/actions' +import { defaultNetworksData } from './networks-tab.constants' +const defaultNetworks = defaultNetworksData.map(network => ({ ...network, viewOnly: true })) + +const mapStateToProps = state => { + const { + frequentRpcListDetail, + provider, + } = state.metamask + const { + networksTabSelectedRpcUrl, + networksTabIsInAddMode, + } = state.appState + + const frequentRpcNetworkListDetails = frequentRpcListDetail.map(rpc => { + return { + label: rpc.nickname, + iconColor: '#6A737D', + providerType: 'rpc', + rpcUrl: rpc.rpcUrl, + chainId: rpc.chainId, + ticker: rpc.ticker, + blockExplorerUrl: rpc.rpcPrefs && rpc.rpcPrefs.blockExplorerUrl || '', + } + }) + + const networksToRender = [ ...defaultNetworks, ...frequentRpcNetworkListDetails ] + let selectedNetwork = networksToRender.find(network => network.rpcUrl === networksTabSelectedRpcUrl) || {} + const networkIsSelected = Boolean(selectedNetwork.rpcUrl) + + let networkDefaultedToProvider = false + if (!networkIsSelected && !networksTabIsInAddMode) { + selectedNetwork = networksToRender.find(network => { + return network.rpcUrl === provider.rpcTarget || network.providerType !== 'rpc' && network.providerType === provider.type + }) || {} + networkDefaultedToProvider = true + } + + return { + selectedNetwork, + networksToRender, + networkIsSelected, + networksTabIsInAddMode, + providerType: provider.type, + providerUrl: provider.rpcTarget, + networkDefaultedToProvider, + } +} + +const mapDispatchToProps = dispatch => { + return { + setSelectedSettingsRpcUrl: newRpcUrl => dispatch(setSelectedSettingsRpcUrl(newRpcUrl)), + setRpcTarget: (newRpc, chainId, ticker, nickname, rpcPrefs) => { + dispatch(updateAndSetCustomRpc(newRpc, chainId, ticker, nickname, rpcPrefs)) + }, + displayWarning: warning => dispatch(displayWarning(warning)), + setNetworksTabAddMode: isInAddMode => dispatch(setNetworksTabAddMode(isInAddMode)), + editRpc: (oldRpc, newRpc, chainId, ticker, nickname, rpcPrefs) => { + dispatch(editRpc(oldRpc, newRpc, chainId, ticker, nickname, rpcPrefs)) + }, + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(NetworksTab) diff --git a/ui/app/pages/settings/settings.component.js b/ui/app/pages/settings/settings.component.js index 061e65060..a2f137264 100644 --- a/ui/app/pages/settings/settings.component.js +++ b/ui/app/pages/settings/settings.component.js @@ -1,11 +1,12 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' -import { Switch, Route, matchPath } from 'react-router-dom' +import { Switch, Route, matchPath, withRouter } from 'react-router-dom' import { ENVIRONMENT_TYPE_POPUP } from '../../../../app/scripts/lib/enums' import { getEnvironmentType } from '../../../../app/scripts/lib/util' import TabBar from '../../components/app/tab-bar' import c from 'classnames' import SettingsTab from './settings-tab' +import NetworksTab from './networks-tab' import AdvancedTab from './advanced-tab' import InfoTab from './info-tab' import SecurityTab from './security-tab' @@ -16,6 +17,7 @@ import { GENERAL_ROUTE, ABOUT_US_ROUTE, SETTINGS_ROUTE, + NETWORKS_ROUTE, } from '../../helpers/constants/routes' const ROUTES_TO_I18N_KEYS = { @@ -25,7 +27,7 @@ const ROUTES_TO_I18N_KEYS = { [ABOUT_US_ROUTE]: 'about', } -export default class SettingsPage extends PureComponent { +class SettingsPage extends PureComponent { static propTypes = { location: PropTypes.object, history: PropTypes.object, @@ -55,7 +57,7 @@ export default class SettingsPage extends PureComponent { > <div className="settings-page__header"> { - !this.isCurrentPath(SETTINGS_ROUTE) && ( + !this.isCurrentPath(SETTINGS_ROUTE) && !this.isCurrentPath(NETWORKS_ROUTE) && ( <div className="settings-page__back-button" onClick={() => history.push(SETTINGS_ROUTE)} @@ -75,6 +77,7 @@ export default class SettingsPage extends PureComponent { { this.renderTabs() } </div> <div className="settings-page__content__modules"> + { this.renderSubHeader() } { this.renderContent() } </div> </div> @@ -82,6 +85,17 @@ export default class SettingsPage extends PureComponent { ) } + renderSubHeader () { + const { t } = this.context + const { location: { pathname } } = this.props + + return ( + <div className="settings-page__subheader"> + {t(ROUTES_TO_I18N_KEYS[pathname] || 'general')} + </div> + ) + } + renderTabs () { const { history, location } = this.props const { t } = this.context @@ -92,6 +106,7 @@ export default class SettingsPage extends PureComponent { { content: t('general'), description: t('generalSettingsDescription'), key: GENERAL_ROUTE }, { content: t('advanced'), description: t('advancedSettingsDescription'), key: ADVANCED_ROUTE }, { content: t('securityAndPrivacy'), description: t('securitySettingsDescription'), key: SECURITY_ROUTE }, + { content: t('networks'), description: t('networkSettingsDescription'), key: NETWORKS_ROUTE }, { content: t('about'), description: t('aboutSettingsDescription'), key: ABOUT_US_ROUTE }, ]} isActive={key => { @@ -125,6 +140,11 @@ export default class SettingsPage extends PureComponent { /> <Route exact + path={NETWORKS_ROUTE} + component={NetworksTab} + /> + <Route + exact path={SECURITY_ROUTE} component={SecurityTab} /> @@ -135,3 +155,5 @@ export default class SettingsPage extends PureComponent { ) } } + +export default withRouter(SettingsPage) diff --git a/ui/app/selectors/selectors.js b/ui/app/selectors/selectors.js index 2d25aa156..c7cb80024 100644 --- a/ui/app/selectors/selectors.js +++ b/ui/app/selectors/selectors.js @@ -48,6 +48,8 @@ const selectors = { getNumberOfAccounts, getNumberOfTokens, isEthereumNetwork, + getMetaMetricState, + getRpcPrefsForCurrentProvider, } module.exports = selectors @@ -165,7 +167,7 @@ function getSelectedToken (state) { const tokens = state.metamask.tokens || [] const selectedTokenAddress = state.metamask.selectedTokenAddress const selectedToken = tokens.filter(({ address }) => address === selectedTokenAddress)[0] - const sendToken = state.metamask.send.token + const sendToken = state.metamask.send && state.metamask.send.token return selectedToken || sendToken || null } @@ -314,3 +316,22 @@ function preferencesSelector ({ metamask }) { function getAdvancedInlineGasShown (state) { return Boolean(state.metamask.featureFlags.advancedInlineGas) } + +function getMetaMetricState (state) { + return { + network: getCurrentNetworkId(state), + activeCurrency: getSelectedAsset(state), + accountType: getAccountType(state), + metaMetricsId: state.metamask.metaMetricsId, + numberOfTokens: getNumberOfTokens(state), + numberOfAccounts: getNumberOfAccounts(state), + participateInMetaMetrics: state.metamask.participateInMetaMetrics, + } +} + +function getRpcPrefsForCurrentProvider (state) { + const { frequentRpcListDetail, provider } = state.metamask + const selectRpcInfo = frequentRpcListDetail.find(rpcInfo => rpcInfo.rpcUrl === provider.rpcTarget) + const { rpcPrefs = {} } = selectRpcInfo || {} + return rpcPrefs +} diff --git a/ui/app/store/actions.js b/ui/app/store/actions.js index f594d9002..7f6cbea1f 100644 --- a/ui/app/store/actions.js +++ b/ui/app/store/actions.js @@ -239,6 +239,7 @@ var actions = { updateAndSetCustomRpc: updateAndSetCustomRpc, setRpcTarget: setRpcTarget, delRpcTarget: delRpcTarget, + editRpc: editRpc, setProviderType: setProviderType, SET_HARDWARE_WALLET_DEFAULT_HD_PATH: 'SET_HARDWARE_WALLET_DEFAULT_HD_PATH', setHardwareWalletDefaultHdPath, @@ -316,6 +317,7 @@ var actions = { UPDATE_PREFERENCES: 'UPDATE_PREFERENCES', setUseNativeCurrencyAsPrimaryCurrencyPreference, setShowFiatConversionOnTestnetsPreference, + setAutoLogoutTimeLimit, // Migration of users to new UI setCompletedUiMigration, @@ -343,12 +345,21 @@ var actions = { createCancelTransaction, createSpeedUpTransaction, - approveProviderRequest, - rejectProviderRequest, + approveProviderRequestByOrigin, + rejectProviderRequestByOrigin, clearApprovedOrigins, setFirstTimeFlowType, SET_FIRST_TIME_FLOW_TYPE: 'SET_FIRST_TIME_FLOW_TYPE', + + SET_SELECTED_SETTINGS_RPC_URL: 'SET_SELECTED_SETTINGS_RPC_URL', + setSelectedSettingsRpcUrl, + SET_NETWORKS_TAB_ADD_MODE: 'SET_NETWORKS_TAB_ADD_MODE', + setNetworksTabAddMode, + + // AppStateController-related actions + SET_LAST_ACTIVE_TIME: 'SET_LAST_ACTIVE_TIME', + setLastActiveTime, } module.exports = actions @@ -761,7 +772,7 @@ function addNewAccount () { function checkHardwareStatus (deviceName, hdPath) { log.debug(`background.checkHardwareStatus`, deviceName, hdPath) - return (dispatch, getState) => { + return (dispatch) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { background.checkHardwareStatus(deviceName, hdPath, (err, unlocked) => { @@ -782,10 +793,10 @@ function checkHardwareStatus (deviceName, hdPath) { function forgetDevice (deviceName) { log.debug(`background.forgetDevice`, deviceName) - return (dispatch, getState) => { + return (dispatch) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { - background.forgetDevice(deviceName, (err, response) => { + background.forgetDevice(deviceName, (err) => { if (err) { log.error(err) dispatch(actions.displayWarning(err.message)) @@ -803,7 +814,7 @@ function forgetDevice (deviceName) { function connectHardware (deviceName, page, hdPath) { log.debug(`background.connectHardware`, deviceName, page, hdPath) - return (dispatch, getState) => { + return (dispatch) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { background.connectHardware(deviceName, page, hdPath, (err, accounts) => { @@ -824,10 +835,10 @@ function connectHardware (deviceName, page, hdPath) { function unlockHardwareWalletAccount (index, deviceName, hdPath) { log.debug(`background.unlockHardwareWalletAccount`, index, deviceName, hdPath) - return (dispatch, getState) => { + return (dispatch) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { - background.unlockHardwareWalletAccount(index, deviceName, hdPath, (err, accounts) => { + background.unlockHardwareWalletAccount(index, deviceName, hdPath, (err) => { if (err) { log.error(err) dispatch(actions.displayWarning(err.message)) @@ -848,7 +859,7 @@ function showInfoPage () { } function showQrScanner (ROUTE) { - return (dispatch, getState) => { + return (dispatch) => { return WebcamUtils.checkStatus() .then(status => { if (!status.environmentReady) { @@ -987,7 +998,7 @@ function signTypedMsg (msgData) { function signTx (txData) { return (dispatch) => { - global.ethQuery.sendTransaction(txData, (err, data) => { + global.ethQuery.sendTransaction(txData, (err) => { if (err) { return dispatch(actions.displayWarning(err.message)) } @@ -1020,7 +1031,6 @@ function setGasTotal (gasTotal) { function updateGasData ({ gasPrice, blockGasLimit, - recentBlocks, selectedAddress, selectedToken, to, @@ -1402,7 +1412,7 @@ function cancelTx (txData) { * @return {function(*): Promise<void>} */ function cancelTxs (txDataList) { - return async (dispatch, getState) => { + return async (dispatch) => { window.onbeforeunload = null dispatch(actions.showLoadingIndication()) const txIds = txDataList.map(({id}) => id) @@ -1807,7 +1817,7 @@ function removeSuggestedTokens () { return (dispatch) => { dispatch(actions.showLoadingIndication()) window.onbeforeunload = null - return new Promise((resolve, reject) => { + return new Promise((resolve) => { background.removeSuggestedTokens((err, suggestedTokens) => { dispatch(actions.hideLoadingIndication()) if (err) { @@ -1826,7 +1836,7 @@ function removeSuggestedTokens () { } function addKnownMethodData (fourBytePrefix, methodData) { - return (dispatch) => { + return () => { background.addKnownMethodData(fourBytePrefix, methodData) } } @@ -1931,7 +1941,7 @@ function setProviderType (type) { return (dispatch, getState) => { const { type: currentProviderType } = getState().metamask.provider log.debug(`background.setProviderType`, type) - background.setProviderType(type, (err, result) => { + background.setProviderType(type, (err) => { if (err) { log.error(err) return dispatch(actions.displayWarning('Had a problem changing networks!')) @@ -1958,10 +1968,10 @@ function setPreviousProvider (type) { } } -function updateAndSetCustomRpc (newRpc, chainId, ticker = 'ETH', nickname) { +function updateAndSetCustomRpc (newRpc, chainId, ticker = 'ETH', nickname, rpcPrefs) { return (dispatch) => { log.debug(`background.updateAndSetCustomRpc: ${newRpc} ${chainId} ${ticker} ${nickname}`) - background.updateAndSetCustomRpc(newRpc, chainId, ticker, nickname || newRpc, (err, result) => { + background.updateAndSetCustomRpc(newRpc, chainId, ticker, nickname || newRpc, rpcPrefs, (err) => { if (err) { log.error(err) return dispatch(actions.displayWarning('Had a problem changing networks!')) @@ -1974,10 +1984,33 @@ function updateAndSetCustomRpc (newRpc, chainId, ticker = 'ETH', nickname) { } } +function editRpc (oldRpc, newRpc, chainId, ticker = 'ETH', nickname, rpcPrefs) { + return (dispatch) => { + log.debug(`background.delRpcTarget: ${oldRpc}`) + background.delCustomRpc(oldRpc, (err) => { + if (err) { + log.error(err) + return dispatch(self.displayWarning('Had a problem removing network!')) + } + dispatch(actions.setSelectedToken()) + background.updateAndSetCustomRpc(newRpc, chainId, ticker, nickname || newRpc, rpcPrefs, (err) => { + if (err) { + log.error(err) + return dispatch(actions.displayWarning('Had a problem changing networks!')) + } + dispatch({ + type: actions.SET_RPC_TARGET, + value: newRpc, + }) + }) + }) + } +} + function setRpcTarget (newRpc, chainId, ticker = 'ETH', nickname) { return (dispatch) => { log.debug(`background.setRpcTarget: ${newRpc} ${chainId} ${ticker} ${nickname}`) - background.setCustomRpc(newRpc, chainId, ticker, nickname || newRpc, (err, result) => { + background.setCustomRpc(newRpc, chainId, ticker, nickname || newRpc, (err) => { if (err) { log.error(err) return dispatch(actions.displayWarning('Had a problem changing networks!')) @@ -1990,7 +2023,7 @@ function setRpcTarget (newRpc, chainId, ticker = 'ETH', nickname) { function delRpcTarget (oldRpc) { return (dispatch) => { log.debug(`background.delRpcTarget: ${oldRpc}`) - background.delCustomRpc(oldRpc, (err, result) => { + background.delCustomRpc(oldRpc, (err) => { if (err) { log.error(err) return dispatch(self.displayWarning('Had a problem removing network!')) @@ -2000,11 +2033,12 @@ function delRpcTarget (oldRpc) { } } + // Calls the addressBookController to add a new address. function addToAddressBook (recipient, nickname = '') { log.debug(`background.addToAddressBook`) return (dispatch) => { - background.setAddressBook(recipient, nickname, (err, result) => { + background.setAddressBook(recipient, nickname, (err) => { if (err) { log.error(err) return dispatch(self.displayWarning('Address book failed to update')) @@ -2273,7 +2307,7 @@ function pairUpdate (coin) { } } -function shapeShiftSubview (network) { +function shapeShiftSubview () { var pair = 'btc_eth' return (dispatch) => { dispatch(actions.showSubLoadingIndication()) @@ -2309,7 +2343,7 @@ function coinShiftRquest (data, marketData) { } function buyWithShapeShift (data) { - return dispatch => new Promise((resolve, reject) => { + return () => new Promise((resolve, reject) => { shapeShiftRequest('shift', { method: 'POST', data}, (response) => { if (response.error) { return reject(response.error) @@ -2356,7 +2390,7 @@ function shapeShiftRequest (query, options, cb) { !options ? options = {} : null options.method ? method = options.method : method = 'GET' - var requestListner = function (request) { + var requestListner = function () { try { queryResponse = JSON.parse(this.responseText) cb ? cb(queryResponse) : null @@ -2439,6 +2473,10 @@ function setShowFiatConversionOnTestnetsPreference (value) { return setPreference('showFiatInTestnets', value) } +function setAutoLogoutTimeLimit (value) { + return setPreference('autoLogoutTimeLimit', value) +} + function setCompletedOnboarding () { return async dispatch => { dispatch(actions.showLoadingIndication()) @@ -2680,20 +2718,20 @@ function setPendingTokens (pendingTokens) { } } -function approveProviderRequest (tabID) { - return (dispatch) => { - background.approveProviderRequest(tabID) +function approveProviderRequestByOrigin (origin) { + return () => { + background.approveProviderRequestByOrigin(origin) } } -function rejectProviderRequest (tabID) { - return (dispatch) => { - background.rejectProviderRequest(tabID) +function rejectProviderRequestByOrigin (origin) { + return () => { + background.rejectProviderRequestByOrigin(origin) } } function clearApprovedOrigins () { - return (dispatch) => { + return () => { background.clearApprovedOrigins() } } @@ -2712,3 +2750,27 @@ function setFirstTimeFlowType (type) { }) } } + +function setSelectedSettingsRpcUrl (newRpcUrl) { + return { + type: actions.SET_SELECTED_SETTINGS_RPC_URL, + value: newRpcUrl, + } +} + +function setNetworksTabAddMode (isInAddMode) { + return { + type: actions.SET_NETWORKS_TAB_ADD_MODE, + value: isInAddMode, + } +} + +function setLastActiveTime () { + return (dispatch) => { + background.setLastActiveTime((err) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + }) + } +} diff --git a/ui/example.js b/ui/example.js index 4627c0e9c..d940d3bc8 100644 --- a/ui/example.js +++ b/ui/example.js @@ -91,7 +91,7 @@ accountManager.setSelectedAccount = function (address, cb) { this._didUpdate() } -accountManager.signTransaction = function (txParams, cb) { +accountManager.signTransaction = function () { alert('signing tx....') } diff --git a/ui/lib/account-link.js b/ui/lib/account-link.js index 3eaa7cf71..f1428ba92 100644 --- a/ui/lib/account-link.js +++ b/ui/lib/account-link.js @@ -1,4 +1,8 @@ -module.exports = function (address, network) { +module.exports = function (address, network, rpcPrefs) { + if (rpcPrefs.blockExplorerUrl) { + return `${rpcPrefs.blockExplorerUrl}/address/${address}` + } + const net = parseInt(network) let link switch (net) { diff --git a/ui/lib/test-timeout.js b/ui/lib/test-timeout.js index 957b0fce2..7d825487f 100644 --- a/ui/lib/test-timeout.js +++ b/ui/lib/test-timeout.js @@ -1,5 +1,5 @@ export default function timeout (time) { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { setTimeout(resolve, time || 1500) }) } |