From 2082c242069e8c36e590d3aadd9cb96d63dbd83b Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Fri, 1 Nov 2019 17:37:38 -0700 Subject: [PATCH 1/6] update plugin permissions flow add wallet_installPlugins method update inpage provider --- ...dleware.js => internalMethodMiddleware.js} | 34 ++++++++++- .../permissions/loggerMiddleware.js | 23 ++++---- .../controllers/permissions/permissions.js | 58 ++++++++++++------- app/scripts/controllers/plugins.js | 34 +++++++++++ app/scripts/inpage.js | 6 -- package.json | 4 +- yarn.lock | 38 +++++------- 7 files changed, 130 insertions(+), 67 deletions(-) rename app/scripts/controllers/permissions/{requestMiddleware.js => internalMethodMiddleware.js} (64%) diff --git a/app/scripts/controllers/permissions/requestMiddleware.js b/app/scripts/controllers/permissions/internalMethodMiddleware.js similarity index 64% rename from app/scripts/controllers/permissions/requestMiddleware.js rename to app/scripts/controllers/permissions/internalMethodMiddleware.js index ee0653f95..4acd9189c 100644 --- a/app/scripts/controllers/permissions/requestMiddleware.js +++ b/app/scripts/controllers/permissions/internalMethodMiddleware.js @@ -6,7 +6,7 @@ const { errors: rpcErrors } = require('eth-json-rpc-errors') * Create middleware for preprocessing permissions requests. */ module.exports = function createRequestMiddleware ({ - store, storeKey, + store, storeKey, handleInstallPlugins, }) { return createAsyncMiddleware(async (req, res, next) => { @@ -16,8 +16,36 @@ module.exports = function createRequestMiddleware ({ } const prefix = 'wallet_' + const pluginPrefix = prefix + 'plugin_' + if (req.method.startsWith(prefix)) { + switch (req.method.split(prefix)[1]) { + + case 'installPlugins': + + if ( + !Array.isArray(req.params) || typeof req.params[0] !== 'object' + ) { + res.error = rpcErrors.invalidParams(null, req) + return + } + + const requestedPlugins = Object.keys(req.params[0]).filter( + p => p.startsWith(pluginPrefix) + ) + + if (requestedPlugins.length === 0) { + res.error = rpcErrors.invalidParams('Must request at least one plugin.', req) + } + + try { + res.result = await handleInstallPlugins(req.origin, requestedPlugins) + } catch (err) { + res.error = err + } + return + case 'sendDomainMetadata': if ( req.siteMetadata && @@ -27,10 +55,12 @@ module.exports = function createRequestMiddleware ({ } res.result = true return + default: break } - // plugins are handled here + + // plugin metadata is handled here // TODO:plugin handle this better, rename siteMetadata to domainMetadata everywhere } else if ( req.origin !== 'MetaMask' && diff --git a/app/scripts/controllers/permissions/loggerMiddleware.js b/app/scripts/controllers/permissions/loggerMiddleware.js index 73bd367a0..fd5d3e080 100644 --- a/app/scripts/controllers/permissions/loggerMiddleware.js +++ b/app/scripts/controllers/permissions/loggerMiddleware.js @@ -81,23 +81,24 @@ module.exports = function createLoggerMiddleware ({ let accounts const entries = result - .map(perm => { + ? result.map(perm => { if (perm.parentCapability === 'eth_accounts') { accounts = getAccountsFromPermission(perm) } return perm.parentCapability }) - .reduce((acc, m) => { - if (requestedMethods.includes(m)) { - acc[m] = { - lastApproved: time, + .reduce((acc, m) => { + if (requestedMethods.includes(m)) { + acc[m] = { + lastApproved: time, + } + if (m === 'eth_accounts') { + acc[m].accounts = accounts + } } - if (m === 'eth_accounts') { - acc[m].accounts = accounts - } - } - return acc - }, {}) + return acc + }, {}) + : {} if (Object.keys(entries).length > 0) { commitHistory(origin, entries, accounts) diff --git a/app/scripts/controllers/permissions/permissions.js b/app/scripts/controllers/permissions/permissions.js index 11482b683..4b76dca53 100644 --- a/app/scripts/controllers/permissions/permissions.js +++ b/app/scripts/controllers/permissions/permissions.js @@ -9,7 +9,7 @@ const { getExternalRestrictedMethods, pluginRestrictedMethodDescriptions, } = require('./restrictedMethods') -const createRequestMiddleware = require('./requestMiddleware') +const createInternalMethodMiddleware = require('./internalMethodMiddleware') const createLoggerMiddleware = require('./loggerMiddleware') // Methods that do not require any permissions to use: @@ -52,9 +52,10 @@ class PermissionsController { const { origin, isPlugin } = options const engine = new JsonRpcEngine() engine.push(this.createPluginMethodRestrictionMiddleware(isPlugin)) - engine.push(createRequestMiddleware({ + engine.push(createInternalMethodMiddleware({ store: this.store, storeKey: METADATA_STORE_KEY, + handleInstallPlugins: this.handleInstallPlugins.bind(this), })) engine.push(createLoggerMiddleware({ walletPrefix: WALLET_METHOD_PREFIX, @@ -91,6 +92,35 @@ class PermissionsController { }) } + /** + * @param {string} origin - The external domain id. + * @param {Array} requestedPlugins - The names of the requested plugin permissions. + */ + async handleInstallPlugins (origin, requestedPlugins) { + + const existingPerms = this.permissions.getPermissionsForDomain(origin).reduce( + (acc, p) => { + acc[p.parentCapability] = true + return acc + }, {} + ) + + requestedPlugins.forEach(p => { + if (!existingPerms[p]) { + throw rpcErrors.eth.unauthorized(`Not authorized to install plugin '${p}'.`) + } + }) + + const installedPlugins = await this.pluginsController.processRequestedPlugins(requestedPlugins) + + if (installedPlugins.length === 0) { + // TODO:plugins reserve error in Ethereum error space? + throw rpcErrors.eth.custom(4301, 'Failed to install all plugins.', requestedPlugins) + } + + return installedPlugins + } + /** * Returns the accounts that should be exposed for the given origin domain, * if any. This method exists for when a trusted context needs to know @@ -176,31 +206,17 @@ class PermissionsController { const approval = this.pendingApprovals[id] this._closePopup && this._closePopup() - // Load any requested plugins first: - const pluginNames = this.pluginsFromPerms(approved.permissions) try { - await Promise.all(pluginNames.map((plugin) => { - return this.pluginsController.add(plugin) - })) + + // TODO:plugins: perform plugin preflight check? + // e.g., is the plugin valid? can its manifest be fetched? is the manifest valid? + // not strictly necessary, but probably good UX. + // const pluginNames = this.pluginsFromPerms(approved.permissions) const resolve = approval.resolve resolve(approved.permissions) delete this.pendingApprovals[id] - // Once we've approved the initial app permissions, - // we are free to prompt for the plugin permissions: - Promise.all(pluginNames.map(async (pluginName) => { - const plugin = await this.pluginsController.authorize(pluginName) - const { sourceCode, approvedPermissions } = plugin - const ethereumProvider = this.pluginsController.setupProvider(pluginName, async () => { return {name: pluginName } }, true) - await this.pluginsController.run(pluginName, approvedPermissions, sourceCode, ethereumProvider) - })) - .catch((err) => { - // We swallow this error, we don't want the plugin permissions prompt to block the resolution - // Of the main dapp's permissions prompt. - console.error(`Error when adding plugin:`, err) - }) - } catch (reason) { const { reject } = approval reject(reason) diff --git a/app/scripts/controllers/plugins.js b/app/scripts/controllers/plugins.js index 57d850140..805a34769 100644 --- a/app/scripts/controllers/plugins.js +++ b/app/scripts/controllers/plugins.js @@ -137,6 +137,40 @@ class PluginsController extends EventEmitter { }) } + /** + * Adds, authorizes, and runs plugins with a plugin provider. + */ + async processRequestedPlugins (requestedPlugins) { + + const pluginNames = requestedPlugins.map( + perm => perm.replace(/^wallet_plugin_/, '') + ) + + const added = await Promise.all(pluginNames.map(async (pluginName) => { + try { + await this.add(pluginName) + const plugin = await this.authorize(pluginName) + const { sourceCode, approvedPermissions } = plugin + const ethereumProvider = this.setupProvider( + pluginName, async () => { return {name: pluginName } }, true + ) + await this.run( + pluginName, approvedPermissions, sourceCode, ethereumProvider + ) + return pluginName + } catch (err) { + // TODO: figure out what to do with this error + console.error(`Error when adding plugin:`, err) + } + })) + .catch((err) => { + // TODO: will this ever happen? + console.error(`Unexpected error when adding plugins:`, err) + }) + + return added ? added.filter(plugin => !!plugin) : [] + } + /** * Returns a promise representing the complete installation of the requested plugin. * If the plugin is already being installed, the previously pending promise will be returned. diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index a1688b785..6d7214291 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -68,9 +68,3 @@ const proxiedInpageProvider = new Proxy(inpageProvider, { }) window.ethereum = proxiedInpageProvider - -inpageProvider.publicConfigStore.subscribe(function (state) { - if (state.onboardingcomplete) { - window.postMessage('onboardingcomplete', '*') - } -}) diff --git a/package.json b/package.json index 8efe924e7..6e36b07f3 100644 --- a/package.json +++ b/package.json @@ -117,14 +117,14 @@ "gaba": "^1.6.0", "human-standard-token-abi": "^2.0.0", "jazzicon": "^1.2.0", - "json-rpc-engine": "^5.1.4", + "json-rpc-engine": "^5.1.5", "json-rpc-middleware-stream": "^2.1.1", "jsonschema": "^1.2.4", "lodash.debounce": "^4.0.8", "lodash.shuffle": "^4.2.0", "loglevel": "^1.4.1", "luxon": "^1.8.2", - "metamask-inpage-provider": "MetaMask/metamask-inpage-provider#plugins", + "metamask-inpage-provider": "MetaMask/metamask-inpage-provider#snaps", "metamask-logo": "^2.1.4", "mkdirp": "^0.5.1", "multihashes": "^0.4.12", diff --git a/yarn.lock b/yarn.lock index d06a3ba2d..48660bc71 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6028,6 +6028,14 @@ capnode@^4.0.1: crypto-random-string "^1.0.0" stream "0.0.2" +capnode@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/capnode/-/capnode-4.0.2.tgz#405f88de12220d9ffb0eed5a133c28761158385e" + integrity sha512-U+7VWe6cH1qLnZNvScGTRMU1n3pIDX8QSPmZ5gjEJniuSDIMgBpfiammo3XSZLumwDqYf8zlOnxc7tzEbRa4bA== + dependencies: + crypto-random-string "^1.0.0" + stream "0.0.2" + capture-stack-trace@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz#a6c0bbe1f38f3aa0b92238ecb6ff42c344d4135d" @@ -15597,27 +15605,7 @@ json-rpc-engine@^3.4.0, json-rpc-engine@^3.6.0: promise-to-callback "^1.0.0" safe-event-emitter "^1.0.1" -json-rpc-engine@^5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/json-rpc-engine/-/json-rpc-engine-5.1.3.tgz#d7410b649e107ed3437db33797f44c51d507002c" - integrity sha512-/rQm6uts6JtjOVEaeSDCJgHDTlbfKDdoR1Uh3f+6za2SwhJyz+jL9iED2aapU9Yx7decLlI7wjVUIwxRg/R7WQ== - dependencies: - async "^2.0.1" - eth-json-rpc-errors "^1.0.1" - promise-to-callback "^1.0.0" - safe-event-emitter "^1.0.1" - -json-rpc-engine@^5.1.4: - version "5.1.4" - resolved "https://registry.yarnpkg.com/json-rpc-engine/-/json-rpc-engine-5.1.4.tgz#c18d1959eb175049fa7301d4866931ae2f879e47" - integrity sha512-nBFWYJ1mvlZL7gqq0M9230SxedL9CbSYO1WgrFi/C1Zo+ZrHUZWLRbr7fUdlLt9TC0G+sf/aEUeuJjR2lHsMvA== - dependencies: - async "^2.0.1" - eth-json-rpc-errors "^1.1.0" - promise-to-callback "^1.0.0" - safe-event-emitter "^1.0.1" - -json-rpc-engine@^5.1.5: +json-rpc-engine@^5.1.3, json-rpc-engine@^5.1.5: version "5.1.5" resolved "https://registry.yarnpkg.com/json-rpc-engine/-/json-rpc-engine-5.1.5.tgz#a5f9915356ea916d5305716354080723c63dede7" integrity sha512-HTT9HixG4j8vHYrmJIckgbISW9Q8tCkySv7x7Q8zjMpcw10wSe/dZSQ0w08VkDm3y195K4074UlvD3hxaznvlw== @@ -17982,11 +17970,11 @@ mersenne-twister@^1.0.1: resolved "https://registry.yarnpkg.com/mersenne-twister/-/mersenne-twister-1.1.0.tgz#f916618ee43d7179efcf641bec4531eb9670978a" integrity sha1-+RZhjuQ9cXnvz2Qb7EUx65Zwl4o= -metamask-inpage-provider@MetaMask/metamask-inpage-provider#plugins: - version "4.0.0" - resolved "https://codeload.github.com/MetaMask/metamask-inpage-provider/tar.gz/2884eef783b1728ac34c52e3dda601a98885ee87" +metamask-inpage-provider@MetaMask/metamask-inpage-provider#snaps: + version "5.0.0" + resolved "https://codeload.github.com/MetaMask/metamask-inpage-provider/tar.gz/38abec68d77364fd561a74ab7c60b54982d406d8" dependencies: - capnode "^4.0.1" + capnode "^4.0.2" eth-json-rpc-errors "^2.0.0" fast-deep-equal "^2.0.1" json-rpc-engine "^5.1.5" From 431d2aba6da94de241d5b2171ab712db2312feca Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Tue, 26 Nov 2019 10:47:19 -0800 Subject: [PATCH 2/6] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit 3e190d2404db352d44dcc03af161470588af026c Author: Erik Marks Date: Thu Nov 21 11:30:42 2019 -0800 update inpage provider commit c3e46f12ccfeb0d57ef6ff0facac13ac99fd9a9f Author: Erik Marks Date: Thu Nov 21 11:28:05 2019 -0800 prevent notifying connections if extension is locked commit ec72780be1de1f81db87dd7b07186fef270ebaa6 Author: Erik Marks Date: Thu Nov 14 12:20:58 2019 -0800 add eth_requestAccounts handling to background commit 1fcaca48868e795cf91aa22c7cf10412c734a9bb Author: Erik Marks Date: Wed Nov 13 14:40:02 2019 -0800 update inpage provider, rpc-cap commit 35786361a2f5f9c4c7059a1ef0ff9a14df448586 Author: Mark Stacey Date: Wed Nov 13 01:20:55 2019 -0400 Add external extension id to metadata (#7396) commit 5d0d1f7b3a4cdb42b098de25620df5da8b629113 Author: Erik Marks Date: Tue Nov 12 16:33:07 2019 -0800 rename unused messages in non-English locales commit 4757a0aa9525230b960bfe2c9a83678a1bece7fa Author: Erik Marks Date: Tue Nov 12 15:54:12 2019 -0800 add hooks for exposing accounts in settings commit 27a99d41505273163c92554b3d4c40c659499824 Author: Erik Marks Date: Sun Oct 27 20:34:59 2019 -0700 add accountsChange notification to inpage provider move functionality to inpage provider update inpage provider Remove 'Connections' settings page (#7369) commit 33749bca49c3b84c5aae061ec9697ed5d211b0f3 Author: Erik Marks Date: Fri Apr 12 14:28:59 2019 +0800 PermissionsController - init remove provider approval controller integrate rpc-cap create PermissionsController move provider approval functionality to permissions controller add permissions approval ui, settings page add permissions activity and history move some functionality to metamask-inpage-provider rename siteMetadata -> domainMetadata commit f763979beded94a8bfe7717a96e36c9c65f87368 Author: Mark Stacey Date: Fri Nov 22 13:03:51 2019 -0400 Add support for one-click onboarding (#7017) * Add support for one-click onboarding MetaMask now allows sites to register as onboarding the user, so that the user is redirected back to the initiating site after onboarding. This is accomplished through the use of the `metamask-onboarding` library and the MetaMask forwarder. At the end of onboarding, a 'snackbar'-stype component will explain to the user they are about to be moved back to the originating dapp, and it will show the origin of that dapp. This is intended to help prevent phishing attempts, as it highlights that a redirect is taking place to an untrusted third party. If the onboarding initiator tab is closed when onboarding is finished, the user is redirected to the onboarding originator as a fallback. Closes #6161 * Add onboarding button to contract test dapp The `contract-test` dapp (run with `yarn dapp`, used in e2e tests) now uses a `Connect` button instead of connecting automatically. This button also serves as an onboarding button when a MetaMask installation is not detected. * Add new static server for test dapp The `static-server` library we were using for the `contract-test` dapp didn't allow referencing files outside the server root. This should have been possible to work around using symlinks, but there was a bug that resulted in symlinks crashing the server. Instead it has been replaced with a simple static file server that will serve paths starting with `node_modules` from the project root. This will be useful in testing the onboarding library without vendoring it. * Add `@metamask/onboarding` and `@metamask/forwarder` Both libraries used to test onboarding are now included as dev dependencies, to help with testing. A few convenience scripts were added to help with this (`yarn forwarder` and `yarn dapp-forwarder`) commit 015ba83c6e69dcca676d626f70ef887a38ce01b9 Author: Tyson Malchow Date: Fri Nov 22 09:13:23 2019 -0600 Update Wyre buy ETH URL (#7482) commit bf57dd5ad918210c5f7e6680ed09d94f5f4b9acf Author: ricky Date: Thu Nov 21 16:59:50 2019 -0500 Add overflow hidden (#7488) commit ff12d0572739d80faadac690e10b46ef8331ed2b Author: Whymarrh Whitby Date: Thu Nov 21 18:09:29 2019 -0330 Remove stray periods from settings tabs descriptions (#7489) commit 71a0bc8b3ff94478e61294c815770e6bc12a72f5 Author: Mark Stacey Date: Wed Nov 20 18:03:49 2019 -0400 Refactor signature-request-original (#7415) The component has been updated to split the container from the base component, and all hyperscript has been replaced with JSX. ES6 imports are used throughout as well. commit a8be9ae42be3e0421114f7393dbf4863808f262a Author: Antonio Savage Date: Wed Nov 20 08:42:36 2019 -0600 Fix link on root README.md (#7480) commit 056e8cdf7e948452b172eb45642228b62e8aca04 Merge: aa4105762 72eb233ee Author: Mark Stacey Date: Tue Nov 19 21:10:55 2019 -0400 Merge pull request #7479 from MetaMask/master Master sync commit aa41057628a812cfa3d330fd36684ca17ab2852d Author: Whymarrh Whitby Date: Tue Nov 19 20:33:20 2019 -0330 Update ESLint rules for curly braces style (#7477) * eslint: Enable curly and brace-style * yarn lint --fix commit 72eb233ee953c381407f0121f76822a79753e4ee Merge: 86c39c806 ea93408bd Author: Thomas Huang Date: Tue Nov 19 15:00:37 2019 -0800 Merge pull request #7474 from MetaMask/Version-v7.6.1 Version v7.6.1 RC commit ea93408bddf6fc51767d1a046650b3ae811a6d24 Author: Mark Stacey Date: Tue Nov 19 17:35:14 2019 -0400 Revert "Update abi-decoder (#7472)" This reverts commit a26e77c61e5117b9db3fe852deed63f1d29f659e. commit cbd4f3f76eb6fe21494bf2c605fb1d4d7eb9180f Merge: 038e2e8ca ab0600ef0 Author: Thomas Huang Date: Tue Nov 19 12:55:35 2019 -0800 Merge pull request #7475 from whymarrh/mkr-follow-up Add 'Remind Me Later' to the Maker notification commit 038e2e8ca997ecf88defa4fca54183fce3d7b74c Author: Mark Stacey Date: Tue Nov 19 16:24:48 2019 -0400 Update changelog for v7.6.1 commit ab0600ef0ba690da15de0c7d2b636703f6af2336 Author: Whymarrh Whitby Date: Tue Nov 19 16:04:33 2019 -0330 Add Remind Me Later to SAI migration notification commit 5f9e8867b4377556796aa92625c613fae603805f Author: Whymarrh Whitby Date: Tue Nov 19 15:01:23 2019 -0330 Allow any renderable type in HomeNotification texts commit 38f8d9906d8d113a549a5cbceea8d6b45cd8f229 Author: MetaMask Bot Date: Tue Nov 19 19:44:23 2019 +0000 Version v7.6.1 commit 476274474f5fc709b176f481d66865f190e14a52 Author: ricky Date: Tue Nov 19 10:46:10 2019 -0500 Add shellcheck lint (#7392) * Add shellcheck lint script * Add to build * Add shellcheck lint to main lint task * Put shellcheck in the right place, hopefully? * Fix declared multiple executor types * Add sudo * Address shellcheck warnings * Add test-lint-shellcheck * Add test-lint-shellcheck to workflow * Use correct lint task * output version which could be helpful for debugging * Address PR feedback * consistency++ commit a26e77c61e5117b9db3fe852deed63f1d29f659e Author: Mark Stacey Date: Tue Nov 19 09:43:42 2019 -0400 Update abi-decoder (#7472) This is a major version update, but it appears that the reason for the breaking change was that the dependency on `web3` was dropped in favour of using two `web3` utility libraries instead. I could find no indication of a breaking change to the API of `abi-decoder` itself. commit 5c356a4cac8f1cbefc3d863e22d8273ab73614aa Author: Mark Stacey Date: Tue Nov 19 09:41:28 2019 -0400 Show transaction fee units on approve screen (#7468) The units for the amounts shown on the approve screen in the transaction fee section were missing. It appears that they were present in an early version of the approve screen (#7271) but they got lost somewhere along the way. commit 346c1f2622d03fad76d0b0f94a3ba5d4ad269c4c Author: ricky Date: Tue Nov 19 00:11:50 2019 -0500 Add additional rpcUrl verification (#7436) * Add additional url verification * Add commas * Address PR feedback * Use URL over URI * Update key in other languages * Add stateKey check * Split validateUrl into two separate methods * Remove unused variable * Add isValidWhenAppended method commit 9d03865274441ec08aaa9c86d6192cab6d57482f Merge: 4ac247462 2a90a886b Author: Thomas Huang Date: Mon Nov 18 20:02:28 2019 -0800 Merge pull request #7470 from MetaMask/update-changelog Update v7.6.0 changelog commit 2a90a886b220aaf7f1dacb8504360636f925cea5 Author: Thomas Date: Mon Nov 18 18:14:02 2019 -0800 Update v7.6.0 changelog commit 4ac247462bccf0bdc7ade96a3655c926d7786753 Merge: b0890b6b3 86c39c806 Author: Thomas Huang Date: Mon Nov 18 17:50:03 2019 -0800 Merge pull request #7469 from MetaMask/master Master sync commit 86c39c806e6eda8b6e29f706c5837285168dc8a9 Merge: 9a3b85a54 a932dce6e Author: Thomas Huang Date: Mon Nov 18 17:15:55 2019 -0800 Merge pull request #7466 from MetaMask/Version-v7.6.0 Version v7.6.0 RC commit b0890b6b32a10c0cddd9f4482d4d5bf5db8bcdcb Author: Whymarrh Whitby Date: Mon Nov 18 19:53:41 2019 -0330 Enforce a single boolean attr notation in JSX (#7465) This changeset enables the ESLint rule enforcing a single notation for boolean attributes in JSX—explictly setting the value to `true` is no longer allowed (as it was never needed).[1] From the docs for JSX:[2] > If you pass no value for a prop, it defaults to `true`. [1]:https://github.com/yannickcr/eslint-plugin-react/blob/80935658/docs/rules/jsx-boolean-value.md [2]:https://reactjs.org/docs/jsx-in-depth.html#props-default-to-true I have chosen to use this default as it the most consistent with HTML (a la `checked` and `disabled`). commit a932dce6e39d7a58417298d9139b6e4829308a83 Merge: 5b88a1da9 7f5d82988 Author: Thomas Huang Date: Mon Nov 18 15:22:41 2019 -0800 Merge pull request #7467 from MetaMask/adjust-notification-icon Set default height of notification icon commit 7f5d82988075a8fe9a031e3ea944002a6b6545b6 Author: Thomas Date: Mon Nov 18 14:45:10 2019 -0800 Set default height and alignment of notification icon commit 5b88a1da9aee8373b1be8dbe8e50d4e939eb3222 Author: MetaMask Bot Date: Mon Nov 18 21:53:48 2019 +0000 Version v7.6.0 commit 86b165ea83be3afb4f2751a7bfd36f8544049b20 Author: Whymarrh Whitby Date: Mon Nov 18 18:16:28 2019 -0330 Add migration notification for users with Sai (#7450) Maker has upgraded its Dai token to "Multi-Collateral Dai" (MCD) and requires all users interacting with Dai migrate their tokens to the new version. Dai now exclusively refers to Multi-Collateral Dai and what was previouly called Dai is now Sai (Single Collateral Dai). In this description, Sai refers to what was (prior to the 2019-11-18) known as Dai. Dai is the _new_ token. This changeset: 1. Only affects users who had non-zero Sai at the old contract address 2. Displays a persistent notification for users with Sai 3. Updates the token symbol for users already tracking the Sai token 4. Bumps our direct and indirect eth-contract-metadata dependencies The notification copy: > A message from Maker: The new Multi-Collateral Dai token has been released. Your old tokens are now called Sai. Please upgrade your Sai tokens to the new Dai. The copy is from the Maker team. commit b3395502f2915a8b2ed1aa5d38bd93f997eea65f Merge: 935cf9312 3003104a5 Author: Thomas Huang Date: Mon Nov 18 12:32:38 2019 -0800 Merge pull request #7463 from MetaMask/expand-home-notification-height Expand home notification height commit 935cf931204c0d0987505eeec95040b7d0577438 Merge: 51bfe5651 273ec7bee Author: Thomas Huang Date: Mon Nov 18 12:21:42 2019 -0800 Merge pull request #7461 from MetaMask/fix-home-notification-styles Import styles for showing multiple notifications commit 3003104a5daa5a61b5ec00aa9dc6d2d5e6d4ff74 Author: Mark Stacey Date: Mon Nov 18 16:03:21 2019 -0400 Expand home notification height The home notification static height of 116px is too cramped for longer messages, such as the one used for the Sai migration. The old height is now used for the minimum height instead, with a margin to ensure the text doesn't get too close to the buttons. commit 273ec7beeff3f7f0cf363d1b798167d69b45d3f3 Author: Mark Stacey Date: Mon Nov 18 15:49:27 2019 -0400 Import styles for showing multiple notifications The styles for the multi-notification component on the home screen were accidentally removed while resolving a merge conflict in #6891. Fixes #7460 commit 51bfe56510fe9e7039c9d2b0f7a00e5f3f1669fa Author: Whymarrh Whitby Date: Mon Nov 18 14:19:03 2019 -0330 Disallow spaces around the equal sign in JSX (#7459) commit f1384e75225d3bbcf7adf1c413b46c9fce03250c Author: Whymarrh Whitby Date: Mon Nov 18 11:38:47 2019 -0330 Disable unnecessary curly braces in JSX (#7454) commit 659b4360bc1a0bfb6c7effd7549dbb64b9f1d963 Author: Whymarrh Whitby Date: Mon Nov 18 11:38:10 2019 -0330 Add ESLint rule forbidding extraneous defaultProps (#7453) commit a271e7f45643b7ac5f42b125b8075aba055e502e Author: Whymarrh Whitby Date: Sun Nov 17 15:32:26 2019 -0330 Update Node version to 10.17 (#7447) commit a6e387fddc44354f513fabaebbc72d6acf08bb2b Author: Sunghee Lee <630sunghee@gmail.com> Date: Mon Nov 18 02:34:17 2019 +0900 Add button disabled when password is empty (#7451) commit 7a6f2693fec455065d1a20a075c7e140259bb9ad Author: Whymarrh Whitby Date: Sat Nov 16 19:43:46 2019 -0330 Add defaultProps to MultipleNotifications (#7449) commit d41522d5c08d9cfa0fd6ad5fef242557b2183ba7 Author: Whymarrh Whitby Date: Sat Nov 16 19:43:20 2019 -0330 Cleanup MultipleNotifications jsx in Home (#7448) commit 076adda5be5866cbd034eb57da7b3e5a186edc00 Merge: 37b5449c1 9a3b85a54 Author: Mark Stacey Date: Fri Nov 15 18:40:52 2019 -0400 Merge pull request #7442 from MetaMask/master Master sync commit 9a3b85a544b2b2077eb06c64262a9a72ff886e6d Merge: 1e609e851 83e34d903 Author: Dan Finlay <542863+danfinlay@users.noreply.github.com> Date: Fri Nov 15 13:50:38 2019 -0800 Merge pull request #7441 from MetaMask/Version-v7.5.3 Version v7.5.3 RC commit 83e34d90338b5043d094460cbf64e305f96b2fc6 Author: Whymarrh Whitby Date: Fri Nov 15 16:19:55 2019 -0330 Update changelog for v7.5.3 commit c3966aecdbcbf7ad13ff8bdf182c3c14be3c5f95 Author: MetaMask Bot Date: Fri Nov 15 19:39:23 2019 +0000 Version v7.5.3 commit 37b5449c132b81b9c37b1399502baaf4d897c0ff Author: Eduardo Antuña Díez <20141918+eduadiez@users.noreply.github.com> Date: Fri Nov 15 19:23:46 2019 +0100 Added webRequest.RequestFilter to filter main_frame .eth requests (#7419) Added webRequest.RequestFilter to filter main_frame .eth requests commit 6e5efed7d9c6606618affcfebec8adf84322c2df Merge: 23ab6e2d5 1e609e851 Author: Mark Stacey Date: Fri Nov 15 14:08:55 2019 -0400 Merge pull request #7440 from MetaMask/master Master sync commit 23ab6e2d544d1879882214c97a96defc2128fb57 Author: Mark Stacey Date: Fri Nov 15 14:04:36 2019 -0400 Add metricsEvent to contextTypes (#7439) The metricsEvent context type was missing, resulting in an error when it was called. commit 494ad031760e60ecfa070363d805917bdbd4909a Merge: 3ffda6c68 f8aaec6d3 Author: Dan Finlay <542863+danfinlay@users.noreply.github.com> Date: Fri Nov 15 09:41:35 2019 -0800 Merge pull request #7420 from MetaMask/e2e-test-for-typed-sig-request Adds and end to end test for typed signature requests commit 3ffda6c684059aeab7f1129f19cefe2bb539a82b Merge: 10114a969 1c6e09b1c Author: Dan Finlay <542863+danfinlay@users.noreply.github.com> Date: Fri Nov 15 09:40:51 2019 -0800 Merge pull request #7410 from MetaMask/fix-sourcemaps Fix sourcemaps commit 10114a969790eedc32844fa131a37f9891a75083 Author: Mark Stacey Date: Fri Nov 15 13:23:59 2019 -0400 Update 512px icon (#7434) This updated 512px icon morely closely resembles the other sizes, particularly in colour. commit 00e43d0b47e630f860226ee9723c99d917009573 Author: Whymarrh Whitby Date: Fri Nov 15 13:47:42 2019 -0330 Ensure Etherscan result is valid before reading it (#7426) commit 5e84c5055c7a1e31993f7d75905e6c26c3db13e4 Author: Mark Stacey Date: Thu Nov 14 23:31:34 2019 -0400 Add all icons to manifest (#7431) Of the 7 different icon sizes we have, only four were referenced in the manifest. All 7 are now listed, which leaves the browser more to choose from when deciding which to use. commit fb83b2937e23f991fcfa5982d4bf8421b866a990 Author: Sergej Müller Date: Thu Nov 14 23:54:24 2019 +0100 Utilize the full size of icon space (#7408) * Utilize the full width of icon space * Replace extension icon by the front-facing logo commit 64d7a95dffe34dbde4e01352e23aeb4984a93deb Merge: f5cec3e6b 5bce06098 Author: Dan Finlay <542863+danfinlay@users.noreply.github.com> Date: Thu Nov 14 14:49:56 2019 -0800 Merge pull request #7430 from MetaMask/change-browser-action-badge-colour Update badge colour commit 5bce06098571cf61a957e9835c9dbb485e2f37d6 Author: Mark Stacey Date: Thu Nov 14 18:19:03 2019 -0400 Update badge colour The badge colour is now '#037DD6', which stands out a bit more on both light and dark modes. commit 1e609e8518f287a004afd58cdac1c73e8c614f2a Merge: 44109337d b36e611cc Author: Thomas Huang Date: Thu Nov 14 12:27:56 2019 -0800 Merge pull request #7428 from MetaMask/Version-v7.5.2 Version v7.5.2 RC commit b36e611cca104406682337cf025669fcb481ca47 Author: Mark Stacey Date: Thu Nov 14 15:57:45 2019 -0400 Update changelog for v7.5.2 commit fcc6baf0d11f49dc4de306b09ed1013870b222ea Author: Mark Stacey Date: Thu Nov 14 14:28:40 2019 -0400 Ensure SignatureRequestOriginal 'beforeunload' handler is bound (#7414) The 'beforeunload' handler was being bound to the module scope instead of the instance scope, because the class was defined using prototypes rather than the ES6 class syntax. The arrow functions were removed, and the handler is now bound explicitly in the constructor. commit 18622d7dcac9984f15ad88c211661c879c2e4a88 Author: MetaMask Bot Date: Thu Nov 14 19:53:55 2019 +0000 Version v7.5.2 commit f5cec3e6b7a2fa303ffbeec96cc3e6240b1612a0 Author: Mark Stacey Date: Thu Nov 14 14:28:40 2019 -0400 Ensure SignatureRequestOriginal 'beforeunload' handler is bound (#7414) The 'beforeunload' handler was being bound to the module scope instead of the instance scope, because the class was defined using prototypes rather than the ES6 class syntax. The arrow functions were removed, and the handler is now bound explicitly in the constructor. commit a34f1eae53a96114e80b1ff898dcc6dff480bfa6 Merge: f2e3fa58b 9ca22d8fa Author: Mark Stacey Date: Thu Nov 14 14:27:43 2019 -0400 Merge pull request #7416 from MetaMask/add-eslint-import-plugin Add eslint import plugin to help detect unresolved paths commit f2e3fa58b6280534e9fa92e085ce7b0c350635f6 Author: Whymarrh Whitby Date: Thu Nov 14 13:17:32 2019 -0330 circleci: v2.1 (#7421) commit f8aaec6d32aa342a17e76bf72ae4d12efb05145b Author: Dan Miller Date: Thu Nov 14 10:22:34 2019 -0330 Adds and end to end test for typed signature requests commit 9ca22d8fae9689059763c4284aba553c3f2f5845 Author: Mark Stacey Date: Thu Nov 14 09:17:55 2019 -0400 Disable `import/no-unresolved` on lines that require build Certain lines only work after a build stage has been completed, so these failure can be ignored by the no-unresolved rule. commit a57ff0b6813c68d17c27528e17a8d94d8ffa1d63 Author: Mark Stacey Date: Wed Nov 13 19:26:26 2019 -0400 Add eslint import plugin to help detect unresolved paths Most of the rules in the import plugin are only useful for projects using purely ES6 imports. The `no-unresolved` rule works with mixed CommonJS and ES6 though, so we at least benefit from that in the meantime. commit 3673459a54bc9794f0a3b4600d6b3b6803542281 Author: Bruno Barbieri Date: Wed Nov 13 20:05:56 2019 -0500 lock eth-contract-metadata (#7412) commit 1c6e09b1cebcc2964197778baddf7cced90da160 Author: Mark Stacey Date: Wed Nov 13 16:54:04 2019 -0400 Fix sourcemaps The `install` script of `@sentry/cli` is required for the Sentry CLI to work correctly. Without this step, the sourcemap upload fails silently. commit 33d6abf0e2b925f68f0756eac8d919bb8d19fac0 Merge: 5a8e8c61e 44109337d Author: Mark Stacey Date: Wed Nov 13 14:40:52 2019 -0400 Merge pull request #7406 from MetaMask/master Master sync commit 44109337d0b0490d37d8ca687d9e68303929dd7e Merge: c594bb340 54d2ec936 Author: Dan Finlay <542863+danfinlay@users.noreply.github.com> Date: Wed Nov 13 09:30:12 2019 -0800 Merge pull request #7405 from MetaMask/Version-v7.5.1 Version v7.5.1 RC commit 54d2ec9363d19414378479b1cbade855c96233e1 Author: Dan Miller Date: Wed Nov 13 11:08:48 2019 -0330 Update changelog for v7.5.1 commit ecfb629cd68bedea2c9ceacbca383bcf4ad08258 Author: MetaMask Bot Date: Wed Nov 13 14:39:24 2019 +0000 Version v7.5.1 commit 5a8e8c61ea4e5bb8aa7b77bae786d9158269af37 Author: Mark Stacey Date: Wed Nov 13 11:03:11 2019 -0400 Reject connection request on window close (#7401) This was first implemented in #7335, but the final version didn't work because the `_beforeUnload` handler was not bound early, so `this` was not defined when it was triggered. commit 4b4c00e94fbb2f38746521864a568aab1400aac4 Author: Mark Stacey Date: Wed Nov 13 11:02:59 2019 -0400 Revert "Use common test build during CI (#7196)" (#7404) This reverts commit b0ec610908f41157b31f8e6c76ae35c87c43509d, which was introduced in #7196. This change was preventing the sourcemaps from being uploaded correctly. commit 9df268cdf1c524d8510cd44ece2fdf5b4a7d0df1 Merge: 6acd5cec3 c594bb340 Author: Mark Stacey Date: Wed Nov 13 10:39:47 2019 -0400 Merge pull request #7403 from MetaMask/master Master/Develop release parity. commit 6acd5cec370ba10515b189cfa7c3d7f31ebf1cfc Author: Dan J Miller Date: Wed Nov 13 09:11:21 2019 -0500 Get prop for signed typed data from domain instead of message (#7402) commit ce7f5166c829dc0d7a95c4129424a070d321a293 Author: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue Nov 12 13:29:56 2019 -0800 Update json-rpc-engine (#7390) * update json-rpc-engine * update lockfile resolution commit c594bb340d2f069b9db5115df23c6294091a1618 Merge: 41e834f88 aeabdfdf5 Author: Thomas Huang Date: Tue Nov 12 11:52:19 2019 -0800 Merge pull request #7351 from MetaMask/Version-v7.5.0 Version v7.5.0 RC commit 069a24c1b94b828d4a2bf7401c8bfe4b11db1405 Author: Thomas Huang Date: Mon Nov 11 13:18:57 2019 -0800 Revert "Adds Wyre Widget (#6434)" (#7385) This reverts commit 6a4df0dc3f620ef0bfe4c1255c1f791390b4280a. commit aeabdfdf51ab3060db1a75583cd05131e8cc712f Author: Mark Stacey Date: Tue Nov 5 13:55:21 2019 -0400 Update changelog for v7.5.0 commit 2ac96919fc6ce1feca67690b9434528a69a6d8eb Author: MetaMask Bot Date: Mon Nov 4 21:52:21 2019 +0000 Version v7.5.0 commit 50bd1830f07498b903333e2a5a525365a30e2ce7 Author: Thomas Huang Date: Mon Nov 11 11:22:18 2019 -0800 Revert "Adds Wyre Widget (#6434)" This reverts commit 6a4df0dc3f620ef0bfe4c1255c1f791390b4280a. commit 78224601b981829345bf85dc40c0bd3f08fec57b Author: ricky Date: Sun Nov 10 21:17:00 2019 -0500 Fix grid-template-columns (#7366) * Fix grid-template-columns * Add fullscreen check commit 728115171e12cb9953a6b1f8ed16a9f213e44ed1 Author: Mark Stacey Date: Sun Nov 10 21:15:59 2019 -0500 Catch reverse resolve ENS errors (#7377) The 'reverseResolveAddress' method is intended to return undefined if unable to reverse resolve the given address. Instead it was throwing an error, which surfaced in the UI console. This error is now caught. commit 66187333b134d1bb818586bb4432bcb86cc93209 Author: Mark Stacey Date: Sun Nov 10 21:15:50 2019 -0500 Prevent attempting ENS resolution on unsupported networks (#7378) The check for whether the network is supported was performed in the constructor, but it was accidentally omitted from the network change handler. commit 42279c474bbb0b1cb83a3b2521cc8356a376e816 Author: Mark Stacey Date: Sun Nov 10 21:15:38 2019 -0500 Set default advanced tab gas limit (#7379) * Set default advanced tab gas limit The advanced tab of the transaction confirmation screen would enter into an infinite loop and crash if the given gas price was falsy and some interaction was made with the gas limit field. To prevent this infinite loop, a default value of 0 has been set. The user will still need to update the gas limit in order to confirm the transaction, as zero is too low a gas limit (the lowest is 21000). 21000 cannot be the default gas limit at this layer, because the limit used is from a layer above this, which wouldn't have that same 21000 set. * Set default gas limit to minimum allowed A transaction initiated from a dapp might not set a gas limit, which would result in a default of zero being used in the advanced tab. The default gas limit in that case has been changed to 21,000, the minimum allowed gas limit, so that users aren't forced to manually update it. commit a8175eb799935d9f7e3a0cb74269e78d7cfc5874 Author: Mark Stacey Date: Sun Nov 10 21:14:53 2019 -0500 Fix advanced tab gas chart (#7380) The gas chart on the advanced tab was not converting the gas price selected into hex before setting it in state, resulting in the UI throwing errors and the price being set incorrectly. It now converts in the same manner as the input fields. commit efe240195bc15889f4f27510694128bfe27bed48 Author: matteopey Date: Sun Nov 10 08:43:36 2019 +0100 Hide accounts dropdown scrollbars on Firefox (#7374) commit ec1f3fa19a2261b04a034ea104e1c04c3acb1439 Author: Mark Stacey Date: Sun Nov 10 01:30:07 2019 -0500 Fix threebox last updated proptype (#7375) * Use child components for multiple notifications component The multiple notifications component has been updated to take its child components as children rather than as a props array, so that the child components are never executed in the case where they aren't needed. * Fix threebox last updated proptype commit 02aebc2e03d045f919d790de9fc0c5d8b7e2f698 Author: ricky Date: Wed Nov 6 12:04:44 2019 -0500 Add onbeforeunload and have it call onCancel (#7335) * Add onbeforeunload and have it call onCancel * Address PR feedback * Get integration tests passing again * Add underscores * Add ENVIRONMENT_TYPE_NOTIFICATION check * Add _beforeUnload + metricsEvent commit b27b568c32ee78d7928838eb0ad4421f6ed54579 Author: Whymarrh Whitby Date: Wed Nov 6 12:33:49 2019 -0330 Update to gaba@1.8.0 (#7357) commit adb2b0aab40b5647ad61ccaf587fa6019adedad9 Merge: 9ed01dff7 41e834f88 Author: Thomas Huang Date: Tue Nov 5 09:43:28 2019 -0800 Merge pull request #7353 from MetaMask/master-parity Develop parity with master/release 7.4.0 commit 9ed01dff7adf03bcaea0cf3ed4dcab2e94943135 Author: hjlee9182 <46337218+hjlee9182@users.noreply.github.com> Date: Wed Nov 6 00:31:28 2019 +0900 fix account menu width (#7354) commit 2673eef3c4f88876d095d0e20e0e21bc312a2af7 Author: Dan J Miller Date: Tue Nov 5 11:43:48 2019 -0330 Redesign approve screen (#7271) * Redesign approve screen * Add translations to approve screen components * Show account in header of approve screen * Use state prop bool for unlimited vs custom check in edit-approval-permission * Set option to custom on input change in edit-approval-permission * Allow setting of approval amount to unlimited in edit-approval-permission * Fix height of confirm-approval popup * Ensure decimals prop passted to confirm-approve.component is correct type * Ensure first param passed to calcTokenValue in confirm-approve.util is the correct type * Fix e2e test of permission editing * Remove unused code from edit-approval-permission.container commit 41e834f88c12a05c371464de972be706b744e708 Merge: 06dc3b260 05b007aa3 Author: Thomas Huang Date: Mon Nov 4 15:39:57 2019 -0800 Merge pull request #7323 from MetaMask/Version-v7.4.0 Version v7.4.0 RC commit 05b007aa358c47f5d74560c0d1229fd4f9175e82 Author: Mark Stacey Date: Mon Nov 4 17:28:50 2019 -0400 Fix provider approval metadata (#7349) * Omit MetaMask `extensionId` from site metadata The site metadata was updated in #7218 to include the extension id of the extension connecting to MetaMask. This was done to allow external extensions to connect with MetaMask, so that we could show the id on the provider approval screen. Unbeknownst to me at the time, the extension id was being set for all connections to MetaMask from dapps. The id was set to MetaMask's id, because the connections are made through MetaMask's contentscript. This has been updated to only set the id when accepting a connection from a different extension. * Fix `siteMetadata` property names In #7218 a few things were added to the site metadata, so the provider approval controller was middleware was updated to accept the site metadata as an object rather than accepting each property as a separate parameter. Unfortunately we failed to notice that the site name and icon were named differently in the site metadata than they were in the provider approval controller, so the names of those properties were unintentionally changed in the controller state. The provider approval controller has been updated to restore the original property names of `siteTitle` and `siteIcon`. An unused prop that was added to the provider approval page in #7218 has also been removed. commit 99b8f2d5445ba2604516c064757b257774066b4d Author: Mark Stacey Date: Mon Nov 4 17:28:50 2019 -0400 Fix provider approval metadata (#7349) * Omit MetaMask `extensionId` from site metadata The site metadata was updated in #7218 to include the extension id of the extension connecting to MetaMask. This was done to allow external extensions to connect with MetaMask, so that we could show the id on the provider approval screen. Unbeknownst to me at the time, the extension id was being set for all connections to MetaMask from dapps. The id was set to MetaMask's id, because the connections are made through MetaMask's contentscript. This has been updated to only set the id when accepting a connection from a different extension. * Fix `siteMetadata` property names In #7218 a few things were added to the site metadata, so the provider approval controller was middleware was updated to accept the site metadata as an object rather than accepting each property as a separate parameter. Unfortunately we failed to notice that the site name and icon were named differently in the site metadata than they were in the provider approval controller, so the names of those properties were unintentionally changed in the controller state. The provider approval controller has been updated to restore the original property names of `siteTitle` and `siteIcon`. An unused prop that was added to the provider approval page in #7218 has also been removed. commit eef570cf952b4be8d430fe25869295851f9fe291 Author: hjlee9182 <46337218+hjlee9182@users.noreply.github.com> Date: Tue Nov 5 04:08:17 2019 +0900 fix width in first time flow button (#7348) commit dbd14d796c4cc1f0d42fbe58a096c3e41d5590a5 Author: Mark Stacey Date: Mon Nov 4 14:03:57 2019 -0400 Clear `beforeunload` handler after button is pressed (#7346) On the signature request and transaction confirmation notification pages, the closure of the notification UI implies that the request has been rejected. However, this rejection is being submitted even when the window is closed as a result of the user explicitly confirming or rejecting. In practice, I suspect this has no effect because the transaction, after being explicitly confirmed or rejected, has already been moved out of a pending state. But just in case there is some present or future edge case that might be affected, the `beforeunload` handler is now removed once the user has explicitly made a choice. This mistake was introduced recently in #7333 commit 6a4df0dc3f620ef0bfe4c1255c1f791390b4280a Author: Dan J Miller Date: Mon Nov 4 11:13:24 2019 -0330 Adds Wyre Widget (#6434) * Adds Wyre widget to the deposit modal. * Move wyre widget code to vendor directory * Get Wyre widget working without metamask connect/sign steps * Code cleanup for wyre changes * Change wyre widget to using prod environment * Remove code allowing signing of wyre messages without confirmations * Update wyre vendor code for wyre 2.0 * Remove unnecessary changes to provider approval constructor, triggerUI and openPopup * Fix Wyre translation message * Delete no longer used signature-request-modal * Fix documentation of matches function in utils/util.js * Code cleanup on wyre branch * Remove front end code changes not needed to support wyre v2 commit 57a29668f3caf3716fb99b904a53ec71cbce6ecb Author: Terry Smith <52763493+tshfx@users.noreply.github.com> Date: Mon Nov 4 08:40:46 2019 -0400 New signature request v3 UI (#6891) * Refactoring signature-request out to a new component. Wip * Styling polish and a better message display. * Update signature request header to no longer use account dropdown mini * Clean up code and styles * Code cleanup for signature request redesign branch * Fix signature request design for full screen * Replace makenode with object.entries in signature-request-message.component.js * Remove unused accounts prop from signature-request.component.js * Use beforeunload instead of window.onbeforeunload in signature-request commit eed4a9ed6547c76299da086916168c48a4e8fef4 Author: Whymarrh Whitby Date: Fri Nov 1 15:24:00 2019 -0230 ENS Reverse Resolution support (#7177) * ENS Reverse Resolution support * Save punycode for ENS domains with Unicode characters * Update SenderToRecipient recipientEns tooltip * Use cached results when reverse-resolving ENS names * Display ENS names in tx activity log commit 0138b0f9bde1256e7d79fae50bdd529b6d22333f Author: Mark Stacey Date: Wed Oct 30 15:34:23 2019 -0300 Update Changelog for v7.4.0 commit dcd3b059dfb0e75a7cdf145c06ab6232bc3cecf9 Author: MetaMask Bot Date: Tue Oct 29 16:13:53 2019 +0000 Version v7.4.0 commit 5455b8e3fd288dea6aa85091032de7ac68860dc5 Author: Whymarrh Whitby Date: Thu Oct 31 21:56:02 2019 -0230 Add web3 deprecation warning (#7334) * Add web3 deprecation warning * Update web3 deprecation article URL commit f9cd775eae5195f1d7ca4ba7c81f77cdda402ac5 Author: Kristian Tapia Date: Thu Oct 31 18:51:28 2019 -0700 Add Estimated time to pending tx (#6924) * Add estimated time to pending transactions * add sytles for pending transactions component * add media queries styling for pending transactions component * fix lint errors, remove extra spaces * refactor code to call `fetchBasicGasAndTimeEstimates` method once * refactor code to call `getgetRenderableTimeEstimate` method once * fix, correct export to use `transaction-time-remaining-component` * fix indentation issues after running `yarn lint` * newBigSigDig in gas-price-chart.utils supports strings * Code cleanup * Ensure fetchBasicGasAndTimeEstimates is only called from tx-list if there are pending-txs * Move gas time estimate utilities into utility file * Move getTxParams to transaction selector file * Add feature flag for display of remaining transaction time in tx history list * Fix circular dependency by removing unused import of transactionSelector in selectors.js * Use correct feature flag property name transactionTime * Ensure that tx list component correctly responds to turning tx time feature on * Prevent precision errors in newBigSigDig * Code clean up for pending transaction times * Update transaction-time-remaining feature to count down seconds, countdown seconds and show '< 30' * Code clean up for transaction-time-remaining feature commit 6bd87e1f0937691e5eed6b92659c6b5e54164b9a Author: Whymarrh Whitby Date: Thu Oct 31 21:56:02 2019 -0230 Add web3 deprecation warning (#7334) * Add web3 deprecation warning * Update web3 deprecation article URL commit 30606327f0b773600f6c226815a4392c6868dd23 Author: Filip Š Date: Thu Oct 31 19:37:06 2019 +0100 Add support for ZeroNet (#7038) commit fe28e0d13427ec5847b2099a946b9388b2ac4cd2 Author: Mark Stacey Date: Thu Oct 31 13:27:22 2019 -0300 Cleanup beforeunload handler after transaction is resolved (#7333) * Cleanup beforeunload handler after transaction is resolved The notification window was updated to reject transactions upon close in #6340. A handler that rejects the transaction was added to `window.onbeforeunload`, and it was cleared in `actions.js` if it was confirmed or rejected. However, the `onbeforeunload` handler remained uncleared if the transaction was resolved in another window. This results in the transaction being rejected when the notification window closes, even long after the transaction is submitted and confirmed. This has been the cause of many problems with the Firefox e2e tests. Instead the `onbeforeunload` handler is cleared in the `componentWillUnmount` lifecycle function, alongside where it's set in the first place. This ensures that it's correctly unset regardless of how the transaction was resolved, and it better matches user expectations. * Fix indentation and remove redundant export The `run-all.sh` Bash script now uses consistent indentation, and is consistent about only re-exporting the Ganache arguments when they change. * Ensure transactions are completed before checking balance Various intermittent e2e test failures appear to be caused by React re-rendering the transaction list during the test, as the transaction goes from pending to confirmed. To avoid this race condition, the transaction is now explicitly looked for in the confirmed transaction list in each of the tests using this pattern. * Enable all e2e tests on Firefox The remaining tests that were disabled on Firefox now work correctly. Only a few timing adjustments were needed. * Update Firefox used in CI Firefox v70 is now used on CI instead of v68. This necessitated rewriting the function where the extension ID was obtained because the Firefox extensions page was redesigned. commit 19965985ad9bbc12853da7f3943b86e37aae0db2 Author: Mark Stacey Date: Wed Oct 30 22:31:04 2019 -0300 Update `ethereumjs-util` (#7332) `ethereumjs-util` is now pinned at `5.1.0`, instead of at the commit `ac5d0908536b447083ea422b435da27f26615de9`. That commit immediately preceded v5.1.0, so there are no functional differences. This was done mainly to remove our last GitHub/git dependency, and to make it more obvious which version we're using. commit 514be408f8eef60b27b79743d6525bf60a354ce4 Author: Frankie Date: Wed Oct 30 12:15:54 2019 -1000 I#6704 eth_getTransactionByHash will now check metamask's local history for pending transactions (#7327) * tests - create tests for pending middlewares * transactions - add r,s,v values to the txMeta to match the JSON rpc response * network - add new middleware for eth_getTransactionByHash that the checks pending tx's for a response value * transactions/pending - use getTransactionReceipt for checking if tx is in a block * meta - file rename commit 51e5220d5eaa08f5a39100cdf1e9c31f55a91969 Author: Frankie Date: Wed Oct 30 11:40:33 2019 -1000 I#3669 ignore known transactions on first broadcast and continue with normal flow (#7328) * transactions - ignore known tx errors * tests - test ignoreing Transaction Failed: known transaction message commit ab0eae1ed3f3890eb564a770dcae930b42a2691f Author: Mark Stacey Date: Wed Oct 30 16:18:57 2019 -0300 Rename ConfirmPageContainerHeader class (#7322) The class has been renamed to reflect that it is a header, to avoid having the same name as the `ConfirmPageContainer` component. Multiple components with the same name can lead to confusing error messages. commit 5d843db5338eb07038ddea1054ca8cc991cc1b04 Author: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue Oct 29 18:38:00 2019 -0700 Update eth-json-rpc-filters (#7325) * update eth-json-rpc-filters * update gaba commit bed58dd7d9aa359fe6e5dced7edaf5ff116d83fa Merge: fd0f6bbeb 9d793015d Author: Thomas Huang Date: Tue Oct 29 18:17:36 2019 -0700 Merge pull request #7326 from MetaMask/edit-contact Edit contact details fix. commit 9d793015d39b2483ac190b53ada93e1b43d136a8 Author: Thomas Date: Tue Oct 29 17:55:10 2019 -0700 Add static defaultProps commit d90b3feab809002629c9317dda1e221dad521a2d Author: Thomas Date: Tue Oct 29 16:42:56 2019 -0700 Add back placeholder addAlias for nickname commit cbd1d47559a9113a4a72b383447a570205c7a79b Author: Thomas Date: Tue Oct 29 16:21:18 2019 -0700 Allow removing of contact details to change details. commit df8b825a671b67733777b0a53420de506292d2be Author: Thomas Date: Tue Oct 29 16:14:18 2019 -0700 Adjust copy-to-clipboard svg width and height on edit contact screen commit fd0f6bbeb6484a7969dc67bb7cc9c4fc3edbab2c Merge: 8dfb0e815 9285a10be Author: Thomas Huang Date: Tue Oct 29 11:23:59 2019 -0700 Merge pull request #7324 from MetaMask/delete-contact Adds the chainId to remove accounts from state commit 9285a10be661573cd5298cdc21c794fdd25c8d0f Author: Thomas Date: Tue Oct 29 10:53:51 2019 -0700 Adds the chainId to remove accounts from state commit 8dfb0e8154d258d5c20ee4d7fd0c5b9e478478d6 Author: Mark Stacey Date: Tue Oct 29 13:14:41 2019 -0300 Add hostname and extensionId to site metadata (#7218) If the extension ID is set, an alternate title and subtitle are used for the Connect Request screen. The title is always `External Extension`, and the subtitle is `Extension ID: [id]` instead of the origin (which would just be `[extension-scheme]://[id]` anyway). The hostname for the site is used as a fallback in case it has no title. The artificial hostname set for internal connections has been renamed from 'MetaMask' to 'metamask' because URL objects automatically normalize hostnames to be all lower-case, and it was more convenient to use a URL object so that the parameter would be the same type as used for an untrusted connection. commit 71817795766693742f59572c0e4781baca301cc4 Author: ricky Date: Tue Oct 29 12:12:41 2019 -0400 Refactor `new-account.js` (#7312) * Start refactor * Use import syntax * Add create-account.component * Continue refactor * Add new line * Start using JSX and make tabs a bit more DRY * :wave: bye-bye hyperscript * These can be disabled when active * Start JSX in new account component * :wave: bye-bye hyperscript * Move newAccountNumber into container * Validate newAccountNumber prop commit 95618d2cdb27e6d89a2e6b51f04dc1932a7ed818 Author: Whymarrh Whitby Date: Tue Oct 29 00:36:38 2019 -0230 Update caniuse-lite dep (#7321) commit b89d96e61a13be0c125eab199cb19f1306fc25bb Author: Dan J Miller Date: Mon Oct 28 20:48:33 2019 -0230 Specify row height in grid template of transaction list items to fix status spacing issue (#7319) commit af888d0ab24f1ba5bdbbee807fd769ab37f47741 Author: ricky Date: Mon Oct 28 09:28:46 2019 -0400 Add "Retry" option for failed transactions (#7296) * Begin mocking out retry ui * Remove "Failed" * I guess this works? * Update corresponding test * wip * Ok, this appears to be working now * cleanup * Move this back to 3 * I don't think I need this * Rename showRetry to showSpeedUp * Address PR feedback * Remove notes * Rename shouldShowRetry -> shouldShowSpeedUp * oops commit c8878f46d47247918e25d19ab8bb7973dd59ef27 Author: Whymarrh Whitby Date: Fri Oct 25 11:39:56 2019 -0230 Remove unused design files (#7313) commit 478d6563f289b73e782bba6e854deab8fe6a923b Author: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu Oct 24 06:54:32 2019 -0700 Freeze Promise global on boot (#7309) * freeze background and UI Promise globals on boot * add new tests * remove tape commit 5f4cce13f00501b1a841e1a87aed68289b46c261 Author: Sirius Tsou Date: Thu Oct 24 19:49:48 2019 +0800 correct the zh-TW translation (#7306) commit 41ec11da0b83d20022a069a92e4ad43e0fcefe6e Merge: adb50d135 5f53e4b67 Author: Thomas Huang Date: Wed Oct 23 13:12:17 2019 -0700 Merge pull request #7310 from MetaMask/signedTypedData-test-adjustment Stub signTypedMessage commit 5f53e4b67e06c9c60bba97ad54de904552baf9d1 Author: Thomas Date: Wed Oct 23 12:43:22 2019 -0700 Remove async commit 94a4b4d7dbfabb5809815dd9658b9ed31ade6890 Author: Thomas Date: Wed Oct 23 12:09:27 2019 -0700 Stub signTypedMessage commit adb50d1357fa555db3a6b1f80ce5db531f3b166a Author: Mark Stacey Date: Wed Oct 23 14:00:16 2019 -0300 Fix flaky e2e tests (#7307) * Add a delay after connecting This addresses an intermittent test failure where the MetaMask Notification window cannot be found. It appears to be caused by the Send button being clicked too soon after connecting to a dapp, before the background has had a chance to process the approval. The premature send is ignored and the window never appears. This delay (2 seconds) should be sufficient time for the connection to be processed. A later 5-second delay was also reduced to 2 seconds. * Select onboarding buttons by button text The onboarding buttons were being selected using the classname, which was common to all onboarding buttons. This resulting in buttons being selected just before a page transition, leading to an error about the element reference being stale when a click was attempted. The CSS class selectors have been replaced by text selectors, which are more specific and shouldn't be at risk of resolving early. They're also easier to read. * Remove retypeSeedPhrase function This function was used to re-type the seed phrase in the event that a failure occurred when confirming the seed phrase. I'm not sure what failure this was meant to address exactly, but this contingency hasn't been needed for some time. We can tell that it hasn't been used because it wasn't updated for the incremental account security changes, so it couldn't have worked since then (it would have clicked the wrong button). commit 5c8048fcc1ab0fe0249e9cb61af97682899754ad Merge: ba25d5267 25076da9c Author: Dan Finlay <542863+danfinlay@users.noreply.github.com> Date: Wed Oct 23 09:43:57 2019 -0700 Merge pull request #7304 from MetaMask/routeTypedSignaturesToKeyrings Move signTypedData signing out to keyrings commit ba25d52670c38fd9379b70d21dc14af0a6d47b9d Author: Mark Stacey Date: Wed Oct 23 09:23:15 2019 -0300 Use `AdvancedGasInputs` in `AdvancedTabContent` (#7186) * Use `AdvancedGasInputs` in `AdvancedTabContent` The `AdvancedGasInputs` component was originally extracted from the `AdvancedTabContent` component, duplicating much of the rendering logic. They have since evolved separately, with bugs being fixed in one place but not the other. The inputs and outputs expected weren't exactly the same, as the `AdvancedGasInputs` component converts the input custom gas price and limit, and it converts them both in the setter methods as well. The `GasModalPageContainer` had to be adjusted to avoid converting these values multiple times. Both components dealt with input debouncing separately, both in less than ideal ways. `AdvancedTabContent` didn't debounce either field, but it did debounce the check for whether the gas limit field was below the minimum value. So if a less-than-minimum value was set, it would be propogated upwards and could be saved if the user clicked 'Save' quickly enough. After a second delay it would snap back to the minimum value. The `AdvancedGasInputs` component debounced both fields, but it would replace any gas limit below the minimum with the minimum value. This led to a problem where a brief pause during typing would reset the field to 21000. The `AdvancedGasInputs` approach was chosen, except that it was updated to no longer change the gas limit if it was below the minimum. Instead it displays an error. Parent components were checked to ensure they would detect the error case of the gas limit being set too low, and prevent the form submission in those cases. Of the three parents, one had already dealt with it correctly, one needed to convert the gas limit from hex first, and another needed the gas limit check added. Closes #6872 * Cleanup send components Empty README files have been removed, and a mistake in the index file for the send page has been corrected. The Gas Slider component class name was updated as well; it looks like it was originally created from `AdvancedTabContent`, so it still had that class name. commit 3acd1db0beb2cd9730311afe5d35977b87fc7d78 Merge: aecc85998 4d6ce8cee Author: Thomas Huang Date: Tue Oct 22 18:12:48 2019 -0700 Merge pull request #7222 from MetaMask/background-controller-tests Background / Controller tests. commit aecc85998152d59a267e02bed08991a92f56edeb Merge: 27d67558f 06dc3b260 Author: Thomas Huang Date: Tue Oct 22 17:51:55 2019 -0700 Merge pull request #7303 from MetaMask/master Sync develop with master commit 4d6ce8ceebfd3c2adf1fad45da97406e4844e948 Author: Thomas Date: Tue Oct 22 17:23:18 2019 -0700 Use async/await commit 25076da9cbab44b71465984fcaec64d3cced83ba Author: Dan Finlay <542863+danfinlay@users.noreply.github.com> Date: Wed Oct 23 08:59:20 2019 +0900 Remove unused dependency commit 3eee9a2458c71087bd68682239754869f97e785a Author: Dan Finlay <542863+danfinlay@users.noreply.github.com> Date: Wed Oct 23 08:47:15 2019 +0900 Linted commit d26a8e7f82938e59b7d6b1668d3b480246e3da49 Author: Dan Finlay Date: Tue Oct 22 14:53:25 2019 -0700 Allow non strigified typed data params commit a8aca0a3265bdbeb4d92dab872cd96f4b4f0ec4e Merge: 4b4bee77c 27d67558f Author: Dan Finlay Date: Tue Oct 22 14:51:21 2019 -0700 Merge remote-tracking branch 'upstream/develop' into routeTypedSignaturesToKeyrings commit 27d67558fcb929af5b708055ed2f1144fd15f0da Author: Mark Stacey Date: Tue Oct 22 16:29:21 2019 -0300 Update `superagent-proxy` to address security advisory (#7301) Security advisory: https://www.npmjs.com/advisories/1184 This advisory was already addressed in #7289 but subsequent releases have made this simpler resolution possible. commit 4b4bee77c708b18531179f1574f1058455c13ff8 Author: Dan Finlay Date: Tue Oct 22 11:33:49 2019 -0700 Move signTypedData signing out to keyrings This simplifies the logic of signing and improves security: - Private keys are never moved to the base controller. - Hardware wallets are abstracted in the same way as local keys. This also paves the way for allowing even more modular accounts, provided by plugins: https://github.com/MetaMask/metamask-plugin-beta/pull/63 Fixes #7075. commit 06dc3b260aa93150b63bd4b102671c03d68e6f3d Merge: 32f20587a 6bdf6e07d Author: Dan Finlay <542863+danfinlay@users.noreply.github.com> Date: Tue Oct 22 10:08:46 2019 -0700 Merge pull request #7299 from MetaMask/Version-v7.3.1 Version v7.3.1 RC commit 55bc9936c67832ab86c6558d832ee6598b1861a5 Author: Dan J Miller Date: Mon Oct 21 21:29:02 2019 -0230 Turn off full screen vs popup a/b test (#7298) commit 6bdf6e07dbd152be6661d58ba8252ce686b4ca4e Author: MetaMask Bot Date: Mon Oct 21 23:10:57 2019 +0000 Version v7.3.1 commit 225b6a98e0ea34954daaaed1640a677d0b8ba480 Author: Dan Miller Date: Mon Oct 21 20:40:03 2019 -0230 Update changelog for v7.3.1 commit 51cc2faf56aae41d146046e2aa46220ea20b528a Author: Dan Miller Date: Mon Oct 21 20:21:25 2019 -0230 Turn off full screen vs popup a/b test commit 2a81083f14257bebcd9c9d5cb76910dd29e42d39 Author: tmashuang Date: Thu Sep 26 11:52:51 2019 -0700 MM controller additions, balance controller, typedMessageManager, and addtional actions tests commit 4f9ea17185a72464e5f0cbafcc1f12a679f861c3 Merge: 994a8a316 32f20587a Author: Dan Finlay <542863+danfinlay@users.noreply.github.com> Date: Tue Oct 22 03:08:55 2019 +0900 Merge pull request #7295 from MetaMask/master Sync develop with master commit 32f20587ace3aa7bb47aeeee78de21da65053dff Merge: bafcbc90a ebc876454 Author: Dan Finlay <542863+danfinlay@users.noreply.github.com> Date: Tue Oct 22 01:57:53 2019 +0900 Merge pull request #7223 from MetaMask/Version-v7.3.0 Version v7.3.0 RC commit 994a8a31678af49cf9014dcecf0cb77affada909 Author: Dan J Miller Date: Mon Oct 21 14:23:26 2019 -0230 Add metrics events for clicking and saving tx speed ups (#7275) commit ebc876454e1ec6ae5b86c2aa84cedcb65d4c5a63 Author: Mark Stacey Date: Fri Sep 27 10:53:09 2019 -0600 Update v7.3.0 changelog commit a5035d49fc4633ec09a48ffc7e2308dd370a95b5 Author: MetaMask Bot Date: Fri Sep 27 04:32:45 2019 +0000 Version v7.3.0 commit 4d4126d4708da23409438853fe36413e351a442a Author: Mark Stacey Date: Mon Oct 21 09:16:21 2019 -0300 Update `https-proxy-agent` as per security advisory (#7289) Security advisory: https://www.npmjs.com/advisories/1184 The package `pac-proxy-agent` (which we use via `pubnub`) hasn't released an update yet, so we're forced to use a resolution for the time being. The updated version appears to be compatible. commit 4ad42d8374a83fda23cff37dddb70de26c89e8df Author: Mark Stacey Date: Mon Oct 21 09:16:21 2019 -0300 Update `https-proxy-agent` as per security advisory (#7289) Security advisory: https://www.npmjs.com/advisories/1184 The package `pac-proxy-agent` (which we use via `pubnub`) hasn't released an update yet, so we're forced to use a resolution for the time being. The updated version appears to be compatible. commit 06536b1d0f4778297b405c03e2b53909cf8b0e13 Author: Whymarrh Whitby Date: Fri Oct 18 13:35:32 2019 -0230 Fix phishing detect script (#7287) commit a646bfb5069a77023cfc260dfa79921f9dbfa29d Author: Thomas Huang Date: Thu Oct 17 09:03:14 2019 -0700 Lessen the length of ENS validation to 3 (#7285) commit 9d9f3685bb58cd7b134345318aed10511df94920 Author: Thomas Huang Date: Thu Oct 17 08:25:37 2019 -0700 Prevent Logout Timer that's longer than a week. (#7253) commit 3d1f214cb094cc835142ece4178278f8d92c9b64 Author: Whymarrh Whitby Date: Wed Oct 16 22:01:19 2019 -0230 Remove trailing commas from JSON files (#7284) commit f1c774d8f319ac6aac9952dccc01792f4d4a0b27 Author: Dan J Miller Date: Wed Oct 9 12:11:18 2019 -0230 Handle empty fetch response (#7111) commit ee913edf9f680d9b1e460d574ae6f7348aab1020 Author: Mark O'Sullivan Date: Wed Oct 9 13:30:48 2019 +0100 fix issue of xyz ens not resolving (#7266) commit 4a7292ad5461435a7fb369e5c9bbdab6269d0c22 Merge: 1a0b0ce7c 08a7308b0 Author: Mark Stacey Date: Tue Oct 8 21:15:37 2019 -0300 Merge pull request #7269 from MetaMask/master-parity Master => develop commit 08a7308b049d53c56f227d1edca59cf515bc4173 Merge: 1a0b0ce7c bafcbc90a Author: Mark Stacey Date: Tue Oct 8 20:26:21 2019 -0300 Merge branch 'master' into develop * master: (34 commits) Update changelog for v7.2.3 Fix e2e tests and gas default (#7267) Do not transate on seed phrases test:integration - fix renamed test data file lint fix test:e2e - fix bail condition test:e2e - fix responsie argument test:e2e - refactor missed spec file test:e2e - only overwrite window.fetch once per session test:e2e - rework fetch-mocks test:e2e - add extra delay before closing popups test:e2e - factor out prepareExtensionForTesting test - e2e - dedupe fetchMocking + compose script as fn Ensure correct tx category when sending to contracts without tx data (#7252) Version v7.2.3 Add v7.2.2 to changelog Update minimum Firefox verison to 56.0 (#7213) Version v7.2.2 Update changelog for v7.2.1, v7.2.0, and v7.1.1 Add `appName` message to each locale ... commit bafcbc90afe5ce8df846781a928e264bdb51b118 Merge: f1111fe40 a8bd527d7 Author: Dan Finlay <542863+danfinlay@users.noreply.github.com> Date: Wed Oct 9 07:43:45 2019 +0900 Merge pull request #7251 from MetaMask/Version-v7.2.3 Version v7.2.3 RC commit a8bd527d7287feb3e1d456edeba60b6968d6ab7c Author: Dan Miller Date: Fri Oct 4 16:29:12 2019 -0230 Update changelog for v7.2.3 commit 4ed452e6ede368a364b704fa0362a5a5883fb28b Author: Dan J Miller Date: Tue Oct 8 14:44:20 2019 -0230 Fix e2e tests and gas default (#7267) * Add extra delay after second send3eth.click() in the 'adds multiple transactions' test * Remove use of ARBITRARY_HIGH_BLOCK_GAS_LIMIT as fallback commit e7bf250eabfd5ce95fdda02d66c53406d98762f1 Author: Thomas Date: Mon Oct 7 10:45:12 2019 -0700 Do not transate on seed phrases commit 4a7c7dcda6bcd45da86a75f44eaf8e206cf5e5b6 Author: kumavis Date: Wed Sep 11 00:13:21 2019 +0800 test:integration - fix renamed test data file commit e6b4f88d17bdbd01737bb365c4f35905c58e078f Author: kumavis Date: Wed Sep 11 00:11:49 2019 +0800 lint fix commit e283ff181e4d39dc0b47c766863a6dc7ff2919f7 Author: kumavis Date: Tue Sep 10 23:45:43 2019 +0800 test:e2e - fix bail condition commit cd62cc190160328bc9259379f3c817aafd247402 Author: kumavis Date: Tue Sep 10 23:43:59 2019 +0800 test:e2e - fix responsie argument commit 7e3f1263f21147cae75c1fb69ee36a6e5b7a41c9 Author: kumavis Date: Tue Sep 10 23:43:41 2019 +0800 test:e2e - refactor missed spec file commit 133ba76785f99961b244c9882df2d2cfa3bc2d55 Author: kumavis Date: Tue Sep 10 23:10:24 2019 +0800 test:e2e - only overwrite window.fetch once per session commit 11e1c3b95d829dd13a4c4c421047b6c50dde3058 Author: kumavis Date: Tue Sep 10 23:04:03 2019 +0800 test:e2e - rework fetch-mocks commit 56dae017a98e8b8859ec89c0cb702240a86cb2e5 Author: kumavis Date: Tue Sep 10 19:53:21 2019 +0800 test:e2e - add extra delay before closing popups commit d43a78432fe650ccfd3082bfbef02ecaf870551b Author: kumavis Date: Sat Sep 7 15:13:58 2019 +0800 test:e2e - factor out prepareExtensionForTesting commit 54491974ec140f2344ffdee6d46b4cfbcc2e1c56 Author: kumavis Date: Sat Sep 7 14:44:22 2019 +0800 test - e2e - dedupe fetchMocking + compose script as fn commit b884cd573cd417e17f7a522cbc249759f1e63bc4 Author: Dan J Miller Date: Mon Oct 7 16:59:37 2019 -0230 Ensure correct tx category when sending to contracts without tx data (#7252) * Ensure correct transaction category when sending to contracts but there is no txParams data * Update gas when pasting address in send * Gracefully fall back is send.util/estimateGas when blockGasLimit from background is falsy * Remove network request frontend fallback for blockGasLimit * Add some needed slow downs to e2e tests commit 1a0b0ce7c792fe9c4a87090b9b34a082425b17e5 Author: Dan J Miller Date: Tue Oct 8 14:44:20 2019 -0230 Fix e2e tests and gas default (#7267) * Add extra delay after second send3eth.click() in the 'adds multiple transactions' test * Remove use of ARBITRARY_HIGH_BLOCK_GAS_LIMIT as fallback commit 38df64783d8b96cd1cb0f496d2573e7a37b0aa66 Author: Dan J Miller Date: Mon Oct 7 16:59:37 2019 -0230 Ensure correct tx category when sending to contracts without tx data (#7252) * Ensure correct transaction category when sending to contracts but there is no txParams data * Update gas when pasting address in send * Gracefully fall back is send.util/estimateGas when blockGasLimit from background is falsy * Remove network request frontend fallback for blockGasLimit * Add some needed slow downs to e2e tests commit f94c3b96ed1ac5d0988569423b5036f065ce178a Author: MetaMask Bot Date: Fri Oct 4 18:54:03 2019 +0000 Version v7.2.3 commit d7a4dfebeb0625377d5cfe094c28ce7d4941d053 Merge: 3383eabc9 aa95e5a4c Author: Thomas Huang Date: Mon Oct 7 11:08:22 2019 -0700 Merge pull request #7260 from MetaMask/notranslate-seed-phrases Do not transate on seed phrases commit 3383eabc9d80b3f60162773a56342649ff0ac301 Author: ryanml Date: Mon Oct 7 10:54:44 2019 -0700 Use translated string for state log (#7255) commit aa95e5a4c82a3f8534e2fd8b5ab59243f26b1edc Author: Thomas Date: Mon Oct 7 10:45:12 2019 -0700 Do not transate on seed phrases commit 8b5ac9340bfe76487ba45df432f554fd6e6db843 Author: Mark Stacey Date: Wed Oct 2 17:42:52 2019 -0300 Fix custom nonce placeholder type (#7243) The placeholder for the custom nonce needed to be converted into a string. The placeholder is omitted if `nextNonce` isn't set, as may be the case for the initial render. commit 93b473d46d5b00e2b139cf5a8c0c8556782efa25 Merge: 9541d1e28 1369d0280 Author: Thomas Huang Date: Wed Oct 2 13:35:17 2019 -0700 Merge pull request #7244 from MetaMask/mark-3Box-sync-as-experimental Mark 3Box sync as experimental commit 1369d0280dc48302f0a5a438ae5967ba26897fa3 Author: Mark Stacey Date: Wed Oct 2 17:06:46 2019 -0300 Mark 3Box sync as experimental The initial release of the 3Box sync will be marked as experimental. This is to allow us time to test the 3Box sync and reduce the load on 3Box's infrastructure. commit 9541d1e281b0ae02d4f41d8bd6cb31d67f27fdc5 Author: Mark Stacey Date: Wed Oct 2 16:12:20 2019 -0300 Don't wait for 3Box initialization during login (#7242) The 3Box initialization is triggered by login, but it no longer blocks the login from finishing. The 3Box initialization is designed to run in the background, so there's no reason to block on it. commit e6e8897434419a5d694161fee9aab4b382452089 Author: Dan J Miller Date: Wed Oct 2 15:42:04 2019 -0230 Custom nonce fixes (#7240) * Allow default nextNonce to be the custom nonce in cases where highest locally pending is higher than nextNonce * Reset custom nonce in cases of transaction submission failures * Make the recommended nonce in the custom nonce field the true 'nextNonce' * Revert automatic setting of custom nonce to nextNonce * Make the nextNonce the default placeholder value * Fix getNextNonce * Remove unused nonceFieldPlaceholder message * Fix nits in getPendingNonce and getNextNonce * Properly handle errors in getNextNonce * Improve placeholder and value defaults in custom nonce field * Remove custom error message from getNextNonce commit 45a8fdebf72a9b2fd6b767412c539a3faafd6b1d Author: Thomas Huang Date: Wed Oct 2 09:15:44 2019 -0700 Update ETH logo, update deposit Ether logo height and width (#7239) commit daf4fe439c054ea897a5ebc10eaccf107305dda7 Author: Dan J Miller Date: Tue Oct 1 10:33:13 2019 -0230 Ensure 3box validation uses the correct address (#7235) commit 8d97bdc5c97a466d75532b97a064223ed2eeafe2 Author: Mark Stacey Date: Tue Oct 1 10:01:57 2019 -0300 Fix error handling when checking for 3Box backup (#7232) The 3Box SDK throws an HTTP 404 error when attempting to get the config for an account that doesn't yet exist in 3Box. The regex we were using to differentiate this error from others was broken. This ended up preventing the user from logging in if they had 3Box enabled but hadn't yet synced. The regex has been corrected to catch this case, while allowing other errors to propogate upward. Other 3Box errors will now be caught and reported rather than interrupting login completely. At some point in the future, we should expose these errors to the user in some way, and allow them to retry in case 3Box was just temporarily offline. commit b69982da847aa4f92fd12dab7cef5a481d980d6b Author: Thomas Huang Date: Mon Sep 30 15:17:45 2019 -0700 Remove logging of action.value of set_next_nonce (#7233) commit 67ab13b74b595df382b57390d888c81051c877ff Author: Thomas Huang Date: Fri Sep 27 10:35:38 2019 -0600 Master parity with developer/release (#7225) * Version v7.2.2 * Add v7.2.2 to changelog commit 5f254f7325b2b3a74916643c31383196ba012c0b Author: ricky Date: Fri Sep 27 00:30:36 2019 -0400 Add advanced setting to enable editing nonce on confirmation screens (#7089) * Add UseNonce toggle * Get the toggle actually working and dispatching * Display nonce field on confirmation page * Remove console.log * Add placeholder * Set customNonceValue * Add nonce key/value to txParams * remove customNonceValue from component state * Use translation file and existing CSS class * Use existing TextField component * Remove console.log * Fix lint nits * Okay this sorta works? * Move nonce toggle to advanced tab * Set min to 0 * Wrap value in Number() * Add customNonceMap * Update custom nonce translation * Update styles * Reset CustomNonce * Fix lint * Get tests passing * Add customNonceValue to defaults * Fix test * Fix comments * Update tests * Use camel case * Ensure custom nonce can only be whole number * Correct font size for custom nonce input * UX improvements for custom nonce feature * Fix advanced-tab-component tests for custom nonce changes * Update title of nonce toggle in settings * Remove unused locale message * Cast custom nonce to string in confirm-transaction-base.component * Handle string conversion and invalid values for custom nonces in handler * Don't call getNonceLock in tx controller if there is a custom nonce * Set nonce details for cases where nonce is customized * Fix incorrectly use value for deciding whether to getnoncelock in approveTransaction * Default nonceLock to empty object in approveTransaction * Reapply use on nonceLock in cases where customNonceValue in approveTransaction. * Show warning message if custom nonce is higher than MetaMask's next nonce * Fix e2e test failure caused by custom nonce and 3box toggle conflict * Update nonce warning message to include the suggested nonce * Handle nextNonce comparison and update logic in lifecycle * Default nonce field to suggested nonce * Clear custom nonce on reject or confirm * Fix bug where nonces are not shown in tx list on self sent transactions * Ensure custom nonce is reset after tx is created in background * Convert customNonceValue to number in approve tranasction controller * Lint fix * Call getNextNonce after updating custom nonce commit 970e90ea702a6819340f71b965a414b616014fde Author: Dan J Miller Date: Thu Sep 26 03:24:52 2019 -0400 Add migration on 3box imports and remove feature flag (#7209) * Delete unused code * Run threebox imports through migrations * Remove 3box feature flag * Remove unnecessary use of 'type' in threebox._updatePlugin * Fix threebox controller getLastUpdated * Turn off threebox by default * Rename restoredFromThreeBox to showRestorePrompt * Remove accientally added method from threebox controller * Restore from threebox on import from unlock screen * Throw on non 404 errors from Box.getconfig in new3Box commit f1111fe401262f2ec5b9d4000b7a8cf0fe06f5f9 Merge: a6131d7a4 1375a86ea Author: Dan Finlay <542863+danfinlay@users.noreply.github.com> Date: Wed Sep 25 08:39:26 2019 -0700 Merge pull request #7214 from MetaMask/Version-v7.2.2 Version v7.2.2 RC commit e1efb4d7acbacf57d94888087e6f03e48bad64c7 Author: kumavis Date: Wed Sep 25 20:01:10 2019 +0800 ci - install deps - limit install scripts to whitelist (#7208) * ci - install deps - limit install scripts to those needed for build * Update .circleci/scripts/deps-install.sh Co-Authored-By: Mark Stacey * ci - install deps - expand install scripts needed for tests * ci - install deps - expand install scripts needed for integration tests * ci - install deps - fix node-sass script ref * github - set codeowners for scripts/deps-install * development - add utility to show deps with install scripts * lint fix * deps - move read-installed to devDeps commit 1bd22b58c0db1dcaa0dcd6865a094e5114f353af Author: Dan J Miller Date: Tue Sep 24 17:08:38 2019 -0400 Add a/b test for full screen transaction confirmations (#7162) * Adds ab test controller with a fullScreenVsPopup test * Add migration for fullScreenVsPopup state * Move abtest state under an 'abtests' object. * MetaMask shows fullScreen group of a/b test unapproved txs in a full browser tab * Ensure cancel metrics event in confirm-transaction-base.component.js is sent in all cases * Switch to existing tab for unapproved tx if it exists when opening in full screen * Send metrics event for entering a/b test from confirm screen * Fix lint, unit and integration tests related to a/b test code * Remove unnecessary tabs.query call in triggerUiInNewTab commit 1375a86eaf007ab288348fff6aefffa94932ad79 Author: Mark Stacey Date: Tue Sep 24 09:57:51 2019 -0600 Add v7.2.2 to changelog commit 288553f300fcab073e0b3201b813db482a050ceb Author: Mark Stacey Date: Tue Sep 24 12:49:24 2019 -0300 Update minimum Firefox verison to 56.0 (#7213) The previous minimum version of 56.2 resulted in the build failing validation when it was uploaded to the Firefox web store, because that version doesn't exist. It was set to that version because a Firefox fork uses it. Instead the minimum version has been reduced so that we pass validation. Unfortunately this will mean that a single incompatible version of Firefox Mobile will allow the extension to be installed (in theory), but there was no other way to avoid cutting off support to WaterFox (the Firefox fork). The warning about this from the addons linter can be ignored for now. commit 2fc6c50de2a25a24d31dacee9f5e080e9d44723f Author: MetaMask Bot Date: Tue Sep 24 15:50:32 2019 +0000 Version v7.2.2 commit 0ad6e2ada8784a1b13e8a89ff0b91eaf16d17d13 Author: Mark Stacey Date: Tue Sep 24 12:49:24 2019 -0300 Update minimum Firefox verison to 56.0 (#7213) The previous minimum version of 56.2 resulted in the build failing validation when it was uploaded to the Firefox web store, because that version doesn't exist. It was set to that version because a Firefox fork uses it. Instead the minimum version has been reduced so that we pass validation. Unfortunately this will mean that a single incompatible version of Firefox Mobile will allow the extension to be installed (in theory), but there was no other way to avoid cutting off support to WaterFox (the Firefox fork). The warning about this from the addons linter can be ignored for now. commit 4d71f3f8547d4cea98454b6f36caebc602b82049 Author: kumavis Date: Mon Sep 23 17:54:41 2019 +0800 mesh-testing - submit infura rpc requests to mesh-testing container (#7031) commit f5b2977764f13df0f0b478718383415156c67a7b Author: kumavis Date: Mon Sep 23 11:45:20 2019 +0800 obs-store/local-store should upgrade webextension error to real error (#7207) * obs-store/local-store should upgrade webextension error to real error * lint fix * local-store - allow lastError through unchanged if error-like commit 8a1ddbbcd81864cc091756480f2af34f57782f6e Author: kumavis Date: Mon Sep 23 11:14:18 2019 +0800 sesify-viz - bump dep for visualization enhancement (#7175) * deps - bump sesify-viz for visualization enhancement * deps - bump sesify-viz for visualization enhancement * deps - bump sesify-viz for visualization enhancement * deps - sesify-viz - use carrot version range Co-Authored-By: Mark Stacey * deps - update yarn lockfile commit e86cebde3baf251c8e76a4854d7bcbd99920a293 Author: Jenny Pollack Date: Sat Sep 21 10:36:06 2019 -0700 address book entries by chainId (#7205) commit e44a2283b5ff147c982e8e89b7aa5e3d618d955c Author: Mark Stacey Date: Sat Sep 21 13:32:17 2019 -0300 Optimize images only during production build (#7194) * Optimize images only during production build Image optimization is fairly slow (over a minute), and isn't necessary for test or development builds. It is now only run as part of the `build` gulp task, which is used during `gulp dist`. * Remove unused gulp tasks There were two high-level tasks and one style formatting task that were not used by any npm scripts, so were probably unused generally. The `dev` task was a duplcate of `dev:extension`. The `build:extension` task was useful for just building the extension without performing other steps required by the final production bundle, but it was broken. It didn't correctly build the ui-libs and bg-libs required. Instead of fixing it, it has been removed until the handling of those separate library bundles is simplified. The style formatting task seems like it could be useful, but I'm unsure about keeping it around as opt-in, as in practice it'll just end up being ignored. Moreover the library authors themselves are recommending switching to `prettier`, so I think we're better off removing it for now, then considering using `prettier` if we want to introduce something like this again. The stylelint dependency was added because it's a peer dependency of gulp-stylelint that should have already been listed among our dependencies. It hadn't caused a problem before because it happened to be a transitive dependency of gulp-stylefmt, which is no longer needed and has been removed. commit b0ec610908f41157b31f8e6c76ae35c87c43509d Author: Mark Stacey Date: Sat Sep 21 13:32:07 2019 -0300 Use common test build during CI (#7196) * Use common test build during CI Previously both e2e test jobs were running `test:build`. Now there is a separate job that runs `test:build` that runs before each e2e test job, so that `test:build` is only run once instead of twice. * Move test builds to separate directory This prevents the test build from conflicting with the production build in later jobs. commit f100d753cf6e9fcffe10e5ba0f8144275d6a93c8 Author: Mark Stacey Date: Sat Sep 21 13:31:45 2019 -0300 Report missing `en` locale messages to Sentry (#7197) Any missing messages in the `en` locale are now reported to Sentry as errors. They are printed to the console as an error upon the first encounter as well. If a missing message is found during e2e testing, the error is thrown. This will likely break the e2e test even if it isn't looking for console errors, as the UI with the missing message will fail to render. The `tOrDefault` method was updated to no longer attempt looking for messages with a key that is a falsey value (e.g. `undefined`). There are a few places where they key is determined dynamically, where it's expected during the normal flow for it to be `undefined` sometimes. In these cases we don't want the error to be thrown. commit 9f21317a30ecf9fa9db1c14face983c6e23ef1e7 Author: Mark Stacey Date: Sat Sep 21 13:31:33 2019 -0300 Verify locales on CI (#7199) * Add '--quiet' flag to verify locales script The `--quiet` flag reduces the console output to just the essential information for running in a CI environment. For each locale, it will print the number of unused messages (if any). * Add `verify-locales` script to lint CI job The locales are now verified as part of the lint CI job. Any unused messages detected will result in the job failing. commit a869fd639b209a047bf94bf9ea8ddb2a41e8d98e Author: Marius van der Wijden Date: Fri Sep 20 14:52:34 2019 +0300 updated ganache and addons-linter (#7204) commit a6131d7a465e9bafc90005fb397cb5bc42f480cc Merge: 27834681a 323a0dc73 Author: Dan Finlay <542863+danfinlay@users.noreply.github.com> Date: Tue Sep 17 14:56:43 2019 -0700 Merge pull request #7179 from MetaMask/Version-v7.2.1 Version v7.2.1 RC commit 323a0dc73cfb0e6e999c5f82cce2e7842ced6105 Author: Mark Stacey Date: Tue Sep 17 17:51:31 2019 -0300 Update changelog for v7.2.1, v7.2.0, and v7.1.1 A new entry has been added for v7.2.1, and the Brave locales have been moved from v7.1.1 to v7.2.0. That feature was mistakenly included under the v7.1.1 heading - it was in fact released in v7.2.0 commit 30e0a85f1def69477d992757b52ba192dd19dd02 Author: Mark Stacey Date: Tue Sep 17 17:14:47 2019 -0300 Add `appName` message to each locale The Chrome Web store was rejecting the build because `appName` was missing from some locales. commit 9ca0c6fcddec37ca47b6956700b05b3526315f1a Author: MetaMask Bot Date: Tue Sep 17 19:53:30 2019 +0000 Version v7.2.1 commit 27834681a72524f9f5620661ef7fdde10a5220ae Merge: ab59eafeb eb478078a Author: Dan Finlay <542863+danfinlay@users.noreply.github.com> Date: Tue Sep 17 10:07:18 2019 -0700 Merge pull request #7138 from MetaMask/Version-v7.2.0 Version v7.2.0 commit eb478078a79469e4bb6075e4d41888a967e6be90 Author: Mark Stacey Date: Fri Sep 13 11:54:53 2019 -0300 Update changelog with additional bug fixes commit 009bf802f76fcbfc5a01aeda6273157884a1b59d Author: Dan J Miller Date: Mon Sep 16 17:05:21 2019 -0230 Fix recipient field of approve screen (#7171) commit 94318d8b28636b9652898559363a021497a73adc Author: Mark Stacey Date: Fri Sep 13 11:32:55 2019 -0300 Replace `undefined` selectedAddress with `null` (#7161) * Replace `undefined` selectedAddress with `null` The `runtime.Port.postMessage` API will drop keys with a value of `undefined` on Chrome, but not on Firefox. This was a problem for the `publicConfig` stream, which passed the key `selectedAddress` with the value of `undefined` to communicate to dapps that the user had logged out. Instead a `null` is now passed for `selectedAddress` upon logout, which is correctly sent by the `runtime.Port.postMessage` API on both Chrome and Firefox. closes #7101 closes #7109 * Update `metamask-inpage-provider` to v3.0.0 The v3.0.0 update includes a change to the `accountsChanged` event. The event will now emit an empty array instead of an array with `undefined` or `null`. The previous behavior was to emit `[undefined]`. The previous commit would have changed that to `[null]` anyway, so we figured if we're going to make a public-facing change to the event anyway we should change it to be correct. `[undefined]` was never intended, and it technically violates EIP-1193, which states that the `accountsChanged` event should emit an array of strings. commit c80deaa1b8ab0c5e4ea7e2a8c2aad827c5efb7d9 Author: Mark Stacey Date: Thu Sep 12 17:07:27 2019 -0300 Add polyfill for AbortController (#7157) The AbortController is used in both the background and the UI. Support for AbortController was added to Chrome in version 66, which is above our minimum supported version. I did consider increasing the minimum Chrome version to 66, but we have a decent number of users still on Chrome 65 unfortunately. commit da4119339ba95ad68d4f016b1442df675d13a6eb Author: Mark Stacey Date: Thu Sep 12 16:55:20 2019 -0300 Set minimum Firefox version to v56.2 to support Waterfox (#7156) The minimum compatible version of Firefox has been lowered from `60.0` to `56.2`. It was originally set to `60.0` because that is Firefox ESR, which currently the minimum Firefox version with security updates. However Waterfox is based upon Firefox `56.0`, and has backported security fixes from the ESR release. This change in minimum version requires no additional transpiling, and doesn't affect any browser APIs we use. It does introduce one additional warning in the `addon-linter` about Firefox for Android `56` lacking support for browser action popups. However there is no version `56.2` of Firefox for Android, so the minimum version remains `57` in practice (which does support browser action popups). commit da1d506de10f58a795b39ce0270dd66833d3e3f2 Merge: 41305b661 ab59eafeb Author: Mark Stacey Date: Wed Sep 11 12:08:17 2019 -0300 Merge remote-tracking branch 'origin/master' into Version-v7.2.0 * origin/master: Allow dismissing privacy mode from popup Add changelog Version v7.1.1 commit 41305b6616343efb2b211252f74ae057472d7dbe Author: Mark Stacey Date: Wed Sep 11 11:44:36 2019 -0300 Publish GitHub release from master branch (#7136) * Publish GitHub release from master branch This ensures that changes made on `develop` since branching for the release are not included. It also ensures that the final release sourcemaps line-up correctly (they were always build on master)`. * Consolidate publish jobs The jobs `job-publish-release` and `create_github_release` both handle different parts of publishing a release. They have been consolidated into a single `job-publish-release` job. * Update release script to expect a merge commit The release script was originally written to be run on `develop`, so it expected the current commit to be a result of `Squash & Merge`. Now that it's run on `master`, it will generally be run against a merge commit. The version is now extracted from the commit message using a regular expression that should work on all version of Bash v3+, and should be tolerant of both merge commits and `Squash & Merge` commits. * Target `master` with release PR `master` is now targeted by the release PR instead of `develop`, as the release has to be created from the master branch. The update to `develop` is handled after the release by a PR from `master` to `develop`, which is created automatically after the release. commit 8524d17d2d803842be18fccb32f402345739bd3b Author: Mark Stacey Date: Tue Sep 10 09:44:24 2019 -0300 Update the changelog for v7.1.1 (#7145) Due to a mistake in the release scripts, there were a few changes accidentally included in the v7.1.1 release. This updates the changelog to include those changes. commit d10d345e5c0404bffd5979353ffef291cb975a3e Author: kumavis Date: Tue Sep 10 16:48:09 2019 +0800 build - replace gulp-uglify-es with gulp-terser-js commit c1994ccf36676907d5b38434440e50fde94095e2 Author: MetaMask Bot Date: Tue Sep 10 03:07:14 2019 +0000 Version v7.2.0 commit f9a10c20918d28f204b6837d4ae5de6a810dd015 Author: Dan Miller Date: Tue Sep 10 00:35:54 2019 -0230 Update changelog with 7.2.0 changes commit ab59eafeb175e8da05475093ead4062c00764cc5 Merge: 3f1229ce8 d2799f40b Author: Dan Finlay <542863+danfinlay@users.noreply.github.com> Date: Tue Sep 3 12:33:32 2019 -0700 Merge pull request #7098 from MetaMask/Version-v7.1.1 Version v7.1.1 commit d2799f40b65e2fb55379488e38050d7fae6f8981 Author: Mark Stacey Date: Tue Sep 3 14:27:20 2019 -0300 Allow dismissing privacy mode from popup commit 76ef33257ecdd2ae564623b1d25d8e534369a63e Author: Mark Stacey Date: Tue Aug 27 00:39:55 2019 -0300 Add changelog commit 4b905ffe39bf1918ae64e00377b77f3b1a0b7559 Author: MetaMask Bot Date: Tue Aug 27 03:37:09 2019 +0000 Version v7.1.1 commit 3f1229ce804aad3c63c413f4987b6aa419875ffa Merge: b3ad16306 e5231eed3 Author: Dan Finlay <542863+danfinlay@users.noreply.github.com> Date: Mon Aug 26 09:16:57 2019 -0700 Merge pull request #7066 from MetaMask/Version-v7.1.0 Version 7.1.0 commit b3ad16306c15f8897757216cb8aaac0b308dd089 Merge: db08881d4 24e397088 Author: Dan Finlay <542863+danfinlay@users.noreply.github.com> Date: Thu Aug 8 08:58:52 2019 -0700 Merge pull request #6977 from MetaMask/develop Master v7.0.1 commit db08881d4527e8a037f401ef22b849e52152864f Merge: 4139019d0 86ad9564a Author: Dan Finlay <542863+danfinlay@users.noreply.github.com> Date: Tue Aug 6 14:53:50 2019 -0700 Merge pull request #6969 from MetaMask/develop Master Version Bump --- .circleci/config.yml | 70 +- .circleci/scripts/deps-install.sh | 25 + .circleci/scripts/firefox-install | 2 +- .eslintrc | 12 +- .github/CODEOWNERS | 2 +- .nvmrc | 2 +- .stylelintrc | 2 +- CHANGELOG.md | 93 + app/_locales/am/messages.json | 21 +- app/_locales/ar/messages.json | 21 +- app/_locales/bg/messages.json | 25 +- app/_locales/bn/messages.json | 21 +- app/_locales/ca/messages.json | 25 +- app/_locales/cs/messages.json | 11 +- app/_locales/da/messages.json | 25 +- app/_locales/de/messages.json | 26 +- app/_locales/el/messages.json | 25 +- app/_locales/en/messages.json | 169 +- app/_locales/es/messages.json | 22 +- app/_locales/es_419/messages.json | 25 +- app/_locales/et/messages.json | 25 +- app/_locales/fa/messages.json | 21 +- app/_locales/fi/messages.json | 25 +- app/_locales/fil/messages.json | 23 +- app/_locales/fr/messages.json | 26 +- app/_locales/gu/messages.json | 3 - app/_locales/he/messages.json | 21 +- app/_locales/hi/messages.json | 21 +- app/_locales/hn/messages.json | 11 +- app/_locales/hr/messages.json | 25 +- app/_locales/ht/messages.json | 11 +- app/_locales/hu/messages.json | 25 +- app/_locales/id/messages.json | 25 +- app/_locales/it/messages.json | 44 +- app/_locales/ja/messages.json | 15 - app/_locales/kn/messages.json | 25 +- app/_locales/ko/messages.json | 26 +- app/_locales/lt/messages.json | 25 +- app/_locales/lv/messages.json | 23 +- app/_locales/ml/messages.json | 3 - app/_locales/mr/messages.json | 3 - app/_locales/ms/messages.json | 25 +- app/_locales/nl/messages.json | 11 +- app/_locales/no/messages.json | 25 +- app/_locales/ph/messages.json | 9 - app/_locales/pl/messages.json | 25 +- app/_locales/pt/messages.json | 11 +- app/_locales/pt_BR/messages.json | 25 +- app/_locales/pt_PT/messages.json | 3 - app/_locales/ro/messages.json | 25 +- app/_locales/ru/messages.json | 22 +- app/_locales/sk/messages.json | 26 +- app/_locales/sl/messages.json | 26 +- app/_locales/sr/messages.json | 25 +- app/_locales/sv/messages.json | 25 +- app/_locales/sw/messages.json | 25 +- app/_locales/ta/messages.json | 14 +- app/_locales/te/messages.json | 3 - app/_locales/th/messages.json | 17 +- app/_locales/tr/messages.json | 11 +- app/_locales/uk/messages.json | 25 +- app/_locales/vi/messages.json | 9 - app/_locales/zh_CN/messages.json | 20 +- app/_locales/zh_TW/messages.json | 28 +- app/images/eth_logo.svg | 24 +- app/images/icon-128.png | Bin 4334 -> 4074 bytes app/images/icon-16.png | Bin 439 -> 606 bytes app/images/icon-19.png | Bin 523 -> 904 bytes app/images/icon-32.png | Bin 1284 -> 1961 bytes app/images/icon-38.png | Bin 1142 -> 2325 bytes app/images/icon-512.png | Bin 35988 -> 24191 bytes app/images/icon-64.png | Bin 2837 -> 4204 bytes app/images/permissions-check.svg | 33 +- app/images/user-check.svg | 3 + app/manifest.json | 18 +- app/scripts/background.js | 32 +- app/scripts/contentscript.js | 62 +- app/scripts/controllers/ab-test.js | 57 + app/scripts/controllers/app-state.js | 7 + app/scripts/controllers/detect-tokens.js | 42 +- app/scripts/controllers/ens/ens.js | 25 + app/scripts/controllers/ens/index.js | 94 + .../controllers/incoming-transactions.js | 8 +- .../controllers/network/createInfuraClient.js | 14 +- .../network/createMetamaskMiddleware.js | 15 +- .../controllers/network/middleware/pending.js | 36 + app/scripts/controllers/network/network.js | 9 +- app/scripts/controllers/network/util.js | 21 + app/scripts/controllers/onboarding.js | 46 +- app/scripts/controllers/permissions/index.js | 556 ++- .../permissions/internalMethodMiddleware.js | 91 - .../permissions/loggerMiddleware.js | 16 +- .../permissions/methodMiddleware.js | 141 + .../permissions/permissions-safe-methods.json | 1 + .../controllers/permissions/permissions.js | 347 -- .../permissions/restrictedMethods.js | 10 +- app/scripts/controllers/plugins.js | 22 +- app/scripts/controllers/preferences.js | 72 +- app/scripts/controllers/recent-blocks.js | 8 +- app/scripts/controllers/threebox.js | 113 +- app/scripts/controllers/token-rates.js | 20 +- app/scripts/controllers/transactions/index.js | 97 +- .../lib/tx-state-history-helper.js | 8 +- .../controllers/transactions/lib/util.js | 12 +- .../transactions/pending-tx-tracker.js | 24 +- .../controllers/transactions/tx-gas-utils.js | 8 +- .../transactions/tx-state-manager.js | 24 +- app/scripts/inpage.js | 2 - app/scripts/lib/account-tracker.js | 12 +- app/scripts/lib/buy-eth-url.js | 6 +- app/scripts/lib/createDnodeRemoteGetter.js | 4 +- app/scripts/lib/createLoggerMiddleware.js | 4 +- app/scripts/lib/ens-ipfs/setup.js | 16 +- app/scripts/lib/freezeGlobals.js | 41 + app/scripts/lib/local-store.js | 26 +- app/scripts/lib/message-manager.js | 16 +- app/scripts/lib/migrator/index.js | 8 +- app/scripts/lib/nodeify.js | 6 +- app/scripts/lib/notification-manager.js | 20 +- app/scripts/lib/pending-balance-calculator.js | 4 +- app/scripts/lib/personal-message-manager.js | 16 +- app/scripts/lib/setupFetchDebugging.js | 4 +- app/scripts/lib/setupMetamaskMeshMetrics.js | 21 +- app/scripts/lib/setupSentry.js | 12 +- app/scripts/lib/stream-utils.js | 4 +- app/scripts/lib/typed-message-manager.js | 20 +- app/scripts/lib/util.js | 7 +- app/scripts/metamask-controller.js | 392 +- app/scripts/migrations/004.js | 4 +- app/scripts/migrations/015.js | 7 +- app/scripts/migrations/016.js | 4 +- app/scripts/migrations/017.js | 4 +- app/scripts/migrations/019.js | 4 +- app/scripts/migrations/022.js | 4 +- app/scripts/migrations/023.js | 11 +- app/scripts/migrations/024.js | 4 +- app/scripts/migrations/025.js | 8 +- app/scripts/migrations/029.js | 1 - app/scripts/migrations/031.js | 2 +- app/scripts/migrations/037.js | 56 + app/scripts/migrations/038.js | 37 + app/scripts/migrations/039.js | 67 + app/scripts/migrations/index.js | 3 + app/scripts/ui.js | 4 + development/auto-changelog.sh | 2 +- development/metamaskbot-build-announce.js | 2 +- development/mock-3box.js | 11 +- development/mock-dev.js | 4 +- development/rollback.sh | 14 +- development/sentry-publish.js | 2 +- development/show-deps-install-scripts.js | 38 + development/sourcemap-validator.js | 8 +- development/states/confirm-sig-requests.json | 3 + development/states/currency-localization.json | 3 + development/states/tx-list-items.json | 3 + development/static-server.js | 92 + development/verify-locale-strings.js | 70 +- gulpfile.js | 36 +- package.json | 44 +- test/data/mock-state.json | 16 +- test/e2e/address-book.spec.js | 67 +- test/e2e/contract-test/contract.js | 361 +- test/e2e/contract-test/index.html | 94 +- test/e2e/ethereum-on.spec.js | 17 +- test/e2e/from-import-ui.spec.js | 11 +- test/e2e/func.js | 2 +- test/e2e/incremental-security.spec.js | 43 +- test/e2e/metamask-responsive-ui.spec.js | 55 +- test/e2e/metamask-ui.spec.js | 288 +- test/e2e/run-all.sh | 41 +- test/e2e/run-web3.sh | 2 +- test/e2e/send-edit.spec.js | 19 +- test/e2e/signature-request.spec.js | 196 + test/e2e/threebox.spec.js | 40 +- test/e2e/web3.spec.js | 10 +- test/helper.js | 12 +- test/integration/index.js | 4 +- test/lib/mock-simple-keychain.js | 4 +- test/lib/util.js | 8 +- test/setup.js | 8 +- test/unit-global/frozenPromise.js | 55 + test/unit/actions/tx_test.js | 12 +- test/unit/app/buy-eth-url.spec.js | 4 +- test/unit/app/controllers/assets-test.js | 8 +- .../controllers/balance-controller.spec.js | 55 + .../app/controllers/ens-controller-test.js | 135 + .../controllers/metamask-controller-test.js | 91 +- .../network-controller-test.js} | 4 +- .../network/pending-middleware-test.js | 85 + test/unit/app/controllers/network/stubs.js | 225 + .../preferences-controller-test.js | 32 +- .../transactions/pending-tx-test.js | 36 +- .../transactions/tx-controller-test.js | 82 +- .../tx-state-history-helper-test.js | 4 +- .../controllers/transactions/tx-utils-test.js | 20 +- test/unit/app/nodeify-test.js | 16 +- test/unit/app/typed-message-manager.spec.js | 116 + test/unit/app/util-test.js | 7 +- test/unit/migrations/023-test.js | 4 +- test/unit/migrations/024-test.js | 7 +- test/unit/migrations/025-test.js | 8 +- test/unit/migrations/027-test.js | 4 +- test/unit/migrations/029-test.js | 4 +- test/unit/migrations/037-test.js | 121 + test/unit/migrations/038-test.js | 60 + test/unit/migrations/039-test.js | 419 ++ test/unit/migrations/migrator-test.js | 4 +- test/unit/test-utils.js | 4 +- test/unit/ui/app/actions.spec.js | 545 ++- .../unit/ui/app/components/token-cell.spec.js | 66 +- test/unit/ui/app/selectors.spec.js | 6 - test/unit/util_test.js | 4 +- .../account-details.component.js | 2 +- ui/app/components/app/account-menu/index.scss | 3 +- .../app/app-header/app-header.component.js | 4 +- .../confirm-detail-row/index.scss | 7 + .../confirm-detail-row.component.test.js | 16 +- .../confirm-page-container-content/index.scss | 1 + ...confirm-page-container-header.component.js | 55 +- .../confirm-page-container-header/index.scss | 13 + .../confirm-page-container.component.js | 30 +- .../app/confirm-page-container/index.scss | 6 + .../dai-migration-notification.component.js | 78 + .../dai-migration-notification.container.js | 34 + .../app/dai-migration-component/index.js | 1 + .../app/dropdowns/components/dropdown.js | 4 - .../app/dropdowns/components/menu.js | 16 +- .../app/dropdowns/network-dropdown.js | 4 +- .../app/dropdowns/tests/dropdown.test.js | 6 +- .../app/dropdowns/tests/menu.test.js | 12 +- .../tests/network-dropdown-icon.test.js | 8 +- .../advanced-gas-inputs.component.js | 206 +- .../advanced-gas-inputs.container.js | 4 +- .../advanced-tab-content.component.js | 168 +- .../advanced-tab-content/index.scss | 130 +- .../advanced-tab-content-component.test.js | 271 +- .../basic-tab-content.component.js | 2 +- .../gas-modal-page-container.component.js | 103 +- .../gas-modal-page-container.container.js | 69 +- ...gas-modal-page-container-component.test.js | 74 +- ...gas-modal-page-container-container.test.js | 15 +- .../gas-price-button-group.component.js | 19 +- .../gas-price-button-group-component.test.js | 8 +- .../gas-price-chart/gas-price-chart.utils.js | 36 +- .../gas-slider/gas-slider.component.js | 2 +- .../home-notification.component.js | 8 +- .../app/home-notification/index.scss | 8 +- ui/app/components/app/index.scss | 2 + ui/app/components/app/input-number.js | 4 +- .../components/app/modal/modal.component.js | 9 +- .../app/modal/tests/modal.component.test.js | 2 +- .../confirm-remove-account.component.js | 2 +- .../app/modals/deposit-ether-modal.js | 4 + .../edit-approval-permission.component.js | 170 + .../edit-approval-permission.container.js | 18 + .../modals/edit-approval-permission/index.js | 1 + .../edit-approval-permission/index.scss | 167 + .../app/modals/export-private-key-modal.js | 1 + ui/app/components/app/modals/index.scss | 2 + .../loading-network-error.component.js | 2 +- .../metametrics-opt-in-modal.component.js | 6 +- ui/app/components/app/modals/modal.js | 26 + .../modals/qr-scanner/qr-scanner.component.js | 10 +- .../multiple-notifications.component.js | 18 +- ...ission-page-container-content.component.js | 2 +- .../app/sidebars/sidebar.component.js | 4 +- .../sidebars/tests/sidebars-component.test.js | 4 +- .../app/signature-request-original/index.js | 1 + .../signature-request-original.component.js | 318 ++ .../signature-request-original.container.js | 72 + ui/app/components/app/signature-request.js | 336 -- .../components/app/signature-request/index.js | 1 + .../app/signature-request/index.scss | 96 + .../signature-request-footer/index.js | 1 + .../signature-request-footer/index.scss | 18 + .../signature-request-footer.component.js | 24 + .../signature-request-header/index.js | 1 + .../signature-request-header/index.scss | 25 + .../signature-request-header.component.js | 29 + .../signature-request-message/index.js | 1 + .../signature-request-message/index.scss | 67 + .../signature-request-message.component.js | 50 + .../signature-request.component.js | 82 + .../signature-request.constants.js | 3 + .../signature-request.container.js | 72 + .../tests/signature-request.test.js | 25 + ui/app/components/app/token-list.js | 12 +- ...transaction-activity-log.component.test.js | 2 +- .../transaction-list-item-details/index.js | 2 +- ...action-list-item-details.component.test.js | 2 +- ...transaction-list-item-details.component.js | 39 +- ...transaction-list-item-details.container.js | 28 + .../app/transaction-list-item/index.scss | 27 +- .../transaction-list-item.component.js | 50 +- .../transaction-list-item.container.js | 18 +- .../transaction-list.component.js | 42 +- .../transaction-list.container.js | 13 +- .../app/transaction-time-remaining/index.js | 1 + .../transaction-time-remaining.component.js | 52 + .../transaction-time-remaining.container.js | 41 + .../transaction-time-remaining.util.js | 13 + .../transaction-view-balance.component.js | 4 +- ...erenced-currency-display.component.test.js | 2 +- .../account-dropdown-mini.container.js | 4 +- .../account-dropdown-mini.component.test.js | 4 +- .../tests/button-group-component.test.js | 6 +- .../tests/currency-input.component.test.js | 2 +- ui/app/components/ui/eth-balance.js | 8 +- .../export-text-container.component.js | 2 +- ui/app/components/ui/fiat-value.js | 4 +- ui/app/components/ui/mascot.js | 4 +- .../tests/metafox-logo.component.test.js | 2 +- .../page-container-footer.component.test.js | 12 +- .../page-container-header.component.test.js | 14 +- .../sender-to-recipient.component.js | 44 +- ui/app/components/ui/snackbar/index.js | 1 + ui/app/components/ui/snackbar/index.scss | 11 + .../ui/snackbar/snackbar.component.js | 18 + ui/app/components/ui/tooltip-v2.js | 8 +- .../ui/unit-input/unit-input.component.js | 4 +- ui/app/css/itcss/components/index.scss | 1 + ui/app/css/itcss/components/new-account.scss | 1 + .../css/itcss/components/wallet-balance.scss | 1 - ui/app/css/itcss/tools/utilities.scss | 4 +- ui/app/ducks/index.js | 4 +- ui/app/ducks/metamask/metamask.js | 13 +- ui/app/helpers/constants/common.js | 6 + .../higher-order-components/i18n-provider.js | 13 +- .../with-token-tracker.component.js | 9 +- .../helpers/utils/gas-time-estimates.util.js | 99 + ui/app/helpers/utils/i18n-helper.js | 22 +- ui/app/helpers/utils/metametrics.util.js | 2 +- ui/app/helpers/utils/token-util.js | 5 + ui/app/helpers/utils/util.js | 50 +- .../confirm-approve-content.component.js | 226 + .../confirm-approve-content/index.js | 1 + .../confirm-approve-content/index.scss | 306 ++ .../confirm-approve.component.js | 101 +- .../confirm-approve.container.js | 97 +- .../confirm-approve/confirm-approve.util.js | 28 + ui/app/pages/confirm-approve/index.scss | 1 + .../confirm-deploy-contract.component.js | 2 +- .../confirm-send-ether.component.js | 2 +- .../confirm-transaction-base.component.js | 175 +- .../confirm-transaction-base.container.js | 58 +- ...confirm-transaction-base.container.test.js | 20 + ui/app/pages/confirm-transaction/conf-tx.js | 50 +- .../confirm-transaction.component.js | 24 +- .../confirm-transaction.container.js | 15 +- .../connect-hardware/connect-screen.js | 8 +- .../create-account.component.js | 79 + .../create-account.container.js | 20 + ui/app/pages/create-account/index.js | 114 +- .../create-account/new-account.component.js | 91 + .../create-account/new-account.container.js | 35 + ui/app/pages/create-account/new-account.js | 130 - .../import-with-seed-phrase.component.js | 7 +- .../end-of-flow/end-of-flow.component.js | 45 +- .../end-of-flow/end-of-flow.container.js | 7 +- .../first-time-flow/end-of-flow/index.scss | 2 +- .../first-time-flow.selectors.js | 28 +- ui/app/pages/first-time-flow/index.scss | 2 +- .../metametrics-opt-in.component.js | 6 +- .../onboarding-initiator-util.js | 48 + .../draggable-seed.component.js | 3 +- .../seed-phrase/reveal-seed-phrase/index.scss | 2 +- .../reveal-seed-phrase.component.js | 34 +- .../reveal-seed-phrase.container.js | 9 +- ui/app/pages/home/home.component.js | 60 +- ui/app/pages/home/home.container.js | 14 +- ui/app/pages/index.scss | 2 + ui/app/pages/keychains/restore-vault.js | 5 + ui/app/pages/mobile-sync/index.js | 4 +- ui/app/pages/routes/index.js | 14 +- ui/app/pages/send/README.md | 0 .../account-list-item-README.md | 0 .../account-list-item.component.js | 4 +- .../tests/account-list-item-component.test.js | 6 +- .../add-recipient/ens-input.component.js | 12 +- .../tests/add-recipient-component.test.js | 4 +- .../send-content/send-amount-row/README.md | 0 .../amount-max-button.component.js | 2 +- .../tests/amount-max-button-component.test.js | 6 +- .../send-amount-row.component.js | 2 +- .../tests/send-amount-row-component.test.js | 14 +- .../send-dropdown-list.component.js | 2 +- .../send-dropdown-list-component.test.js | 4 +- .../send/send-content/send-gas-row/README.md | 0 .../test/gas-fee-display.component.test.js | 6 +- .../send-gas-row/send-gas-row.component.js | 4 +- .../tests/send-gas-row-component.test.js | 6 +- .../send-hex-data-row.component.js | 2 +- .../send-row-error-message-README.md | 0 .../send-row-error-message-component.test.js | 2 +- .../send-row-wrapper-README.md | 0 .../tests/send-row-wrapper-component.test.js | 12 +- .../tests/send-content-component.test.js | 2 +- ui/app/pages/send/send-footer/README.md | 0 .../send/send-footer/send-footer.component.js | 11 +- .../send/send-footer/send-footer.container.js | 6 +- .../tests/send-footer-component.test.js | 32 +- .../tests/send-footer-container.test.js | 4 +- ui/app/pages/send/send-header/README.md | 0 .../tests/send-header-component.test.js | 2 +- ui/app/pages/send/send.component.js | 4 +- ui/app/pages/send/send.container.js | 10 +- ui/app/pages/send/send.utils.js | 17 +- .../pages/send/tests/send-component.test.js | 28 +- .../send/tests/send-selectors-test-data.js | 14 +- ui/app/pages/send/tests/send-utils.test.js | 8 +- .../pages/send/to-autocomplete.component.js | 14 +- .../advanced-tab/advanced-tab.component.js | 71 +- .../advanced-tab/advanced-tab.container.js | 15 +- .../tests/advanced-tab-component.test.js | 21 +- .../tests/advanced-tab-container.test.js | 5 +- .../add-contact/add-contact.component.js | 6 +- .../edit-contact/edit-contact.component.js | 32 +- .../edit-contact/edit-contact.container.js | 5 +- .../settings/contact-list-tab/index.scss | 2 + .../network-form/network-form.component.js | 31 +- .../networks-tab/networks-tab.component.js | 4 +- .../permissions-activity.component.js | 8 +- .../permissions-history.component.js | 4 +- .../permissions-list.component.js | 12 +- .../plugins-tab/plugins-list/index.js | 4 +- ui/app/selectors/custom-gas.js | 14 +- ui/app/selectors/custom-gas.test.js | 60 +- ui/app/selectors/selectors.js | 29 +- ui/app/selectors/tests/selectors-test-data.js | 16 +- ui/app/selectors/tests/selectors.test.js | 8 +- ui/app/selectors/transactions.js | 14 + ui/app/store/actions.js | 292 +- ui/design/00-metamask-SignIn.jpg | Bin 57848 -> 0 bytes ui/design/01-metamask-SelectAcc.jpg | Bin 76063 -> 0 bytes ui/design/02-metamask-AccDetails.jpg | Bin 75780 -> 0 bytes .../02a-metamask-AccDetails-OverToken.jpg | Bin 121847 -> 0 bytes ...2a-metamask-AccDetails-OverTransaction.jpg | Bin 122075 -> 0 bytes ui/design/02a-metamask-AccDetails.jpg | Bin 117570 -> 0 bytes ui/design/02b-metamask-AccDetails-Send.jpg | Bin 110143 -> 0 bytes ui/design/03-metamask-Qr.jpg | Bin 66052 -> 0 bytes ui/design/05-metamask-Menu.jpg | Bin 130264 -> 0 bytes .../final_screen_dao_accounts.png | Bin 249708 -> 0 bytes .../final_screen_dao_locked.png | Bin 220295 -> 0 bytes .../final_screen_dao_notification.png | Bin 214405 -> 0 bytes .../final_screen_wei_account.png | Bin 253382 -> 0 bytes .../final_screen_wei_notification.png | Bin 193865 -> 0 bytes ui/design/chromeStorePics/icon-128.png | Bin 5770 -> 0 bytes ui/design/chromeStorePics/icon-64.png | Bin 3573 -> 0 bytes ui/design/chromeStorePics/metamask_icon.ai | 2383 ---------- ui/design/chromeStorePics/promo1400560.png | Bin 261644 -> 0 bytes ui/design/chromeStorePics/promo440280.png | Bin 57471 -> 0 bytes ui/design/chromeStorePics/promo920680.png | Bin 206713 -> 0 bytes .../chromeStorePics/screen_dao_accounts.png | Bin 517598 -> 0 bytes .../chromeStorePics/screen_dao_locked.png | Bin 287108 -> 0 bytes .../screen_dao_notification.png | Bin 296498 -> 0 bytes .../chromeStorePics/screen_wei_account.png | Bin 653633 -> 0 bytes .../screen_wei_notification.png | Bin 402486 -> 0 bytes ui/design/metamask-logo-eyes.png | Bin 146076 -> 0 bytes ui/design/wireframes/1st_time_use.png | Bin 937556 -> 0 bytes ui/design/wireframes/metamask_wfs_jan_13.pdf | Bin 452413 -> 0 bytes ui/design/wireframes/metamask_wfs_jan_13.png | Bin 419066 -> 0 bytes ui/design/wireframes/metamask_wfs_jan_18.pdf | Bin 612778 -> 0 bytes ui/index.html | 21 - ui/index.js | 8 +- yarn.lock | 4224 +++++++++-------- 465 files changed, 12418 insertions(+), 9146 deletions(-) create mode 100755 .circleci/scripts/deps-install.sh create mode 100644 app/images/user-check.svg create mode 100644 app/scripts/controllers/ab-test.js create mode 100644 app/scripts/controllers/ens/ens.js create mode 100644 app/scripts/controllers/ens/index.js create mode 100644 app/scripts/controllers/network/middleware/pending.js delete mode 100644 app/scripts/controllers/permissions/internalMethodMiddleware.js create mode 100644 app/scripts/controllers/permissions/methodMiddleware.js delete mode 100644 app/scripts/controllers/permissions/permissions.js create mode 100644 app/scripts/lib/freezeGlobals.js create mode 100644 app/scripts/migrations/037.js create mode 100644 app/scripts/migrations/038.js create mode 100644 app/scripts/migrations/039.js create mode 100644 development/show-deps-install-scripts.js create mode 100644 development/static-server.js create mode 100644 test/e2e/signature-request.spec.js create mode 100644 test/unit-global/frozenPromise.js create mode 100644 test/unit/app/controllers/balance-controller.spec.js create mode 100644 test/unit/app/controllers/ens-controller-test.js rename test/unit/app/controllers/{network-contoller-test.js => network/network-controller-test.js} (95%) create mode 100644 test/unit/app/controllers/network/pending-middleware-test.js create mode 100644 test/unit/app/controllers/network/stubs.js create mode 100644 test/unit/app/typed-message-manager.spec.js create mode 100644 test/unit/migrations/037-test.js create mode 100644 test/unit/migrations/038-test.js create mode 100644 test/unit/migrations/039-test.js create mode 100644 ui/app/components/app/dai-migration-component/dai-migration-notification.component.js create mode 100644 ui/app/components/app/dai-migration-component/dai-migration-notification.container.js create mode 100644 ui/app/components/app/dai-migration-component/index.js create mode 100644 ui/app/components/app/modals/edit-approval-permission/edit-approval-permission.component.js create mode 100644 ui/app/components/app/modals/edit-approval-permission/edit-approval-permission.container.js create mode 100644 ui/app/components/app/modals/edit-approval-permission/index.js create mode 100644 ui/app/components/app/modals/edit-approval-permission/index.scss create mode 100644 ui/app/components/app/signature-request-original/index.js create mode 100644 ui/app/components/app/signature-request-original/signature-request-original.component.js create mode 100644 ui/app/components/app/signature-request-original/signature-request-original.container.js delete mode 100644 ui/app/components/app/signature-request.js create mode 100644 ui/app/components/app/signature-request/index.js create mode 100644 ui/app/components/app/signature-request/index.scss create mode 100644 ui/app/components/app/signature-request/signature-request-footer/index.js create mode 100644 ui/app/components/app/signature-request/signature-request-footer/index.scss create mode 100644 ui/app/components/app/signature-request/signature-request-footer/signature-request-footer.component.js create mode 100644 ui/app/components/app/signature-request/signature-request-header/index.js create mode 100644 ui/app/components/app/signature-request/signature-request-header/index.scss create mode 100644 ui/app/components/app/signature-request/signature-request-header/signature-request-header.component.js create mode 100644 ui/app/components/app/signature-request/signature-request-message/index.js create mode 100644 ui/app/components/app/signature-request/signature-request-message/index.scss create mode 100644 ui/app/components/app/signature-request/signature-request-message/signature-request-message.component.js create mode 100644 ui/app/components/app/signature-request/signature-request.component.js create mode 100644 ui/app/components/app/signature-request/signature-request.constants.js create mode 100644 ui/app/components/app/signature-request/signature-request.container.js create mode 100644 ui/app/components/app/signature-request/tests/signature-request.test.js create mode 100644 ui/app/components/app/transaction-list-item-details/transaction-list-item-details.container.js create mode 100644 ui/app/components/app/transaction-time-remaining/index.js create mode 100644 ui/app/components/app/transaction-time-remaining/transaction-time-remaining.component.js create mode 100644 ui/app/components/app/transaction-time-remaining/transaction-time-remaining.container.js create mode 100644 ui/app/components/app/transaction-time-remaining/transaction-time-remaining.util.js create mode 100644 ui/app/components/ui/snackbar/index.js create mode 100644 ui/app/components/ui/snackbar/index.scss create mode 100644 ui/app/components/ui/snackbar/snackbar.component.js create mode 100644 ui/app/helpers/utils/gas-time-estimates.util.js create mode 100644 ui/app/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js create mode 100644 ui/app/pages/confirm-approve/confirm-approve-content/index.js create mode 100644 ui/app/pages/confirm-approve/confirm-approve-content/index.scss create mode 100644 ui/app/pages/confirm-approve/confirm-approve.util.js create mode 100644 ui/app/pages/confirm-approve/index.scss create mode 100644 ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.container.test.js create mode 100644 ui/app/pages/create-account/create-account.component.js create mode 100644 ui/app/pages/create-account/create-account.container.js create mode 100644 ui/app/pages/create-account/new-account.component.js create mode 100644 ui/app/pages/create-account/new-account.container.js delete mode 100644 ui/app/pages/create-account/new-account.js create mode 100644 ui/app/pages/first-time-flow/onboarding-initiator-util.js delete mode 100644 ui/app/pages/send/README.md delete mode 100644 ui/app/pages/send/account-list-item/account-list-item-README.md delete mode 100644 ui/app/pages/send/send-content/send-amount-row/README.md delete mode 100644 ui/app/pages/send/send-content/send-gas-row/README.md delete mode 100644 ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md delete mode 100644 ui/app/pages/send/send-content/send-row-wrapper/send-row-wrapper-README.md delete mode 100644 ui/app/pages/send/send-footer/README.md delete mode 100644 ui/app/pages/send/send-header/README.md delete mode 100644 ui/design/00-metamask-SignIn.jpg delete mode 100644 ui/design/01-metamask-SelectAcc.jpg delete mode 100644 ui/design/02-metamask-AccDetails.jpg delete mode 100644 ui/design/02a-metamask-AccDetails-OverToken.jpg delete mode 100644 ui/design/02a-metamask-AccDetails-OverTransaction.jpg delete mode 100644 ui/design/02a-metamask-AccDetails.jpg delete mode 100644 ui/design/02b-metamask-AccDetails-Send.jpg delete mode 100644 ui/design/03-metamask-Qr.jpg delete mode 100644 ui/design/05-metamask-Menu.jpg delete mode 100644 ui/design/chromeStorePics/final_screen_dao_accounts.png delete mode 100644 ui/design/chromeStorePics/final_screen_dao_locked.png delete mode 100644 ui/design/chromeStorePics/final_screen_dao_notification.png delete mode 100644 ui/design/chromeStorePics/final_screen_wei_account.png delete mode 100644 ui/design/chromeStorePics/final_screen_wei_notification.png delete mode 100644 ui/design/chromeStorePics/icon-128.png delete mode 100644 ui/design/chromeStorePics/icon-64.png delete mode 100644 ui/design/chromeStorePics/metamask_icon.ai delete mode 100644 ui/design/chromeStorePics/promo1400560.png delete mode 100644 ui/design/chromeStorePics/promo440280.png delete mode 100644 ui/design/chromeStorePics/promo920680.png delete mode 100644 ui/design/chromeStorePics/screen_dao_accounts.png delete mode 100644 ui/design/chromeStorePics/screen_dao_locked.png delete mode 100644 ui/design/chromeStorePics/screen_dao_notification.png delete mode 100644 ui/design/chromeStorePics/screen_wei_account.png delete mode 100644 ui/design/chromeStorePics/screen_wei_notification.png delete mode 100644 ui/design/metamask-logo-eyes.png delete mode 100644 ui/design/wireframes/1st_time_use.png delete mode 100644 ui/design/wireframes/metamask_wfs_jan_13.pdf delete mode 100644 ui/design/wireframes/metamask_wfs_jan_13.png delete mode 100644 ui/design/wireframes/metamask_wfs_jan_18.pdf delete mode 100644 ui/index.html diff --git a/.circleci/config.yml b/.circleci/config.yml index 831a3d2ac..5240611e9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,7 +1,6 @@ -version: 2 +version: 2.1 workflows: - version: 2 test_and_release: jobs: - create_release_pull_request: @@ -23,6 +22,7 @@ workflows: - test-lint: requires: - prep-deps + - test-lint-shellcheck # - test-e2e-chrome: # requires: # - prep-deps @@ -32,6 +32,9 @@ workflows: - test-unit: requires: - prep-deps + - test-unit-global: + requires: + - prep-deps - test-mozilla-lint: requires: - prep-deps @@ -47,7 +50,9 @@ workflows: - all-tests-pass: requires: - test-lint + - test-lint-shellcheck - test-unit + - test-unit-global - test-mozilla-lint # - test-e2e-chrome # - test-e2e-firefox @@ -74,7 +79,7 @@ workflows: jobs: create_release_pull_request: docker: - - image: circleci/node:8.15.1-browsers + - image: circleci/node:10.17-browsers steps: - checkout - run: @@ -86,13 +91,13 @@ jobs: prep-deps: docker: - - image: circleci/node:10.16-browsers + - image: circleci/node:10.17-browsers steps: - checkout - run: name: Install deps command: | - yarn --frozen-lockfile --har + .circleci/scripts/deps-install.sh - run: name: Collect yarn install HAR logs command: | @@ -105,7 +110,7 @@ jobs: prep-build: docker: - - image: circleci/node:10.16-browsers + - image: circleci/node:10.17-browsers steps: - checkout - attach_workspace: @@ -124,7 +129,7 @@ jobs: prep-docs: docker: - - image: circleci/node:10.16-browsers + - image: circleci/node:10.17-browsers steps: - checkout - attach_workspace: @@ -139,7 +144,7 @@ jobs: prep-scss: docker: - - image: circleci/node:10.16-browsers + - image: circleci/node:10.17-browsers steps: - checkout - attach_workspace: @@ -158,18 +163,31 @@ jobs: test-lint: docker: - - image: circleci/node:10.16-browsers + - image: circleci/node:10.17-browsers steps: - checkout - attach_workspace: at: . - run: - name: Test + name: Lint command: yarn lint + - run: + name: Verify locales + command: yarn verify-locales --quiet + + test-lint-shellcheck: + docker: + - image: circleci/node:10.17-browsers + steps: + - checkout + - run: sudo apt-get install shellcheck + - run: + name: Shellcheck Lint + command: yarn lint:shellcheck test-deps: docker: - - image: circleci/node:10.16-browsers + - image: circleci/node:10.17-browsers steps: - checkout - attach_workspace: @@ -180,7 +198,7 @@ jobs: test-e2e-chrome: docker: - - image: circleci/node:10.16-browsers + - image: circleci/node:10.17-browsers steps: - checkout - attach_workspace: @@ -195,7 +213,7 @@ jobs: test-e2e-firefox: docker: - - image: circleci/node:10.16-browsers + - image: circleci/node:10.17-browsers steps: - checkout - run: @@ -213,7 +231,7 @@ jobs: job-publish-prerelease: docker: - - image: circleci/node:10.16-browsers + - image: circleci/node:10.17-browsers steps: - checkout - attach_workspace: @@ -243,7 +261,7 @@ jobs: job-publish-release: docker: - - image: circleci/node:10.16-browsers + - image: circleci/node:10.17-browsers steps: - checkout - attach_workspace: @@ -267,7 +285,7 @@ jobs: test-unit: docker: - - image: circleci/node:10.16-browsers + - image: circleci/node:10.17-browsers steps: - checkout - attach_workspace: @@ -280,9 +298,19 @@ jobs: paths: - .nyc_output - coverage + test-unit-global: + docker: + - image: circleci/node:10.17-browsers + steps: + - checkout + - attach_workspace: + at: . + - run: + name: test:unit:global + command: yarn test:unit:global test-mozilla-lint: docker: - - image: circleci/node:10.16-browsers + - image: circleci/node:10.17-browsers steps: - checkout - attach_workspace: @@ -293,7 +321,7 @@ jobs: test-integration-flat-firefox: docker: - - image: circleci/node:10.16-browsers + - image: circleci/node:10.17-browsers steps: - checkout - attach_workspace: @@ -309,7 +337,7 @@ jobs: environment: browsers: '["Chrome"]' docker: - - image: circleci/node:10.16-browsers + - image: circleci/node:10.17-browsers steps: - checkout - attach_workspace: @@ -320,7 +348,7 @@ jobs: all-tests-pass: docker: - - image: circleci/node:10.16-browsers + - image: circleci/node:10.17-browsers steps: - run: name: All Tests Passed @@ -328,7 +356,7 @@ jobs: coveralls-upload: docker: - - image: circleci/node:10.16-browsers + - image: circleci/node:10.17-browsers steps: - checkout - attach_workspace: diff --git a/.circleci/scripts/deps-install.sh b/.circleci/scripts/deps-install.sh new file mode 100755 index 000000000..605eb8593 --- /dev/null +++ b/.circleci/scripts/deps-install.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -x + +yarn --frozen-lockfile --ignore-scripts --har + +# run each in subshell so directory change does not persist +# scripts can be any of: +# preinstall +# install +# postinstall + +# for build +(cd node_modules/node-sass && yarn run postinstall) +(cd node_modules/optipng-bin && yarn run postinstall) +(cd node_modules/gifsicle && yarn run postinstall) +(cd node_modules/jpegtran-bin && yarn run postinstall) + +# for test +(cd node_modules/scrypt && yarn run install) +(cd node_modules/weak && yarn run install) +(cd node_modules/chromedriver && yarn run install) +(cd node_modules/geckodriver && yarn run postinstall) + +# for release +(cd node_modules/@sentry/cli && yarn run install) diff --git a/.circleci/scripts/firefox-install b/.circleci/scripts/firefox-install index 3f0772f49..21766467e 100755 --- a/.circleci/scripts/firefox-install +++ b/.circleci/scripts/firefox-install @@ -4,7 +4,7 @@ set -e set -u set -o pipefail -FIREFOX_VERSION='68.0' +FIREFOX_VERSION='70.0' FIREFOX_BINARY="firefox-${FIREFOX_VERSION}.tar.bz2" FIREFOX_BINARY_URL="https://ftp.mozilla.org/pub/firefox/releases/${FIREFOX_VERSION}/linux-x86_64/en-US/${FIREFOX_BINARY}" FIREFOX_PATH='/opt/firefox' diff --git a/.eslintrc b/.eslintrc index c047f91eb..9193c25a5 100644 --- a/.eslintrc +++ b/.eslintrc @@ -30,7 +30,8 @@ "mocha", "chai", "react", - "json" + "json", + "import" ], "globals": { @@ -44,17 +45,18 @@ }, "rules": { + "import/no-unresolved": ["error", { "commonjs": true }], "no-restricted-globals": ["error", "event"], "accessor-pairs": 2, "arrow-spacing": [2, { "before": true, "after": true }], "block-spacing": [2, "always"], - "brace-style": [2, "1tbs", { "allowSingleLine": true }], + "brace-style": 2, "camelcase": [2, { "properties": "never" }], "comma-dangle": [2, "always-multiline"], "comma-spacing": [2, { "before": false, "after": true }], "comma-style": [2, "last"], "constructor-super": 2, - "curly": [2, "multi-line"], + "curly": 2, "dot-location": [2, "property"], "eol-last": 2, "eqeqeq": [2, "allow-null"], @@ -147,7 +149,11 @@ "operator-linebreak": [2, "after", { "overrides": { "?": "ignore", ":": "ignore" } }], "padded-blocks": "off", "quotes": [2, "single", {"avoidEscape": true, "allowTemplateLiterals": true}], + "react/jsx-boolean-value": 2, + "react/jsx-curly-brace-presence": [2, { "props": "never", "children": "never" }], + "react/jsx-equals-spacing": 2, "react/no-deprecated": 0, + "react/default-props-match-prop-types": 2, "semi": [2, "never"], "semi-spacing": [2, { "before": false, "after": true }], "space-before-blocks": [2, "always"], diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index adef939d1..90936ceea 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,4 +5,4 @@ package.json @danjm @whymarrh @Gudahtt yarn.lock @danjm @whymarrh @Gudahtt ui/ @danjm @whymarrh @Gudahtt app/scripts/controllers/transactions @frankiebee - +.circleci/scripts/deps-install.sh @kumavis @Gudahtt \ No newline at end of file diff --git a/.nvmrc b/.nvmrc index 70047db82..c095bf0f4 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v10.16.3 +v10.17.0 diff --git a/.stylelintrc b/.stylelintrc index d080d68d9..3615abd98 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -33,7 +33,7 @@ "always", { "ignore": [ - "after-comment", + "after-comment" ] } ], diff --git a/CHANGELOG.md b/CHANGELOG.md index f20abe56b..a37d5c989 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,100 @@ # Changelog ## Current Develop Branch +- [#7480](https://github.com/MetaMask/metamask-extension/pull/7480): Fixed link on root README.md + +## 7.6.1 Tue Nov 19 2019 +- [#7475](https://github.com/MetaMask/metamask-extension/pull/7475): Add 'Remind Me Later' to the Maker notification +- [#7436](https://github.com/MetaMask/metamask-extension/pull/7436): Add additional rpcUrl verification +- [#7468](https://github.com/MetaMask/metamask-extension/pull/7468): Show transaction fee units on approve screen + +## 7.6.0 Mon Nov 18 2019 +- [#7450](https://github.com/MetaMask/metamask-extension/pull/7450): Add migration notification for users with non-zero Sai +- [#7461](https://github.com/MetaMask/metamask-extension/pull/7461): Import styles for showing multiple notifications +- [#7451](https://github.com/MetaMask/metamask-extension/pull/7451): Add button disabled when password is empty + +## 7.5.3 Fri Nov 15 2019 +- [#7412](https://github.com/MetaMask/metamask-extension/pull/7412): lock eth-contract-metadata (#7412) +- [#7416](https://github.com/MetaMask/metamask-extension/pull/7416): Add eslint import plugin to help detect unresolved paths +- [#7414](https://github.com/MetaMask/metamask-extension/pull/7414): Ensure SignatureRequestOriginal 'beforeunload' handler is bound (#7414) +- [#7430](https://github.com/MetaMask/metamask-extension/pull/7430): Update badge colour +- [#7408](https://github.com/MetaMask/metamask-extension/pull/7408): Utilize the full size of icon space (#7408) +- [#7431](https://github.com/MetaMask/metamask-extension/pull/7431): Add all icons to manifest (#7431) +- [#7426](https://github.com/MetaMask/metamask-extension/pull/7426): Ensure Etherscan result is valid before reading it (#7426) +- [#7434](https://github.com/MetaMask/metamask-extension/pull/7434): Update 512px icon (#7434) +- [#7410](https://github.com/MetaMask/metamask-extension/pull/7410): Fix sourcemaps for Sentry +- [#7420](https://github.com/MetaMask/metamask-extension/pull/7420): Adds and end to end test for typed signature requests +- [#7439](https://github.com/MetaMask/metamask-extension/pull/7439): Add metricsEvent to contextTypes (#7439) +- [#7419](https://github.com/MetaMask/metamask-extension/pull/7419): Added webRequest.RequestFilter to filter main_frame .eth requests (#7419) + +## 7.5.2 Thu Nov 14 2019 +- [#7414](https://github.com/MetaMask/metamask-extension/pull/7414): Ensure SignatureRequestOriginal 'beforeunload' handler is bound + +## 7.5.1 Tuesday Nov 13 2019 +- [#7402](https://github.com/MetaMask/metamask-extension/pull/7402): Fix regression for signed types data screens +- [#7390](https://github.com/MetaMask/metamask-extension/pull/7390): Update json-rpc-engine +- [#7401](https://github.com/MetaMask/metamask-extension/pull/7401): Reject connection request on window close + +## 7.5.0 Mon Nov 04 2019 +- [#7328](https://github.com/MetaMask/metamask-extension/pull/7328): ignore known transactions on first broadcast and continue with normal flow +- [#7327](https://github.com/MetaMask/metamask-extension/pull/7327): eth_getTransactionByHash will now check metamask's local history for pending transactions +- [#7333](https://github.com/MetaMask/metamask-extension/pull/7333): Cleanup beforeunload handler after transaction is resolved +- [#7038](https://github.com/MetaMask/metamask-extension/pull/7038): Add support for ZeroNet +- [#7334](https://github.com/MetaMask/metamask-extension/pull/7334): Add web3 deprecation warning +- [#6924](https://github.com/MetaMask/metamask-extension/pull/6924): Add Estimated time to pending tx +- [#7177](https://github.com/MetaMask/metamask-extension/pull/7177): ENS Reverse Resolution support +- [#6891](https://github.com/MetaMask/metamask-extension/pull/6891): New signature request v3 UI +- [#7348](https://github.com/MetaMask/metamask-extension/pull/7348): fix width in first time flow button +- [#7271](https://github.com/MetaMask/metamask-extension/pull/7271): Redesign approve screen +- [#7354](https://github.com/MetaMask/metamask-extension/pull/7354): fix account menu width +- [#7379](https://github.com/MetaMask/metamask-extension/pull/7379): Set default advanced tab gas limit +- [#7380](https://github.com/MetaMask/metamask-extension/pull/7380): Fix advanced tab gas chart +- [#7374](https://github.com/MetaMask/metamask-extension/pull/7374): Hide accounts dropdown scrollbars on Firefox +- [#7357](https://github.com/MetaMask/metamask-extension/pull/7357): Update to gaba@1.8.0 +- [#7335](https://github.com/MetaMask/metamask-extension/pull/7335): Add onbeforeunload and have it call onCancel + +## 7.4.0 Tue Oct 29 2019 +- [#7186](https://github.com/MetaMask/metamask-extension/pull/7186): Use `AdvancedGasInputs` in `AdvancedTabContent` +- [#7304](https://github.com/MetaMask/metamask-extension/pull/7304): Move signTypedData signing out to keyrings +- [#7306](https://github.com/MetaMask/metamask-extension/pull/7306): correct the zh-TW translation +- [#7309](https://github.com/MetaMask/metamask-extension/pull/7309): Freeze Promise global on boot +- [#7296](https://github.com/MetaMask/metamask-extension/pull/7296): Add "Retry" option for failed transactions +- [#7319](https://github.com/MetaMask/metamask-extension/pull/7319): Fix transaction list item status spacing issue +- [#7218](https://github.com/MetaMask/metamask-extension/pull/7218): Add hostname and extensionId to site metadata +- [#7324](https://github.com/MetaMask/metamask-extension/pull/7324): Fix contact deletion +- [#7326](https://github.com/MetaMask/metamask-extension/pull/7326): Fix edit contact details +- [#7325](https://github.com/MetaMask/metamask-extension/pull/7325): Update eth-json-rpc-filters to fix memory leak +- [#7334](https://github.com/MetaMask/metamask-extension/pull/7334): Add web3 deprecation warning + +## 7.3.1 Mon Oct 21 2019 +- [#7298](https://github.com/MetaMask/metamask-extension/pull/7298): Turn off full screen vs popup a/b test + +## 7.3.0 Fri Sep 27 2019 +- [#6972](https://github.com/MetaMask/metamask-extension/pull/6972): 3box integration - [#7168](https://github.com/MetaMask/metamask-extension/pull/7168): Add fixes for German translations +- [#7170](https://github.com/MetaMask/metamask-extension/pull/7170): Remove the disk store +- [#7176](https://github.com/MetaMask/metamask-extension/pull/7176): Performance: Delivery optimized images +- [#7189](https://github.com/MetaMask/metamask-extension/pull/7189): add goerli to incoming tx +- [#7190](https://github.com/MetaMask/metamask-extension/pull/7190): Remove unused locale messages +- [#7173](https://github.com/MetaMask/metamask-extension/pull/7173): Fix RPC error messages +- [#7205](https://github.com/MetaMask/metamask-extension/pull/7205): address book entries by chainId +- [#7207](https://github.com/MetaMask/metamask-extension/pull/7207): obs-store/local-store should upgrade webextension error to real error +- [#7162](https://github.com/MetaMask/metamask-extension/pull/7162): Add a/b test for full screen transaction confirmations +- [#7089](https://github.com/MetaMask/metamask-extension/pull/7089): Add advanced setting to enable editing nonce on confirmation screens +- [#7239](https://github.com/MetaMask/metamask-extension/pull/7239): Update ETH logo, update deposit Ether logo height and width +- [#7255](https://github.com/MetaMask/metamask-extension/pull/7255): Use translated string for state log +- [#7266](https://github.com/MetaMask/metamask-extension/pull/7266): fix issue of xyz ens not resolving +- [#7253](https://github.com/MetaMask/metamask-extension/pull/7253): Prevent Logout Timer that's longer than a week. +- [#7285](https://github.com/MetaMask/metamask-extension/pull/7285): Lessen the length of ENS validation to 3 +- [#7287](https://github.com/MetaMask/metamask-extension/pull/7287): Fix phishing detect script + +## 7.2.3 Fri Oct 04 2019 +- [#7252](https://github.com/MetaMask/metamask-extension/pull/7252): Fix gas limit when sending tx without data to a contract +- [#7260](https://github.com/MetaMask/metamask-extension/pull/7260): Do not transate on seed phrases +- [#7252](https://github.com/MetaMask/metamask-extension/pull/7252): Ensure correct tx category when sending to contracts without tx data + +## 7.2.2 Tue Sep 24 2019 +- [#7213](https://github.com/MetaMask/metamask-extension/pull/7213): Update minimum Firefox verison to 56.0 ## 7.2.1 Tue Sep 17 2019 - [#7180](https://github.com/MetaMask/metamask-extension/pull/7180): Add `appName` message to each locale diff --git a/app/_locales/am/messages.json b/app/_locales/am/messages.json index b122b754c..2f043a099 100644 --- a/app/_locales/am/messages.json +++ b/app/_locales/am/messages.json @@ -1,28 +1,16 @@ { - "privacyModeDefault": { - "message": "የግላዊነት ኩነት አሁን በንቡር ነቅቷል" - }, "chartOnlyAvailableEth": { "message": "ቻርት የሚገኘው በ Ethereum አውታረ መረቦች ላይ ብቻ ነው።" }, - "confirmClear": { - "message": "የተፈቀዱ ድረ ገጾችን ለማጥራት እንደሚፈልጉ እርግጠኛ ነዎት?" - }, "contractInteraction": { "message": "የግንኙነት ተግባቦት" }, - "clearApprovalData": { + "clearPermissions": { "message": "የግላዊነት ውሂብን አጥራ" }, "reject": { "message": "አይቀበሉ" }, - "providerRequest": { - "message": "$1ከመለያዎ ጋር ለመገናኘት ይፈልጋል" - }, - "providerRequestInfo": { - "message": "ይህ ድረ ገጽ የእርስዎን መለያ ወቅታዊ አድራሻ ለማየት እየጠየቀ ነው። ምንጊዜም ግንኙነት የሚያደርጉባቸውን ድረ ገጾች የሚያምኗቸው መሆኑን ያረጋግጡ።" - }, "about": { "message": "ስለ" }, @@ -252,7 +240,7 @@ "connect": { "message": "ይገናኙ" }, - "connectRequest": { + "permissionsRequest": { "message": "የግንኙነት ጥያቄ" }, "connectingTo": { @@ -375,9 +363,6 @@ "directDepositEtherExplainer": { "message": "ቀደም ሲል የተወሰነ Ether ካለዎት፣ በአዲሱ ቋትዎ Ether ለማግኘት ፈጣኑ መንገድ ቀጥተኛ ተቀማጭ ነው።" }, - "dismiss": { - "message": "አሰናብት" - }, "done": { "message": "ተጠናቅቋል" }, @@ -1344,7 +1329,7 @@ "updatedWithDate": { "message": "የዘመነ $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URIs አግባብነት ያለው የ HTTP/HTTPS ቅድመ ቅጥያ ይፈልጋል።" }, "usedByClients": { diff --git a/app/_locales/ar/messages.json b/app/_locales/ar/messages.json index 5d21df192..095dcc98b 100644 --- a/app/_locales/ar/messages.json +++ b/app/_locales/ar/messages.json @@ -1,28 +1,16 @@ { - "privacyModeDefault": { - "message": "يتم تمكين وضع الخصوصية الآن بشكل افتراضي" - }, "chartOnlyAvailableEth": { "message": "الرسم البياني متاح فقط على شبكات إيثيريوم." }, - "confirmClear": { - "message": "هل أنت متأكد من أنك تريد مسح المواقع المعتمدة؟" - }, "contractInteraction": { "message": "التفاعل على العقد" }, - "clearApprovalData": { + "clearPermissions": { "message": "مسح بيانات الخصوصية" }, "reject": { "message": "رفض" }, - "providerRequest": { - "message": "يرغب $1 في الاتصال بحسابك" - }, - "providerRequestInfo": { - "message": "يطلب هذا الموقع حق الوصول لعرض عنوان حسابك الحالي. تأكد دائماً من ثقتك في المواقع التي تتفاعل معها." - }, "about": { "message": "حول" }, @@ -252,7 +240,7 @@ "connect": { "message": "اتصال" }, - "connectRequest": { + "permissionsRequest": { "message": "طلب اتصال" }, "connectingTo": { @@ -375,9 +363,6 @@ "directDepositEtherExplainer": { "message": "إذا كان لديك بالفعل بعض الأثير، فإن أسرع طريقة للحصول على الأثير في محفظتك الجديدة عن طريق الإيداع المباشر." }, - "dismiss": { - "message": "رفض" - }, "done": { "message": "تم" }, @@ -1340,7 +1325,7 @@ "updatedWithDate": { "message": "تم تحديث $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "تتطلب الروابط بادئة HTTP/HTTPS مناسبة." }, "usedByClients": { diff --git a/app/_locales/bg/messages.json b/app/_locales/bg/messages.json index d5ca6f5cb..f54e27130 100644 --- a/app/_locales/bg/messages.json +++ b/app/_locales/bg/messages.json @@ -1,33 +1,21 @@ { - "privacyModeDefault": { - "message": "Режимът на поверителност вече е активиран по подразбиране" - }, "chartOnlyAvailableEth": { "message": "Диаграмата е достъпна само в мрежи на Ethereum." }, - "confirmClear": { - "message": "Сигурни ли сте, че искате да изчистите одобрените уебсайтове?" - }, "contractInteraction": { "message": "Взаимодействие с договор" }, - "clearApprovalData": { + "clearPermissions": { "message": "Изчистване на данните за поверителност" }, "reject": { "message": "Отхвърляне" }, - "providerRequest": { - "message": "$1 би искал да се свърже с вашия акаунт" - }, - "providerRequestInfo": { - "message": "Този сайт иска достъп за преглед на адреса на текущия ви акаунт. Винаги се уверявайте, че се доверявате на сайтовете, с които взаимодействате." - }, "about": { "message": "Информация" }, "aboutSettingsDescription": { - "message": "Версия, център за поддръжка и информация за контакт." + "message": "Версия, център за поддръжка и информация за контакт" }, "acceleratingATransaction": { "message": "* Ускоряването на транзакция чрез използване на по-висока цена на газа увеличава шансовете й да се обработва по-бързо от мрежата, но това не винаги е гарантирано." @@ -63,7 +51,7 @@ "message": "Разширени" }, "advancedSettingsDescription": { - "message": "Достъп до функции за разработчици, изтегляйте дневници, нулиране на акаунта, тестови мрежи за настройка и персонализиран RPC." + "message": "Достъп до функции за разработчици, изтегляйте дневници, нулиране на акаунта, тестови мрежи за настройка и персонализиран RPC" }, "advancedOptions": { "message": "Разширени опции" @@ -252,7 +240,7 @@ "connect": { "message": "Свързване" }, - "connectRequest": { + "permissionsRequest": { "message": "Свържете заявка" }, "connectingTo": { @@ -375,9 +363,6 @@ "directDepositEtherExplainer": { "message": "Ако вече имате някакъв етер, най-бързият начин да получите етер в новия си портфейл е чрез директен депозит." }, - "dismiss": { - "message": "Отхвърляне" - }, "done": { "message": "Готово" }, @@ -1343,7 +1328,7 @@ "updatedWithDate": { "message": "Актуализирано $1 " }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URI изискват съответния HTTP / HTTPS префикс." }, "usedByClients": { diff --git a/app/_locales/bn/messages.json b/app/_locales/bn/messages.json index 3e7e44e2f..326e2da7b 100644 --- a/app/_locales/bn/messages.json +++ b/app/_locales/bn/messages.json @@ -1,28 +1,16 @@ { - "privacyModeDefault": { - "message": "গোপনীয়তার মোড এখন ডিফল্ট হিসাবে সক্রিয় করা আছে" - }, "chartOnlyAvailableEth": { "message": "শুধুমাত্র Ethereum নেটওয়ার্কগুলিতে চার্ট উপলভ্য। " }, - "confirmClear": { - "message": "আপনি কি অনুমোদিত ওয়েবসাইটগুলি মুছে পরিস্কার করার বিষয়ে নিশ্চিত?" - }, "contractInteraction": { "message": "কন্ট্র্যাক্ট বাক্যালাপ" }, - "clearApprovalData": { + "clearPermissions": { "message": "গোপনীয়তার ডেটা মুছে পরিস্কার করুন" }, "reject": { "message": "প্রত্যাখ্যান" }, - "providerRequest": { - "message": "$1 আপনার অ্যাকাউন্টের সাথে সংযোগ করতে চায়" - }, - "providerRequestInfo": { - "message": "এই সাইটটি আপনার বর্তমান অ্যাকাউন্টের ঠিকানা দেখার অ্যাক্সেসের জন্য অনুরোধ জানাচ্ছে। সবসময় নিশ্চিত হয়ে নেবেন যে আপনি যে সাইটের সাথে যোগাযোগ করছেন সেটি বিশ্বাসযোগ্য কিনা।" - }, "about": { "message": "সম্পর্কে" }, @@ -252,7 +240,7 @@ "connect": { "message": "সংযুক্ত করুন" }, - "connectRequest": { + "permissionsRequest": { "message": "সংযোগের অনুরোধ" }, "connectingTo": { @@ -375,9 +363,6 @@ "directDepositEtherExplainer": { "message": "আপনার ইতিমধ্যে কিছু ইথার থেকে থাকলে আপনার নতুন ওয়ালেটে ইথার পাওয়ার দ্রুততম উপায় হল সরাসরি জমা করা।" }, - "dismiss": { - "message": "খারিজ" - }, "done": { "message": "সম্পন্ন " }, @@ -1347,7 +1332,7 @@ "updatedWithDate": { "message": "আপডেট করা $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URI গুলির যথাযথ HTTP/HTTPS প্রেফিক্সের প্রয়োজন।" }, "usedByClients": { diff --git a/app/_locales/ca/messages.json b/app/_locales/ca/messages.json index a31e42357..f5fb5129e 100644 --- a/app/_locales/ca/messages.json +++ b/app/_locales/ca/messages.json @@ -1,33 +1,21 @@ { - "privacyModeDefault": { - "message": "El mode de privacitat ara està activat per defecte" - }, "chartOnlyAvailableEth": { "message": "Mostra només els disponibles a les xarxes Ethereum." }, - "confirmClear": { - "message": "Estàs segur que vols eliminar totes les pàgines web aprovades?" - }, "contractInteraction": { "message": "Contractar Interacció" }, - "clearApprovalData": { + "clearPermissions": { "message": "Elimina les dades de privacitat" }, "reject": { "message": "Rebutja" }, - "providerRequest": { - "message": "a $1 li agradaria connectar-se al teu compte" - }, - "providerRequestInfo": { - "message": "Aquesta pàgina està demanant accès a la teva adreça" - }, "about": { "message": "Informació" }, "aboutSettingsDescription": { - "message": "Versió, centre de suport, i informació de contacte." + "message": "Versió, centre de suport, i informació de contacte" }, "acceleratingATransaction": { "message": "* Accelerar una transacció utilitzant un preu de gas més alt augmenta les possibilitats de ser processat més ràpidament per la xarxa, però no sempre es pot garantir." @@ -63,7 +51,7 @@ "message": "Configuració avançada" }, "advancedSettingsDescription": { - "message": "Accedeix a característiques de desenvolupador, descarrega Registres d'Estat, Reinicia el Compte, instal·la testnets i personalitza RPC." + "message": "Accedeix a característiques de desenvolupador, descarrega Registres d'Estat, Reinicia el Compte, instal·la testnets i personalitza RPC" }, "advancedOptions": { "message": "Opcions Avançades" @@ -249,7 +237,7 @@ "connect": { "message": "Connecta" }, - "connectRequest": { + "permissionsRequest": { "message": "Sol·licitud de connexió" }, "connectingTo": { @@ -372,9 +360,6 @@ "directDepositEtherExplainer": { "message": "Si ja tens una mica d'Ether, la manera més ràpida de posar Ether al teu nou moneder és per dipòsit directe." }, - "dismiss": { - "message": "Omet" - }, "done": { "message": "Fet" }, @@ -1316,7 +1301,7 @@ "updatedWithDate": { "message": "Actualitzat $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "Els URIs requereixen el prefix HTTP/HTTPS apropiat." }, "usedByClients": { diff --git a/app/_locales/cs/messages.json b/app/_locales/cs/messages.json index 9f645afa4..11fcddd10 100644 --- a/app/_locales/cs/messages.json +++ b/app/_locales/cs/messages.json @@ -1,10 +1,4 @@ { - "confirmClear": { - "message": "Naozaj chcete vymazať schválené webové stránky?" - }, - "clearPermissionsSuccess": { - "message": "Schválené údaje webových stránek byly úspěšně zrušeny." - }, "permissionsSettings": { "message": "Údaje o schválení" }, @@ -17,9 +11,6 @@ "reject": { "message": "Odmítnout" }, - "providerRequestInfo": { - "message": "Níže uvedená doména se pokouší požádat o přístup k API Ethereum, aby mohla komunikovat s blokádou Ethereum. Před schválením přístupu Ethereum vždy zkontrolujte, zda jste na správném místě." - }, "account": { "message": "Účet" }, @@ -538,7 +529,7 @@ "unknownNetwork": { "message": "Neznámá soukromá síť" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URI vyžadují korektní HTTP/HTTPS prefix." }, "usedByClients": { diff --git a/app/_locales/da/messages.json b/app/_locales/da/messages.json index 1c05509c8..b7e7238fe 100644 --- a/app/_locales/da/messages.json +++ b/app/_locales/da/messages.json @@ -1,33 +1,21 @@ { - "privacyModeDefault": { - "message": "Privatlivstilstand er nu som udgangspunkt aktiveret" - }, "chartOnlyAvailableEth": { "message": "Skema kun tilgængeligt på Ethereum-netværk." }, - "confirmClear": { - "message": "Er du sikker på, at du vil rydde godkendte hjemmesider?" - }, "contractInteraction": { "message": "Kontraktinteraktion" }, - "clearApprovalData": { + "clearPermissions": { "message": "Ryd fortrolighedsdata" }, "reject": { "message": "Afvis" }, - "providerRequest": { - "message": "$1 ønsker at forbinde til din konto" - }, - "providerRequestInfo": { - "message": "Denne side anmoder om at se din nuværende kontoadresse. Sørg altid for, at du stoler på de sider du interagerer med." - }, "about": { "message": "Om" }, "aboutSettingsDescription": { - "message": "Version, supportcenter og kontaktinformation." + "message": "Version, supportcenter og kontaktinformation" }, "acceleratingATransaction": { "message": "* At gøre din transaktion hurtigere ved at bruge en højere Gas-priser, øger dennes chancer for at blive behandlet af netværket hurtigere, men det er ikke altid garanteret." @@ -63,7 +51,7 @@ "message": "Avanceret" }, "advancedSettingsDescription": { - "message": "Få adgang til udviklerfunktioner, hent tilstandslogs, nulstil konto, opsæt testnetværk og brugerdefineret RPC." + "message": "Få adgang til udviklerfunktioner, hent tilstandslogs, nulstil konto, opsæt testnetværk og brugerdefineret RPC" }, "advancedOptions": { "message": "Avancerede Valgmuligheder" @@ -252,7 +240,7 @@ "connect": { "message": "Få forbindelse" }, - "connectRequest": { + "permissionsRequest": { "message": "Tilslutningsanmodning" }, "connectingTo": { @@ -375,9 +363,6 @@ "directDepositEtherExplainer": { "message": "Hvis du allerede har Ether, er den hurtigste måde at få Ether i din nye tegnebog ved direkte indbetaling." }, - "dismiss": { - "message": "Luk" - }, "done": { "message": "Færdig" }, @@ -1313,7 +1298,7 @@ "updatedWithDate": { "message": "Opdaterede $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "Links kræver det rette HTTP/HTTPS-præfix." }, "usedByClients": { diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 2a1540e05..bd47b218b 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -1,19 +1,10 @@ { - "privacyModeDefault": { - "message": "Der Datenschutzmodus ist jetzt standardmäßig aktiviert" - }, "chartOnlyAvailableEth": { "message": "Die Grafik ist nur in Ethereum-Netzwerken verfügbar." }, - "confirmClear": { - "message": "Möchten Sie die genehmigten Websites wirklich löschen?" - }, "contractInteraction": { "message": "Vertragsinteraktion" }, - "clearPermissionsSuccess": { - "message": "Genehmigte Website-Daten wurden erfolgreich gelöscht." - }, "permissionsSettings": { "message": "Genehmigungsdaten" }, @@ -26,17 +17,11 @@ "reject": { "message": "Ablehnen" }, - "providerRequest": { - "message": "$1 möchte sich mit deinem Account verbinden" - }, - "providerRequestInfo": { - "message": "Diese Website fordert Zugriff auf Ihre aktuelle Kontoadresse. Stellen Sie immer sicher, dass Sie den Websites vertrauen, mit denen Sie interagieren." - }, "about": { "message": "Über" }, "aboutSettingsDescription": { - "message": "Version, Supportcenter und Kontaktinformationen." + "message": "Version, Supportcenter und Kontaktinformationen" }, "acceleratingATransaction": { "message": "* Die Beschleunigung einer Transaktion durch die Verwendung eines höheren Gaspreises erhöht die Chancen einer schnelleren Verarbeitung durch das Netz, wofür es allerdings keine Garantie gibt." @@ -69,7 +54,7 @@ "message": "Erweitert" }, "advancedSettingsDescription": { - "message": "Zugriff auf Entwicklerfunktionen, Download von Statusprotokollen, Zurücksetzen des Kontos, Einrichten von Testnetzen und benutzerdefinierten RPCs." + "message": "Zugriff auf Entwicklerfunktionen, Download von Statusprotokollen, Zurücksetzen des Kontos, Einrichten von Testnetzen und benutzerdefinierten RPCs" }, "advancedOptions": { "message": "Erweiterte Optionen" @@ -249,7 +234,7 @@ "connect": { "message": "Verbinden" }, - "connectRequest": { + "permissionsRequest": { "message": "Verbindungsanfrage" }, "connectingTo": { @@ -369,9 +354,6 @@ "directDepositEtherExplainer": { "message": "Wenn du bereits Ether besitzt, ist die sofortige Einzahlung die schnellste Methode Ether in deine neue Wallet zu bekommen." }, - "dismiss": { - "message": "Schließen" - }, "done": { "message": "Fertig" }, @@ -1313,7 +1295,7 @@ "updatedWithDate": { "message": "$1 aktualisiert" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URIs benötigen die korrekten HTTP/HTTPS Präfixe." }, "usedByClients": { diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 860c5a563..e64c33d1c 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -1,33 +1,21 @@ { - "privacyModeDefault": { - "message": "Η Λειτουργία Απορρήτου είναι πλέον ενεργοποιημένη από προεπιλογή" - }, "chartOnlyAvailableEth": { "message": "Το διάγραμμα είναι διαθέσιμο μόνο σε δίκτυα Ethereum." }, - "confirmClear": { - "message": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τους εγκεκριμένους ιστότοπους;" - }, "contractInteraction": { "message": "Αλληλεπίδραση Σύμβασης" }, - "clearApprovalData": { + "clearPermissions": { "message": "Εκκαθάριση Δεδομένων Απορρήτου" }, "reject": { "message": "Απόρριψη" }, - "providerRequest": { - "message": "Αίτημα σύνδεσης στον λογαριασμό σας από $1" - }, - "providerRequestInfo": { - "message": "Ο ιστότοπος ζητά πρόσβαση για προβολή της τρέχουσας διεύθυνσης του λογαριασμού σας. Να σιγουρεύεστε πάντα ότι εμπιστεύεστε τους ιστότοπους με τους οποίους αλληλεπιδράτε." - }, "about": { "message": "Σχετικά με" }, "aboutSettingsDescription": { - "message": "Έκδοση, κέντρο υποστήριξης και πληροφορίες επικοινωνίας." + "message": "Έκδοση, κέντρο υποστήριξης και πληροφορίες επικοινωνίας" }, "acceleratingATransaction": { "message": "* Η επιτάχυνση μιας συναλλαγής με τη χρήση υψηλότερης τιμής καυσίμου αυξάνει τις πιθανότητές της για ταχύτερη επεξεργασία από το δίκτυο, αλλά δεν είναι πάντοτε εγγυημένη." @@ -63,7 +51,7 @@ "message": "Σύνθετες" }, "advancedSettingsDescription": { - "message": "Αποκτήστε πρόσβαση στις λειτουργίες του προγραμματιστή, κατεβάστε Αρχεία Καταγραφών Καταστάσεων, Επαναφέρετε τον Λογαριασμό, εγκαταστήστε δοκιμαστικά δίκτυα και προσαρμοσμένα RPC." + "message": "Αποκτήστε πρόσβαση στις λειτουργίες του προγραμματιστή, κατεβάστε Αρχεία Καταγραφών Καταστάσεων, Επαναφέρετε τον Λογαριασμό, εγκαταστήστε δοκιμαστικά δίκτυα και προσαρμοσμένα RPC" }, "advancedOptions": { "message": "Σύνθετες Επιλογές" @@ -249,7 +237,7 @@ "connect": { "message": "Σύνδεση" }, - "connectRequest": { + "permissionsRequest": { "message": "Αίτημα Σύνδεσης" }, "connectingTo": { @@ -372,9 +360,6 @@ "directDepositEtherExplainer": { "message": "Αν έχετε ήδη κάποια Ether, ο πιο γρήγορος τρόπος για να πάρετε τα Ether στο νέο σας πορτοφόλι με άμεση κατάθεση." }, - "dismiss": { - "message": "Παράβλεψη" - }, "done": { "message": "Τέλος" }, @@ -1341,7 +1326,7 @@ "updatedWithDate": { "message": "Ενημερώθηκε το $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "Τα URI απαιτούν το κατάλληλο πρόθεμα HTTP/HTTPS." }, "usedByClients": { diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index ca7b111a6..227009008 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1,40 +1,22 @@ { + "migrateSai": { + "message": "A message from Maker: The new Multi-Collateral Dai token has been released. Your old tokens are now called Sai. Please upgrade your Sai tokens to the new Dai." + }, + "migrateSaiInfo": { + "message": "To dismiss this notification you can migrate your tokens or hide SAI from the token list." + }, + "migrate": { + "message": "Migrate" + }, "showIncomingTransactions": { "message": "Show Incoming Transactions" }, "showIncomingTransactionsDescription": { "message": "Select this to use Etherscan to show incoming transactions in the transactions list" }, - "caveat_filterParams": { - "message": "Only with: " - }, - "caveat_filterResponse": { - "message": "Only: " - }, "chartOnlyAvailableEth": { "message": "Chart only available on Ethereum networks." }, - "connections": { - "message": "Connections" - }, - "connectionsSettingsDescription": { - "message": "Sites allowed to read your accounts" - }, - "addSite": { - "message": "Add Site" - }, - "addSiteDescription": { - "message": "Manually add a site to allow it access to your accounts, useful for older dapps" - }, - "connected": { - "message": "Connected" - }, - "connectedDescription": { - "message": "The list of sites allowed access to your addresses" - }, - "privacyModeDefault": { - "message": "Privacy Mode is now enabled by default" - }, "contractInteraction": { "message": "Contract Interaction" }, @@ -50,15 +32,9 @@ "clearPlugins": { "message": "Clear Snaps" }, - "clearPluginsDescription": { - "message": "Remove all Snaps and their associated permissions." - }, "confirmClearPlugins": { "message": "Are you sure you want to remove all Snaps and their associated permissions?" }, - "clearPluginsSuccess": { - "message": "Snaps cleared successfully." - }, "pluginsSettings": { "message": "Manage Snaps" }, @@ -74,27 +50,15 @@ "permissionsDescription": { "message": "All currently granted permissions, by dapp/website. Deselect permissions and click \"Update Permissions\" to remove them." }, - "permissions": { - "message": "Permissions" - }, - "permissionsSettingsDescription": { - "message": "Manage dapp/website permissions" - }, "permissionsEmpty": { "message": "No permissions found." }, "clearPermissions": { "message": "Clear Permissions" }, - "clearPermissionsDescription": { - "message": "Clear permissions so that all dapps/websites must request access again." - }, "confirmClearPermissions": { "message": "Are you sure you want to remove all dapp/website permissions?" }, - "clearPermissionsSuccess": { - "message": "Permissions cleared successfully." - }, "updatePermissions": { "message": "Update Permissions" }, @@ -110,9 +74,6 @@ "clearPermissionsActivity": { "message": "Clear Permissions Activity" }, - "clearPermissionsActivityDescription": { - "message": "Clear the permissions activity log." - }, "confirmClearPermissionsActivity": { "message": "Are you sure you want to clear the permissions activity log?" }, @@ -128,30 +89,25 @@ "clearPermissionsHistory": { "message": "Clear Permissions History" }, - "clearPermissionsHistoryDescription": { - "message": "Clear the permissions history." - }, "confirmClearPermissionsHistory": { "message": "Are you sure you want to clear the permissions history?" }, "reject": { "message": "Reject" }, - "providerRequest": { - "message": "$1 would like to connect to your account" - }, - "providerRequestInfo": { - "message": "This site is requesting access to view your current account address. Always make sure you trust the sites you interact with." - }, "about": { "message": "About" }, "aboutSettingsDescription": { - "message": "Version, support center, and contact info." + "message": "Version, support center, and contact info" }, "acceleratingATransaction": { "message": "* Accelerating a transaction by using a higher gas price increases its chances of getting processed by the network faster, but it is not always guaranteed." }, + "accessAndSpendNotice": { + "message": "$1 may access and spend up to this max amount", + "description": "$1 is the url of the site requesting ability to spend" + }, "accessingYourCamera": { "message": "Accessing your camera..." }, @@ -183,7 +139,7 @@ "message": "Advanced" }, "advancedSettingsDescription": { - "message": "Access developer features, download State Logs, Reset Account, setup testnets and custom RPC." + "message": "Access developer features, download State Logs, Reset Account, setup testnets and custom RPC" }, "advancedOptions": { "message": "Advanced Options" @@ -209,9 +165,20 @@ "addAcquiredTokens": { "message": "Add the tokens you've acquired using MetaMask" }, + "allowOriginSpendToken": { + "message": "Allow $1 to spend your $2?", + "description": "$1 is the url of the site and $2 is the symbol of the token they are requesting to spend" + }, + "allowWithdrawAndSpend": { + "message": "Allow $1 to withdraw and spend up to the following amount:", + "description": "The url of the site that requested permission to 'withdraw and spend'" + }, "amount": { "message": "Amount" }, + "amountWithColon": { + "message": "Amount:" + }, "appDescription": { "message": "An Ethereum Wallet in your Browser", "description": "The description of the application" @@ -291,6 +258,15 @@ "blockiesIdenticon": { "message": "Use Blockies Identicon" }, + "nonceField": { + "message": "Customize transaction nonce" + }, + "nonceFieldHeading": { + "message": "Custom Nonce" + }, + "nonceFieldDescription": { + "message": "Turn this on to change the nonce (transaction number) on confirmation screens. This is an advanced feature, use cautiously." + }, "browserNotSupported": { "message": "Your Browser is not supported..." }, @@ -471,6 +447,9 @@ "customRPC": { "message": "Custom RPC" }, + "customSpendLimit": { + "message": "Custom Spend Limit" + }, "dataBackupFoundInfo": { "message": "Some of your account data was backed up during a previous installation of MetaMask. This could include your settings, contacts and tokens. Would you like to restore this data now?" }, @@ -504,9 +483,6 @@ "directDepositEtherExplainer": { "message": "If you already have some Ether, the quickest way to get Ether in your new wallet by direct deposit." }, - "dismiss": { - "message": "Dismiss" - }, "done": { "message": "Done" }, @@ -531,6 +507,9 @@ "editContact": { "message": "Edit Contact" }, + "editPermission": { + "message": "Edit Permission" + }, "emailUs": { "message": "Email us!" }, @@ -564,6 +543,10 @@ "endOfFlowMessage10": { "message": "All Done" }, + "onboardingReturnNotice": { + "message": "\"$1\" will close this tab and direct back to $2", + "description": "Return the user to the site that initiated onboarding" + }, "ensRegistrationError": { "message": "Error in ENS name registration" }, @@ -573,6 +556,9 @@ "enterAnAlias": { "message": "Enter an alias" }, + "enterMaxSpendLimit": { + "message": "Enter Max Spend Limit" + }, "enterPassword": { "message": "Enter password" }, @@ -603,6 +589,9 @@ "faster": { "message": "Faster" }, + "feeAssociatedRequest": { + "message": "A fee is associated with this request." + }, "fiat": { "message": "Fiat", "description": "Exchange type" @@ -620,6 +609,9 @@ "fromShapeShift": { "message": "From ShapeShift" }, + "functionApprove": { + "message": "Function: Approve" + }, "functionType": { "message": "Function Type" }, @@ -828,6 +820,9 @@ "logout": { "message": "Log out" }, + "logoutTimeTooGreat": { + "message": "Logout time is too great" + }, "mainnet": { "message": "Main Ethereum Network" }, @@ -947,6 +942,10 @@ "next": { "message": "Next" }, + "nextNonceWarning": { + "message": "Nonce is higher than suggested nonce of $1", + "description": "The next nonce according to MetaMask's internal logic" + }, "noAddressForName": { "message": "No address has been set for this name." }, @@ -1008,6 +1007,12 @@ "pending": { "message": "pending" }, + "permissions": { + "message": "Permissions" + }, + "permissionsSettingsDescription": { + "message": "Manage dapp/website permissions." + }, "personalAddressDetected": { "message": "Personal address detected. Input the token contract address." }, @@ -1033,6 +1038,9 @@ "privateNetwork": { "message": "Private Network" }, + "proposedApprovalLimit": { + "message": "Proposed Approval Limit" + }, "qrCode": { "message": "Show QR Code" }, @@ -1268,6 +1276,9 @@ "signatureRequest": { "message": "Signature Request" }, + "signatureRequest1": { + "message": "Message" + }, "signed": { "message": "Signed" }, @@ -1289,9 +1300,19 @@ "speedUpTransaction": { "message": "Speed up this transaction" }, + "spendLimitPermission": { + "message": "Spend limit permission" + }, + "spendLimitRequestedBy": { + "message": "Spend limit requested by $1", + "description": "Origin of the site requesting the spend limit" + }, "switchNetworks": { "message": "Switch Networks" }, + "stateLogFileName": { + "message": "MetaMask State Logs" + }, "stateLogs": { "message": "State Logs" }, @@ -1338,10 +1359,10 @@ "message": "Symbol must be between 0 and 12 characters." }, "syncWithThreeBox": { - "message": "Sync data with 3Box" + "message": "Sync data with 3Box (experimental)" }, "syncWithThreeBoxDescription": { - "message": "Turn on to have your settings backed up with 3Box" + "message": "Turn on to have your settings backed up with 3Box. This feature is currenty experimental; use at your own risk." }, "syncWithThreeBoxDisabled": { "message": "3Box has been disabled due to an error during the initial sync" @@ -1382,6 +1403,9 @@ "to": { "message": "To" }, + "toWithColon": { + "message": "To:" + }, "toETHviaShapeShift": { "message": "$1 to ETH via ShapeShift", "description": "system will fill in deposit type in start of message" @@ -1456,6 +1480,10 @@ "message": "We had trouble loading your token balances. You can view them ", "description": "Followed by a link (here) to view token balances" }, + "trustSiteApprovePermission": { + "message": "Do you trust this site? By granting this permission, you’re allowing $1 to withdraw your $2 and automate transactions for you.", + "description": "$1 is the url requesting permission and $2 is the symbol of the currency that the request is for" + }, "tryAgain": { "message": "Try again" }, @@ -1483,6 +1511,9 @@ "unknownCameraError": { "message": "There was an error while trying to access your camera. Please try again..." }, + "unlimited": { + "message": "Unlimited" + }, "unlock": { "message": "Unlock" }, @@ -1492,8 +1523,11 @@ "updatedWithDate": { "message": "Updated $1" }, - "uriErrorMsg": { - "message": "URIs require the appropriate HTTP/HTTPS prefix." + "urlErrorMsg": { + "message": "URLs require the appropriate HTTP/HTTPS prefix." + }, + "urlExistsErrorMsg": { + "message": "URL is already present in existing list of networks" }, "usedByClients": { "message": "Used by a variety of different clients" @@ -1519,6 +1553,9 @@ "viewOnEtherscan": { "message": "View on Etherscan" }, + "retryTransaction": { + "message": "Retry Transaction" + }, "visitWebSite": { "message": "Visit our web site" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index edf6d5688..f9210db22 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -1,19 +1,10 @@ { - "privacyModeDefault": { - "message": "Modo Privado está activado ahora por defecto" - }, "chartOnlyAvailableEth": { "message": "Tabla solo disponible en redes Ethereum." }, - "confirmClear": { - "message": "¿Seguro que quieres borrar los sitios web aprobados?" - }, "contractInteraction": { "message": "Interacción con contrato" }, - "clearPermissionsSuccess": { - "message": "Los datos aprobados del sitio web se borraron con éxito." - }, "permissionsSettings": { "message": "Datos de aprobación" }, @@ -26,12 +17,6 @@ "reject": { "message": "Rechazar" }, - "providerRequest": { - "message": "$1 quisiera conectar con tu cuenta" - }, - "providerRequestInfo": { - "message": "El dominio que se muestra a continuación intenta solicitar acceso a la API Ethereum para que pueda interactuar con la blockchain de Ethereum. Siempre verifique que esté en el sitio correcto antes de aprobar el acceso Ethereum." - }, "about": { "message": "Acerca" }, @@ -218,7 +203,7 @@ "connect": { "message": "Conectar" }, - "connectRequest": { + "permissionsRequest": { "message": "Petición para conectar" }, "connectingTo": { @@ -341,9 +326,6 @@ "directDepositEtherExplainer": { "message": "Si posees Ether, la forma más rápida de transferirlo a tu nueva billetera es depositándolo directamente" }, - "dismiss": { - "message": "Descartar" - }, "done": { "message": "Completo" }, @@ -1100,7 +1082,7 @@ "updatedWithDate": { "message": "Actualizado $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URI necesita el prefijo HTTP/HTTPS apropiado" }, "usedByClients": { diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index 244eff885..e0e869cb2 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -1,33 +1,21 @@ { - "privacyModeDefault": { - "message": "El modo de privacidad está habilitado de manera predeterminada" - }, "chartOnlyAvailableEth": { "message": "Chart está disponible únicamente en las redes de Ethereum." }, - "confirmClear": { - "message": "¿Estás seguro de que deseas borrar los sitios web aprobados?" - }, "contractInteraction": { "message": "Interacción contractual" }, - "clearApprovalData": { + "clearPermissions": { "message": "Borrar datos de privacidad" }, "reject": { "message": "Rechazar" }, - "providerRequest": { - "message": "$1 desea conectarse a tu cuenta" - }, - "providerRequestInfo": { - "message": "Este sitio está solicitando acceso para ver la dirección de tu cuenta corriente. Asegúrate siempre de que confías en los sitios con los que interactúas." - }, "about": { "message": "Acerca de" }, "aboutSettingsDescription": { - "message": "Versión, centro de soporte técnico e información de contacto." + "message": "Versión, centro de soporte técnico e información de contacto" }, "acceleratingATransaction": { "message": "* Aumentar una transacción utilizando un precio de gas más alto aumenta sus posibilidades de ser procesada más rápido por la red, pero esto no siempre está garantizado." @@ -63,7 +51,7 @@ "message": "Avanzada" }, "advancedSettingsDescription": { - "message": "Accede a las funciones de desarrollador, descarga los registros de estado, restablece la cuenta, y configura las redes Testnet y el RPC personalizado." + "message": "Accede a las funciones de desarrollador, descarga los registros de estado, restablece la cuenta, y configura las redes Testnet y el RPC personalizado" }, "advancedOptions": { "message": "Opciones avanzadas" @@ -249,7 +237,7 @@ "connect": { "message": "Conectar" }, - "connectRequest": { + "permissionsRequest": { "message": "Solicitud de conexión" }, "connectingTo": { @@ -372,9 +360,6 @@ "directDepositEtherExplainer": { "message": "Si ya tienes algunos Ethers, la forma más rápida de ingresarlos en tu nueva billetera es a través de un depósito directo." }, - "dismiss": { - "message": "Rechazar" - }, "done": { "message": "Listo" }, @@ -1326,7 +1311,7 @@ "updatedWithDate": { "message": "Actualización: $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "Los URI deben tener el prefijo HTTP/HTTPS apropiado." }, "usedByClients": { diff --git a/app/_locales/et/messages.json b/app/_locales/et/messages.json index 79bc3ebd4..962cb4a1b 100644 --- a/app/_locales/et/messages.json +++ b/app/_locales/et/messages.json @@ -1,33 +1,21 @@ { - "privacyModeDefault": { - "message": "Privaatsusrežiim on nüüd vaikimisi lubatud" - }, "chartOnlyAvailableEth": { "message": "Tabel on saadaval vaid Ethereumi võrkudes." }, - "confirmClear": { - "message": "Kas soovite kindlasti kinnitatud veebisaidid kustutada?" - }, "contractInteraction": { "message": "Lepingu suhtlus" }, - "clearApprovalData": { + "clearPermissions": { "message": "Tühjenda privaatsusandmed" }, "reject": { "message": "Lükka tagasi" }, - "providerRequest": { - "message": "$1 soovib teie kontoga ühenduse luua" - }, - "providerRequestInfo": { - "message": "See sait taotleb juurdepääsu teie praeguse konto aadressi vaatamiseks. Veenduge alati, et usaldate saite, millega suhtlete." - }, "about": { "message": "Teave" }, "aboutSettingsDescription": { - "message": "Versioon, tugikeskus ja kontaktteave." + "message": "Versioon, tugikeskus ja kontaktteave" }, "acceleratingATransaction": { "message": "* Tehingu kiirendamine kõrgemate gaasihindadega suurendab võimalust kiiremaks võrgus töötlemiseks, kuid see ei ole alati tagatud." @@ -63,7 +51,7 @@ "message": "Täpsemad" }, "advancedSettingsDescription": { - "message": "Juurdepääs arendaja funktsioonidele, olekulogide allalaadimine, konto lähtestamine, testvõrkude ja kohandatud RPC-de seadistamine." + "message": "Juurdepääs arendaja funktsioonidele, olekulogide allalaadimine, konto lähtestamine, testvõrkude ja kohandatud RPC-de seadistamine" }, "advancedOptions": { "message": "Täpsemad suvandid" @@ -252,7 +240,7 @@ "connect": { "message": "Ühendamine" }, - "connectRequest": { + "permissionsRequest": { "message": "Ühenduse taotlus" }, "connectingTo": { @@ -375,9 +363,6 @@ "directDepositEtherExplainer": { "message": "Kui teil on juba veidi eetrit, on kiirem viis eetri rahakotti saamiseks otsene sissemakse." }, - "dismiss": { - "message": "Loobu" - }, "done": { "message": "Valmis" }, @@ -1337,7 +1322,7 @@ "updatedWithDate": { "message": "Värskendatud $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URI-d nõuavad sobivat HTTP/HTTPS-i prefiksit." }, "usedByClients": { diff --git a/app/_locales/fa/messages.json b/app/_locales/fa/messages.json index 848341941..21824a466 100644 --- a/app/_locales/fa/messages.json +++ b/app/_locales/fa/messages.json @@ -1,28 +1,16 @@ { - "privacyModeDefault": { - "message": "وضعیت محرمیت حالا بصورت خودکار فعال است" - }, "chartOnlyAvailableEth": { "message": "تنها قابل دسترس را در شبکه های ایتریوم جدول بندی نمایید" }, - "confirmClear": { - "message": "آیا مطمئن هستید تا وب سایت های تصدیق شده را حذف کنید؟" - }, "contractInteraction": { "message": "تعامل قرارداد" }, - "clearApprovalData": { + "clearPermissions": { "message": "حذف اطلاعات حریم خصوصی" }, "reject": { "message": "عدم پذیرش" }, - "providerRequest": { - "message": "1$1 میخواهید تا با حساب تان وصل شوید" - }, - "providerRequestInfo": { - "message": "این سایت در حال درخواست دسترسی است تا آدرس فعلی حساب تان را مشاهده نماید. همیشه متوجه باشید که بالای سایتی که با آن معامله میکنید، اعتماد دارید یا خیر." - }, "about": { "message": "درباره" }, @@ -252,7 +240,7 @@ "connect": { "message": "اتصال" }, - "connectRequest": { + "permissionsRequest": { "message": "درخواست اتصال" }, "connectingTo": { @@ -375,9 +363,6 @@ "directDepositEtherExplainer": { "message": "در صورتیکه شما کدام ایتر داشته باشید، سریعترین روش برای گرفتن ایتر در کیف جدید تان توسط پرداخت مستقیم." }, - "dismiss": { - "message": "لغو کردن" - }, "done": { "message": "تمام" }, @@ -1347,7 +1332,7 @@ "updatedWithDate": { "message": "بروزرسانی شد 1$1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URl ها نیازمند پیشوند مناسب HTTP/HTTPS اند." }, "usedByClients": { diff --git a/app/_locales/fi/messages.json b/app/_locales/fi/messages.json index a0a0ebfe2..0817e25ee 100644 --- a/app/_locales/fi/messages.json +++ b/app/_locales/fi/messages.json @@ -1,33 +1,21 @@ { - "privacyModeDefault": { - "message": "Yksityisyystila on nyt oletusarvoisesti käytössä" - }, "chartOnlyAvailableEth": { "message": "Kaavio saatavilla vain Ethereum-verkoissa." }, - "confirmClear": { - "message": "Haluatko varmasti tyhjentää hyväksytyt verkkosivustot?" - }, "contractInteraction": { "message": "Sopimustoiminta" }, - "clearApprovalData": { + "clearPermissions": { "message": "Tyhjennä yksityisyystiedot" }, "reject": { "message": "Hylkää" }, - "providerRequest": { - "message": "$1 haluaisi yhdistää tiliisi" - }, - "providerRequestInfo": { - "message": "Tämä sivusto pyytää oikeuksia nähdä nykyisen tiliosoitteesi. Varmista aina, että luotat sivustoihin, joiden kanssa toimit." - }, "about": { "message": "Tietoja asetuksista" }, "aboutSettingsDescription": { - "message": "Versio, tukikeskus ja yhteystiedot." + "message": "Versio, tukikeskus ja yhteystiedot" }, "acceleratingATransaction": { "message": "* Tapahtuman nopeuttaminen käyttämällä korkeampaa gas-hintaa parantaa mahdollisuutta, että verkko käsittelee sen nopeammin, mutta tämä ei ole aina taattua." @@ -63,7 +51,7 @@ "message": "Lisäasetukset" }, "advancedSettingsDescription": { - "message": "Käytä kehittäjän ominaisuuksia, lataa tilalokeja, palauta tilit, asenna testiverkostoja ja muokattavia RPC:itä." + "message": "Käytä kehittäjän ominaisuuksia, lataa tilalokeja, palauta tilit, asenna testiverkostoja ja muokattavia RPC:itä" }, "advancedOptions": { "message": "Tarkemmat vaihtoehdot" @@ -249,7 +237,7 @@ "connect": { "message": "Muodosta yhteys" }, - "connectRequest": { + "permissionsRequest": { "message": "Yhdistämispyyntö" }, "connectingTo": { @@ -372,9 +360,6 @@ "directDepositEtherExplainer": { "message": "Jos sinulla on jo etheriä, nopein tapa hankkia etheriä uuteen lompakkoosi on suoratalletus." }, - "dismiss": { - "message": "Piilota" - }, "done": { "message": "Valmis" }, @@ -1344,7 +1329,7 @@ "updatedWithDate": { "message": "$1 päivitetty" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URI:t vaativat asianmukaisen HTTP/HTTPS-etuliitteen." }, "usedByClients": { diff --git a/app/_locales/fil/messages.json b/app/_locales/fil/messages.json index b62df2909..711aeec7c 100644 --- a/app/_locales/fil/messages.json +++ b/app/_locales/fil/messages.json @@ -1,33 +1,21 @@ { - "privacyModeDefault": { - "message": "Naka-enable ang Privacy Mode bilang default" - }, "chartOnlyAvailableEth": { "message": "Available lang ang chart sa mga Ethereum network." }, - "confirmClear": { - "message": "Sigurado ka bang gusto mong i-clear ang mga inaprubahang website?" - }, "contractInteraction": { "message": "Paggamit sa Contract" }, - "clearApprovalData": { + "clearPermissions": { "message": "I-clear ang Privacy Data" }, "reject": { "message": "Tanggihan" }, - "providerRequest": { - "message": "Gusto ng $1 na kumonekta sa iyong account" - }, - "providerRequestInfo": { - "message": "Humihiling ng access ang site na ito na tingnan ang kasalukuyan mong account address. Palaging tiyaking pinagkakatiwalaan mo ang mga site na pinupuntahan mo." - }, "about": { "message": "Tungkol sa" }, "aboutSettingsDescription": { - "message": "Bersyon, support center, at impormasyon sa pakikipag-ugnayan." + "message": "Bersyon, support center, at impormasyon sa pakikipag-ugnayan" }, "acceleratingATransaction": { "message": "* Ang pagpapabilis sa isang transaksyon sa pamamagitan ng paggamit ng mas mataas na presyo ng gas ay makakadagdag sa tsansa nitong maproseso ng network nang mas mabilis, pero hindi ito palaging garantisado." @@ -57,7 +45,7 @@ "message": "Magdagdag ng Recipient" }, "advancedSettingsDescription": { - "message": "I-access ang mga feature para sa mga developer, mag-download ng mga State Log, I-reset ang Account, mag-set up ng mga testnet at custom RPC." + "message": "I-access ang mga feature para sa mga developer, mag-download ng mga State Log, I-reset ang Account, mag-set up ng mga testnet at custom RPC" }, "advancedOptions": { "message": "Mga Advanced na Opsyon" @@ -342,9 +330,6 @@ "directDepositEtherExplainer": { "message": "Kung mayroon ka nang Ether, ang pinakamabilis na paraan para magkaroon ng Ether sa iyong bagong wallet ay sa pamamagitan ng direkang deposito." }, - "dismiss": { - "message": "Balewalain" - }, "done": { "message": "Tapos na" }, @@ -1235,7 +1220,7 @@ "updatedWithDate": { "message": "Na-update ang $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "Kinakailangan ng mga URI ang naaangkop na HTTP/HTTPS prefix." }, "usedByClients": { diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index c44a057eb..8d58b8d69 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -1,19 +1,10 @@ { - "privacyModeDefault": { - "message": "Le mode conversation privée est maintenant activé par défaut" - }, "chartOnlyAvailableEth": { "message": "Tableau disponible uniquement sur les réseaux Ethereum." }, - "confirmClear": { - "message": "Êtes-vous sûr de vouloir supprimer les sites Web approuvés?" - }, "contractInteraction": { "message": "Interaction avec un contrat" }, - "clearPermissionsSuccess": { - "message": "Les données de site Web approuvées ont été supprimées." - }, "permissionsSettings": { "message": "Données d'approbation" }, @@ -26,17 +17,11 @@ "reject": { "message": "Rejeter" }, - "providerRequest": { - "message": "$1 voudrait se connecter à votre compte" - }, - "providerRequestInfo": { - "message": "Le domaine répertorié ci-dessous tente de demander l'accès à l'API Ethereum pour pouvoir interagir avec la chaîne de blocs Ethereum. Vérifiez toujours que vous êtes sur le bon site avant d'autoriser l'accès à Ethereum." - }, "about": { "message": "À propos" }, "aboutSettingsDescription": { - "message": "Version, centre d'assistance et coordonnées." + "message": "Version, centre d'assistance et coordonnées" }, "acceleratingATransaction": { "message": "* Accélérer une transaction en utilisant un prix de l'essence plus élevé augmente ses chances d'être traitée plus rapidement par le réseau, mais ce n'est pas toujours garanti." @@ -72,7 +57,7 @@ "message": "Paramètres avancés" }, "advancedSettingsDescription": { - "message": "Accédez aux fonctionnalités pour les développeurs, téléchargez State Logs, réinitialisez votre compte, configurez testnets et personnalisez RPC." + "message": "Accédez aux fonctionnalités pour les développeurs, téléchargez State Logs, réinitialisez votre compte, configurez testnets et personnalisez RPC" }, "advancedOptions": { "message": "Options avancées" @@ -249,7 +234,7 @@ "connect": { "message": "Connecter" }, - "connectRequest": { + "permissionsRequest": { "message": "Demande de connexion" }, "connectingTo": { @@ -372,9 +357,6 @@ "directDepositEtherExplainer": { "message": "Si vous avez déjà de l'Ether, le moyen le plus rapide d'obtenir des Ether dans votre nouveau portefeuille est par dépôt direct." }, - "dismiss": { - "message": "Ignorer" - }, "done": { "message": "Terminé" }, @@ -1317,7 +1299,7 @@ "updatedWithDate": { "message": "Mis à jour $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "Les URLs requièrent un préfixe HTTP/HTTPS approprié." }, "usedByClients": { diff --git a/app/_locales/gu/messages.json b/app/_locales/gu/messages.json index bcd467be1..92ab96b09 100644 --- a/app/_locales/gu/messages.json +++ b/app/_locales/gu/messages.json @@ -57,9 +57,6 @@ "details": { "message": "વિગતો" }, - "dismiss": { - "message": "કાઢી નાખો" - }, "done": { "message": "થઈ ગયું" }, diff --git a/app/_locales/he/messages.json b/app/_locales/he/messages.json index 64bf6a0a5..be1e762d2 100644 --- a/app/_locales/he/messages.json +++ b/app/_locales/he/messages.json @@ -1,28 +1,16 @@ { - "privacyModeDefault": { - "message": "מצב פרטיות זמין עכשיו כברירת מחדל" - }, "chartOnlyAvailableEth": { "message": "טבלה זמינה רק ברשתות אתריום." }, - "confirmClear": { - "message": "הנך בטוח/ה כי ברצונך למחוק אתרים שאושרו?" - }, "contractInteraction": { "message": "אינטראקציית חוזה" }, - "clearApprovalData": { + "clearPermissions": { "message": "נקה נתוני פרטיות" }, "reject": { "message": "דחה" }, - "providerRequest": { - "message": "$1 מבקש להתחבר לחשבון שלך" - }, - "providerRequestInfo": { - "message": "אתר זה מבקש גישה לצפייה בכתובת החשבון הנוכחית שלך. יש לוודא תמיד כי הנך בוטח/ת באתרים עמם הנך מתקשר/ת." - }, "about": { "message": "מידע כללי" }, @@ -252,7 +240,7 @@ "connect": { "message": "התחברות" }, - "connectRequest": { + "permissionsRequest": { "message": "חבר/י בקשה" }, "connectingTo": { @@ -375,9 +363,6 @@ "directDepositEtherExplainer": { "message": "אם כבר יש ברשותך את'ר (Ether) , הדרך המהירה ביותר להכניס את'ר לארנק החדש שלך היא באמצעות הפקדה ישירה." }, - "dismiss": { - "message": "סגור" - }, "done": { "message": "סיום" }, @@ -1341,7 +1326,7 @@ "updatedWithDate": { "message": "עודכן $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "כתובות URI דורשות את קידומת HTTP/HTTPS המתאימה." }, "usedByClients": { diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index f434c93b1..84485b68d 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -1,28 +1,16 @@ { - "privacyModeDefault": { - "message": "गोपनीय मोड अब डिफ़ॉल्ट रूप से सक्षम है" - }, "chartOnlyAvailableEth": { "message": "केवल ईथरअम नेटवर्क पर उपलब्ध चार्ट।" }, - "confirmClear": { - "message": "क्या आप वाकई स्वीकृत वेबसाइटों को क्लियर करना चाहते हैं?" - }, "contractInteraction": { "message": "कॉन्ट्रैक्ट की बातचीत" }, - "clearApprovalData": { + "clearPermissions": { "message": "गोपनीयता डेटा रिक्त करें" }, "reject": { "message": "अस्‍वीकार करें" }, - "providerRequest": { - "message": "$1 आपके खाते से कनेक्ट होता चाहता हैं" - }, - "providerRequestInfo": { - "message": "यह साइट आपके वर्तमान खाते का पता देखने के लिए एक्सेस का अनुरोध कर रही है। हमेशा सुनिश्चित करें कि आप जिन साइटों पर जाते हैं वे विश्वसनीय हैं।" - }, "about": { "message": "इसके बारे में" }, @@ -252,7 +240,7 @@ "connect": { "message": "कनेक्ट करें" }, - "connectRequest": { + "permissionsRequest": { "message": "संपर्क अनुरोध" }, "connectingTo": { @@ -375,9 +363,6 @@ "directDepositEtherExplainer": { "message": "यदि आपके पास पहले से ही कुछ Ether हैं, तो अपने नए वॉलेट में Ether पाने का सबसे तेज़ तरीका सीधे जमा करना है।" }, - "dismiss": { - "message": "खारिज करें" - }, "done": { "message": "पूर्ण" }, @@ -1341,7 +1326,7 @@ "updatedWithDate": { "message": "$1 अपडेट किया गया" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URI को उपयुक्त HTTP/HTTPS प्रीफ़िक्स की आवश्यकता होती है।" }, "usedByClients": { diff --git a/app/_locales/hn/messages.json b/app/_locales/hn/messages.json index 660aed6a6..eff1e37f0 100644 --- a/app/_locales/hn/messages.json +++ b/app/_locales/hn/messages.json @@ -1,10 +1,4 @@ { - "confirmClear": { - "message": "क्या आप वाकई अनुमोदित वेबसाइटों को साफ़ करना चाहते हैं?" - }, - "clearPermissionsSuccess": { - "message": "स्वीकृत वेबसाइट डेटा सफलतापूर्वक मंजूरी दे दी।" - }, "permissionsSettings": { "message": "स्वीकृति डेटा" }, @@ -20,9 +14,6 @@ "reject": { "message": "अस्वीकार" }, - "providerRequestInfo": { - "message": "नीचे सूचीबद्ध डोमेन वेब 3 एपीआई तक पहुंच का अनुरोध करने का प्रयास कर रहा है ताकि यह एथेरियम ब्लॉकचेन से बातचीत कर सके। वेब 3 एक्सेस को मंजूरी देने से पहले हमेशा सही जांच करें कि आप सही साइट पर हैं।" - }, "account": { "message": "खाता" }, @@ -500,7 +491,7 @@ "unknownNetwork": { "message": "अज्ञात निजी नेटवर्क" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URI-यूआरआई को उपयुक्त HTTP / HTTPS उपसर्ग की आवश्यकता होती है।" }, "usedByClients": { diff --git a/app/_locales/hr/messages.json b/app/_locales/hr/messages.json index f022f2f80..877a26d58 100644 --- a/app/_locales/hr/messages.json +++ b/app/_locales/hr/messages.json @@ -1,17 +1,11 @@ { - "privacyModeDefault": { - "message": "Način se Privatnost sada zadano omogućava" - }, "chartOnlyAvailableEth": { "message": "Grafikon je dostupan samo na mrežama Ethereum." }, - "confirmClear": { - "message": "Sigurno želite očistiti odobrena mrežna mjesta?" - }, "contractInteraction": { "message": "Ugovorna interakcija" }, - "clearApprovalData": { + "clearPermissions": { "message": "Očisti podatke o privatnosti" }, "appName": { @@ -21,17 +15,11 @@ "reject": { "message": "Odbaci" }, - "providerRequest": { - "message": "Korisnik $1 želi se povezati na vaš račun" - }, - "providerRequestInfo": { - "message": "Na ovom se mjestu zahtijeva pristup za pregledavanje vaše trenutačne adrese računa. Uvijek pazite da vjerujete mrežnim mjestima s kojima rukujete." - }, "about": { "message": "O opcijama" }, "aboutSettingsDescription": { - "message": "Inačica, centar za podršku i informacije za kontakt." + "message": "Inačica, centar za podršku i informacije za kontakt" }, "acceleratingATransaction": { "message": "* Ubrzavanjem se transakcije pomoću veće cijene goriva povećava šansa za bržu obradu mrežom, ali se uvijek ne jamči." @@ -67,7 +55,7 @@ "message": "Napredno" }, "advancedSettingsDescription": { - "message": "Pristup značajkama razvojnog inženjera, preuzimanje zapisnika stanja, poništavanje računa, postavljanje testnih mreža i prilagođeni RPC." + "message": "Pristup značajkama razvojnog inženjera, preuzimanje zapisnika stanja, poništavanje računa, postavljanje testnih mreža i prilagođeni RPC" }, "advancedOptions": { "message": "Napredne mogućnosti" @@ -252,7 +240,7 @@ "connect": { "message": "Povežite se" }, - "connectRequest": { + "permissionsRequest": { "message": "Zahtjev za povezivanjem" }, "connectingTo": { @@ -375,9 +363,6 @@ "directDepositEtherExplainer": { "message": "Ako imate nešto Ethera, najbrži je način prebacivanja Ethera u vaš novi novčanik izravan polog." }, - "dismiss": { - "message": "Odbaci" - }, "done": { "message": "Gotovo" }, @@ -1337,7 +1322,7 @@ "updatedWithDate": { "message": "Ažurirano $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URI-jevima se zahtijeva prikladan prefiks HTTP/HTTPS." }, "usedByClients": { diff --git a/app/_locales/ht/messages.json b/app/_locales/ht/messages.json index 8358ce5a1..040dfe784 100644 --- a/app/_locales/ht/messages.json +++ b/app/_locales/ht/messages.json @@ -1,10 +1,4 @@ { - "confirmClear": { - "message": "Èske ou sèten ou vle klè sitwèb apwouve?" - }, - "clearPermissionsSuccess": { - "message": "Done sou sit wèb apwouve yo te klarifye avèk siksè." - }, "permissionsSettings": { "message": "Done sou vi prive" }, @@ -14,9 +8,6 @@ "clearPermissions": { "message": "Klè Done sou vi prive" }, - "providerRequestInfo": { - "message": "Domèn ki nan lis anba a ap mande pou jwenn aksè a blòkchou Ethereum ak pou wè kont ou ye kounye a. Toujou double tcheke ke ou sou sit ki kòrèk la anvan apwouve aksè." - }, "accessingYourCamera": { "message": "Aksè a Kamera" }, @@ -860,7 +851,7 @@ "updatedWithDate": { "message": "Mete ajou $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URIs mande pou apwopriye prefiks HTTP / HTTPS a." }, "usedByClients": { diff --git a/app/_locales/hu/messages.json b/app/_locales/hu/messages.json index 15a03716a..c5386eecd 100644 --- a/app/_locales/hu/messages.json +++ b/app/_locales/hu/messages.json @@ -1,17 +1,11 @@ { - "privacyModeDefault": { - "message": "Az adatvédelmi mód mostantól alapbeállításként engedélyezve van" - }, "chartOnlyAvailableEth": { "message": "A diagram csak Ethereum hálózatokon érhető el" }, - "confirmClear": { - "message": "Biztosan törölni szeretnéd a jóváhagyott weboldalakat?" - }, "contractInteraction": { "message": "Szerződéses interakció" }, - "clearApprovalData": { + "clearPermissions": { "message": "Adatvédelmi adatok törlése" }, "appName": { @@ -21,17 +15,11 @@ "reject": { "message": "Elutasítás" }, - "providerRequest": { - "message": "$1 szeretne kapcsolódni az ön fiókjához" - }, - "providerRequestInfo": { - "message": "A webhely hozzáférést kér az ön jelenlegi fiókcímének megtekintéséhez. Mindig győződjön meg arról, hogy megbízható webhellyel létesít kapcsolatot." - }, "about": { "message": "Névjegy" }, "aboutSettingsDescription": { - "message": "Verzió, ügyfélszolgálat és elérhetőségek." + "message": "Verzió, ügyfélszolgálat és elérhetőségek" }, "acceleratingATransaction": { "message": "* Ha szeretné felgyorsítani a tranzakciót azzal, hogy magasabb gázárat használ, az növeli a gyorsabb feldolgozás esélyét, de ez nem mindig garantált." @@ -67,7 +55,7 @@ "message": "Speciális" }, "advancedSettingsDescription": { - "message": "Hozzáférés fejlesztői funkciókhoz, állapotnaplók letöltése, fiók újraállítása, testnetek és egyéni RPC-k beállítása." + "message": "Hozzáférés fejlesztői funkciókhoz, állapotnaplók letöltése, fiók újraállítása, testnetek és egyéni RPC-k beállítása" }, "advancedOptions": { "message": "Haladó beállítások" @@ -252,7 +240,7 @@ "connect": { "message": "Csatlakozás" }, - "connectRequest": { + "permissionsRequest": { "message": "Csatlakozási kérelem" }, "connectingTo": { @@ -375,9 +363,6 @@ "directDepositEtherExplainer": { "message": "Amennyiben már rendelkezik némi Ether-rel, a közvetlen letéttel gyorsan elhelyezheti azt új pénztárcájában." }, - "dismiss": { - "message": "Elvetés" - }, "done": { "message": "Kész" }, @@ -1337,7 +1322,7 @@ "updatedWithDate": { "message": "$1 frissítve" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "Az URI-hez szükség van a megfelelő HTTP/HTTPS előtagra." }, "usedByClients": { diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index f0d55ea8a..753c6a91d 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -1,17 +1,11 @@ { - "privacyModeDefault": { - "message": "Modus Privasi kini aktif secara default" - }, "chartOnlyAvailableEth": { "message": "Grafik hanya tersedia pada jaringan Ethereum." }, - "confirmClear": { - "message": "Yakin ingin mengosongkan website yang disetujui?" - }, "contractInteraction": { "message": "Interaksi Kontrak" }, - "clearApprovalData": { + "clearPermissions": { "message": "Bersihkan Data Privasi" }, "appName": { @@ -21,17 +15,11 @@ "reject": { "message": "Tolak" }, - "providerRequest": { - "message": "$1 ingin menghubungkan ke akun Anda" - }, - "providerRequestInfo": { - "message": "Situs ini meminta akses untuk melihat alamat akun Anda saat ini. Selalu pastikan bahwa Anda bisa mempercayai situs yang berinteraksi dengan Anda." - }, "about": { "message": "Tentang" }, "aboutSettingsDescription": { - "message": "Versi, pusat dukungan, dan informasi kontak." + "message": "Versi, pusat dukungan, dan informasi kontak" }, "acceleratingATransaction": { "message": "* Mempercepat transaksi dengan menggunakan harga gas yang lebih tinggi meningkatkan peluangnya untuk lebih cepat diproses oleh jaringan, tetapi tak selalu terjamin pasti cepat." @@ -67,7 +55,7 @@ "message": "Lanjutan" }, "advancedSettingsDescription": { - "message": "Akses fitur pengembang, unduh Log Status, Atur Ulang Akun, tata testnets dan RPC kustom." + "message": "Akses fitur pengembang, unduh Log Status, Atur Ulang Akun, tata testnets dan RPC kustom" }, "advancedOptions": { "message": "Opsi Lanjutan" @@ -252,7 +240,7 @@ "connect": { "message": "Sambungkan" }, - "connectRequest": { + "permissionsRequest": { "message": "Permintaan Sambungan" }, "connectingTo": { @@ -369,9 +357,6 @@ "directDepositEtherExplainer": { "message": "Jika Anda sudah memiliki Ether, cara tercepat mendapatkan Ether di dompet baru lewat deposit langsung." }, - "dismiss": { - "message": "Tutup" - }, "done": { "message": "Selesai" }, @@ -1316,7 +1301,7 @@ "updatedWithDate": { "message": "Diperbarui $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URI memerlukan awalan HTTP/HTTPS yang sesuai." }, "usedByClients": { diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index 639b67685..8513d2fa9 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -1,19 +1,10 @@ { - "privacyModeDefault": { - "message": "La modalità privacy è ora abilitata per impostazione predefinita" - }, "chartOnlyAvailableEth": { "message": "Grafico disponibile solo per le reti Ethereum." }, - "confirmClear": { - "message": "Sei sicuro di voler cancellare i siti Web approvati?" - }, "contractInteraction": { "message": "Interazione Contratto" }, - "clearPermissionsSuccess": { - "message": "Dati del sito Web approvati cancellati correttamente." - }, "permissionsSettings": { "message": "Dati di approvazione" }, @@ -26,17 +17,11 @@ "reject": { "message": "Annulla" }, - "providerRequest": { - "message": "$1 vorrebbe connettersi al tuo account" - }, - "providerRequestInfo": { - "message": "Il dominio elencato di seguito sta tentando di richiedere l'accesso all'API Ethereum in modo che possa interagire con la blockchain di Ethereum. Controlla sempre di essere sul sito corretto prima di approvare l'accesso a Ethereum." - }, "about": { "message": "Informazioni" }, "aboutSettingsDescription": { - "message": "Version, centro di supporto e contatti." + "message": "Version, centro di supporto e contatti" }, "acceleratingATransaction": { "message": "* Accelerare una transazione usando un prezzo del gas maggiore aumenta la probabilità che la rete la elabori più velocemente, ma non è garantito." @@ -69,7 +54,7 @@ "message": "Avanzate" }, "advancedSettingsDescription": { - "message": "Accedi alle funzionalità sviluppatore, download dei log di Stato, Reset Account, imposta reti di test e RPC personalizzata." + "message": "Accedi alle funzionalità sviluppatore, download dei log di Stato, Reset Account, imposta reti di test e RPC personalizzata" }, "advancedOptions": { "message": "Opzioni Avanzate" @@ -240,7 +225,7 @@ "connect": { "message": "Connetti" }, - "connectRequest": { + "permissionsRequest": { "message": "Richiesta Connessione" }, "connectingTo": { @@ -363,9 +348,6 @@ "directDepositEtherExplainer": { "message": "Se possiedi già degli Ether, questa è la via più veloce per aggiungere Ether al tuo portafoglio con un deposito diretto." }, - "dismiss": { - "message": "Ignora" - }, "done": { "message": "Finito" }, @@ -1316,7 +1298,7 @@ "updatedWithDate": { "message": "Aggiornata $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "Gli URI richiedono un prefisso HTTP/HTTPS." }, "usedByClients": { @@ -1376,24 +1358,6 @@ "zeroGasPriceOnSpeedUpError": { "message": "Prezzo del gas maggiore di zero" }, - "connections": { - "message": "Connessioni" - }, - "connectionsSettingsDescription": { - "message": "Siti autorizzati ad accedere ai tuoi accounts" - }, - "addSite": { - "message": "Aggiungi Sito" - }, - "addSiteDescription": { - "message": "Aggiungi un sito autorizzato ad accedere ai tuoi accounts, utile per dapps obsolete" - }, - "connected": { - "message": "Connesso" - }, - "connectedDescription": { - "message": "La lista di siti web autorizzati ad accedere ai tuoi indirizzi" - }, "contacts": { "message": "Contatti" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 643da3a84..871f3c75c 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -1,19 +1,10 @@ { - "privacyModeDefault": { - "message": "プライバシーモードがデフォルトで有効になりました" - }, "chartOnlyAvailableEth": { "message": "チャートはEthereumネットワークでのみ利用可能です。" }, - "confirmClear": { - "message": "承認されたウェブサイトをクリアしてもよろしいですか?" - }, "contractInteraction": { "message": "コントラクトへのアクセス" }, - "clearPermissionsSuccess": { - "message": "承認されたウェブサイトデータが正常に消去されました。" - }, "permissionsSettings": { "message": "承認データ" }, @@ -26,12 +17,6 @@ "reject": { "message": "拒否" }, - "providerRequest": { - "message": "$1 はあなたのアカウントにアクセスしようとしています。" - }, - "providerRequestInfo": { - "message": "下記のドメインは、Ethereumブロックチェーンとやり取りできるようにEthereum APIへのアクセスをリクエストしようとしています。 Web3アクセスを承認する前に、正しいサイトにいることを常に確認してください。" - }, "aboutSettingsDescription": { "message": "バージョンやサポート、問合せ先など" }, diff --git a/app/_locales/kn/messages.json b/app/_locales/kn/messages.json index b99bf69d7..f3c3a9020 100644 --- a/app/_locales/kn/messages.json +++ b/app/_locales/kn/messages.json @@ -1,17 +1,11 @@ { - "privacyModeDefault": { - "message": "ಗೌಪ್ಯತೆ ಮೋಡ್ ಅನ್ನು ಡೀಫಾಲ್ಟ್‌ ಆಗಿ ಸಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ" - }, "chartOnlyAvailableEth": { "message": "ಎಥೆರಿಯಮ್ ನೆಟ್‌ವರ್ಕ್‌ಗಳಲ್ಲಿ ಮಾತ್ರವೇ ಚಾರ್ಟ್‌ಗಳು ಲಭ್ಯವಿರುತ್ತವೆ." }, - "confirmClear": { - "message": "ನೀವು ಅನುಮೋದಿಸಿದ ವೆಬ್‌‌ಸೈಟ್‌ಗಳನ್ನು ತೆರವುಗೊಳಿಸಲು ಬಯಸುವಿರಾ?" - }, "contractInteraction": { "message": "ಒಪ್ಪಂದದ ಸಂವಹನ" }, - "clearApprovalData": { + "clearPermissions": { "message": "ಗೌಪ್ಯತೆ ಡೇಟಾವನ್ನು ತೆರವುಗೊಳಿಸಿ" }, "appName": { @@ -21,17 +15,11 @@ "reject": { "message": "ತಿರಸ್ಕರಿಸಿ" }, - "providerRequest": { - "message": "$1 ನಿಮ್ಮ ಖಾತೆಗೆ ಸಂಪರ್ಕಿಸಲು ಬಯಸುತ್ತಿದೆ" - }, - "providerRequestInfo": { - "message": "ಈ ಸೈಟ್ ನಿಮ್ಮ ಪ್ರಸ್ತುತ ಖಾತೆ ವಿಳಾಸವನ್ನು ವೀಕ್ಷಿಸಲು ಪ್ರವೇಶವನ್ನು ವಿನಂತಿಸುತ್ತಿದೆ. ನೀವು ಸಂವಹನ ನಡೆಸುವ ಸೈಟ್‌ಗಳನ್ನು ನೀವು ನಂಬಿರುವಿರಿ ಎಂಬುದನ್ನು ಯಾವಾಗಲೂ ಖಚಿತಪಡಿಸಿಕೊಳ್ಳಿ." - }, "about": { "message": "ಕುರಿತು" }, "aboutSettingsDescription": { - "message": "ಆವೃತ್ತಿ, ಬೆಂಬಲ ಕೇಂದ್ರ ಮತ್ತು ಸಂಪರ್ಕ ಮಾಹಿತಿ." + "message": "ಆವೃತ್ತಿ, ಬೆಂಬಲ ಕೇಂದ್ರ ಮತ್ತು ಸಂಪರ್ಕ ಮಾಹಿತಿ" }, "acceleratingATransaction": { "message": "* ಹೆಚ್ಚಿನ ಗ್ಯಾಸ್ ಬೆಲೆಯನ್ನು ಬಳಸಿಕೊಂಡು ವಹಿವಾಟನ್ನು ವೇಗಗೊಳಿಸುವುದರಿಂದ ನೆಟ್‌ವರ್ಕ್ ವೇಗವಾಗಿ ಪ್ರಕ್ರಿಯೆಗೊಳ್ಳುವ ಸಾಧ್ಯತೆಗಳನ್ನು ಅದು ಹೆಚ್ಚಿಸುತ್ತದೆ, ಆದರೆ ಇದು ಯಾವಾಗಲೂ ಖಚಿತವಾಗಿರುವುದಿಲ್ಲ." @@ -67,7 +55,7 @@ "message": "ಸುಧಾರಿತ" }, "advancedSettingsDescription": { - "message": "ಡೆವಲಪರ್ ವೈಶಿಷ್ಟ್ಯಗಳನ್ನು ಪ್ರವೇಶಿಸಿ, ರಾಜ್ಯದ ಲಾಗ್‌ಗಳನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಿ, ಖಾತೆಯನ್ನು ಮರುಹೊಂದಿಸಿ, ಟೆಸ್ಟ್‌ನೆಟ್ಸ್‌ ಹೊಂದಿಸಿ ಮತ್ತು ಕಸ್ಟಮ್ RPC." + "message": "ಡೆವಲಪರ್ ವೈಶಿಷ್ಟ್ಯಗಳನ್ನು ಪ್ರವೇಶಿಸಿ, ರಾಜ್ಯದ ಲಾಗ್‌ಗಳನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಿ, ಖಾತೆಯನ್ನು ಮರುಹೊಂದಿಸಿ, ಟೆಸ್ಟ್‌ನೆಟ್ಸ್‌ ಹೊಂದಿಸಿ ಮತ್ತು ಕಸ್ಟಮ್ RPC" }, "advancedOptions": { "message": "ಸುಧಾರಿತ ಆಯ್ಕೆಗಳು" @@ -252,7 +240,7 @@ "connect": { "message": "ಸಂಪರ್ಕಿಸು" }, - "connectRequest": { + "permissionsRequest": { "message": "ವಿನಂತಿಯನ್ನು ಸಂಪರ್ಕಪಡಿಸಿ" }, "connectingTo": { @@ -375,9 +363,6 @@ "directDepositEtherExplainer": { "message": "ನೀವು ಈಗಾಗಲೇ ಕೆಲವು ಎಥರ್ ಹೊಂದಿದ್ದರೆ, ನೇರ ಠೇವಣಿ ಮೂಲಕ ನಿಮ್ಮ ಹೊಸ ವ್ಯಾಲೆಟ್‌ನಲ್ಲಿ ಎಥರ್ ಅನ್ನು ಪಡೆಯುವ ತ್ವರಿತ ಮಾರ್ಗ." }, - "dismiss": { - "message": "ವಜಾಗೊಳಿಸಿ" - }, "done": { "message": "ಮುಗಿದಿದೆ" }, @@ -1347,7 +1332,7 @@ "updatedWithDate": { "message": "$1 ನವೀಕರಿಸಲಾಗಿದೆ" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URI ಗಳಿಗೆ ಸೂಕ್ತವಾದ HTTP/HTTPS ಪೂರ್ವಪ್ರತ್ಯಯದ ಅಗತ್ಯವಿದೆ." }, "usedByClients": { diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index d55fb027b..9a2073269 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -1,19 +1,10 @@ { - "privacyModeDefault": { - "message": "이제 프라이버시 모드가 기본 설정으로 활성화됐습니다" - }, "chartOnlyAvailableEth": { "message": "이더리움 네트워크에서만 사용 가능한 차트." }, - "confirmClear": { - "message": "승인 된 웹 사이트를 삭제 하시겠습니까?" - }, "contractInteraction": { "message": "계약 상호 작용" }, - "clearPermissionsSuccess": { - "message": "승인 된 웹 사이트 데이터가 성공적으로 삭제되었습니다." - }, "permissionsSettings": { "message": "승인 데이터" }, @@ -26,17 +17,11 @@ "reject": { "message": "거부" }, - "providerRequest": { - "message": "$1이 당신의 계정에 연결하길 원합니다." - }, - "providerRequestInfo": { - "message": "아래 나열된 도메인은 Web3 API에 대한 액세스를 요청하여 Ethereum 블록 체인과 상호 작용할 수 있습니다. Ethereum 액세스를 승인하기 전에 항상 올바른 사이트에 있는지 다시 확인하십시오." - }, "about": { "message": "정보" }, "aboutSettingsDescription": { - "message": "버전, 지원 센터, 그리고 연락처 정보." + "message": "버전, 지원 센터, 그리고 연락처 정보" }, "acceleratingATransaction": { "message": "* 더 높은 가스 요금을 사용하여 트랜잭션을 가속화하면 네트워크에 의해 더 빨리 처리될 가능성이 증가하지만 항상 빠른 처리가 보장되는 것은 아닙니다." @@ -72,7 +57,7 @@ "message": "고급" }, "advancedSettingsDescription": { - "message": "개발자 기능 사용, 상태 로그 다운로드, 계정 재설정, 테스트넷 및 사용자 정의 RPC 설정." + "message": "개발자 기능 사용, 상태 로그 다운로드, 계정 재설정, 테스트넷 및 사용자 정의 RPC 설정" }, "advancedOptions": { "message": "고급 옵션" @@ -258,7 +243,7 @@ "connect": { "message": "연결" }, - "connectRequest": { + "permissionsRequest": { "message": "연결 요청" }, "connectingTo": { @@ -381,9 +366,6 @@ "directDepositEtherExplainer": { "message": "약간의 이더를 이미 보유하고 있다면, 새로 만든 지갑에 직접 입금하여 이더를 보유할 수 있습니다." }, - "dismiss": { - "message": "숨기기" - }, "done": { "message": "완료" }, @@ -1347,7 +1329,7 @@ "updatedWithDate": { "message": "$1에 업데이트 됨" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URI는 HTTP/HTTPS로 시작해야 합니다." }, "usedByClients": { diff --git a/app/_locales/lt/messages.json b/app/_locales/lt/messages.json index 36e8f52dd..b855f0800 100644 --- a/app/_locales/lt/messages.json +++ b/app/_locales/lt/messages.json @@ -1,33 +1,21 @@ { - "privacyModeDefault": { - "message": "Dabar privatumo režimas suaktyvintas pagal numatytąją nuostatą" - }, "chartOnlyAvailableEth": { "message": "Diagramos yra tik „Ethereum“ tinkluose." }, - "confirmClear": { - "message": "Ar tikrai norite panaikinti patvirtintas svetaines?" - }, "contractInteraction": { "message": "Sutartinė sąveika" }, - "clearApprovalData": { + "clearPermissions": { "message": "Išvalyti asmeninius duomenis" }, "reject": { "message": "Atmesti" }, - "providerRequest": { - "message": "$1 norėtų prisijungti prie jūsų paskyros" - }, - "providerRequestInfo": { - "message": "Ši svetainė prašo prieigos peržiūrėti jūsų dabartinės paskyros adresą. Visada patikrinkite, ar pasitikite svetainėmis, su kuriomis sąveikaujate." - }, "about": { "message": "Apie" }, "aboutSettingsDescription": { - "message": "Versija, palaikymo centras ir kontaktinė informacija." + "message": "Versija, palaikymo centras ir kontaktinė informacija" }, "acceleratingATransaction": { "message": "Operacijos paspartinimas naudojantis didesne dujų kaina padidina galimybes, kad ji bus greičiau apdorota tinkle, tačiau tai ne visada garantuojama. " @@ -63,7 +51,7 @@ "message": "Išplėstiniai" }, "advancedSettingsDescription": { - "message": "Prieigos kūrėjo funkcijos, būsenos žurnalų atsiuntimas, paskyros atstatymas, „testnet“ nustatymas ir pritaikytas RPC." + "message": "Prieigos kūrėjo funkcijos, būsenos žurnalų atsiuntimas, paskyros atstatymas, „testnet“ nustatymas ir pritaikytas RPC" }, "advancedOptions": { "message": "Išplėstinės parinktys" @@ -252,7 +240,7 @@ "connect": { "message": "Prisijungti" }, - "connectRequest": { + "permissionsRequest": { "message": "Prijungimo užklausa" }, "connectingTo": { @@ -375,9 +363,6 @@ "directDepositEtherExplainer": { "message": "Jeigu jau turite šiek tiek eterių, sparčiausias būdas gauti eterių į naują piniginę yra tiesioginis įnašas." }, - "dismiss": { - "message": "Atsisakyti" - }, "done": { "message": "Atlikta" }, @@ -1347,7 +1332,7 @@ "updatedWithDate": { "message": "Atnaujinta $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URI reikia atitinkamo HTTP/HTTPS priešdėlio." }, "usedByClients": { diff --git a/app/_locales/lv/messages.json b/app/_locales/lv/messages.json index 6261816be..ea88dc4e1 100644 --- a/app/_locales/lv/messages.json +++ b/app/_locales/lv/messages.json @@ -1,17 +1,11 @@ { - "privacyModeDefault": { - "message": "Privātais režīms tagad ieslēgts pēc noklusējuma" - }, "chartOnlyAvailableEth": { "message": "Grafiks pieejams vienīgi Ethereum tīklos." }, - "confirmClear": { - "message": "Vai tiešām vēlaties dzēst apstiprinātās vietnes?" - }, "contractInteraction": { "message": "Līguma mijiedarbības" }, - "clearApprovalData": { + "clearPermissions": { "message": "Notīrīt konfidencialitātes datus" }, "appName": { @@ -21,12 +15,6 @@ "reject": { "message": "Noraidīt" }, - "providerRequest": { - "message": "$1 vēlas izveidot savienojumu ar jūsu kontu" - }, - "providerRequestInfo": { - "message": "Šī lapa pieprasa piekļuvi jūsu pašreizēja konta adreses informācijai. Vienmēr pārliecinieties, ka uzticaties lapām, kuras apmeklējat." - }, "about": { "message": "Par" }, @@ -67,7 +55,7 @@ "message": "Papildu" }, "advancedSettingsDescription": { - "message": "Piekļūstiet izstrādātāju funkcijām, lejupielādējiet stāvokļu žurnālus, atiestatiet kontu, iestatiet testa tīklus un pielāgotos RPC izsaukumus." + "message": "Piekļūstiet izstrādātāju funkcijām, lejupielādējiet stāvokļu žurnālus, atiestatiet kontu, iestatiet testa tīklus un pielāgotos RPC izsaukumus" }, "advancedOptions": { "message": "Papildu opcijas" @@ -252,7 +240,7 @@ "connect": { "message": "Pievienošana" }, - "connectRequest": { + "permissionsRequest": { "message": "Savienojuma pieprasījums" }, "connectingTo": { @@ -375,9 +363,6 @@ "directDepositEtherExplainer": { "message": "Ja jums jau ir Ether, tad visātrāk Ether savā makā varat saņemt ar tiešo iemaksu." }, - "dismiss": { - "message": "Noraidīt" - }, "done": { "message": "Pabeigts" }, @@ -1343,7 +1328,7 @@ "updatedWithDate": { "message": "Atjaunināts $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URI jāsākas ar atbilstošo HTTP/HTTPS priedēkli." }, "usedByClients": { diff --git a/app/_locales/ml/messages.json b/app/_locales/ml/messages.json index cbcee4186..d3ba24111 100644 --- a/app/_locales/ml/messages.json +++ b/app/_locales/ml/messages.json @@ -57,9 +57,6 @@ "details": { "message": "വിശദാംശങ്ങൾ‌" }, - "dismiss": { - "message": "ബഹിഷ്‌ക്കരിക്കുക" - }, "done": { "message": "പൂർത്തിയാക്കി" }, diff --git a/app/_locales/mr/messages.json b/app/_locales/mr/messages.json index af06c03fb..2490a6c00 100644 --- a/app/_locales/mr/messages.json +++ b/app/_locales/mr/messages.json @@ -57,9 +57,6 @@ "details": { "message": "तपशील" }, - "dismiss": { - "message": "डिसमिस करा" - }, "done": { "message": "पूर्ण झाले" }, diff --git a/app/_locales/ms/messages.json b/app/_locales/ms/messages.json index e6227f619..af747ff80 100644 --- a/app/_locales/ms/messages.json +++ b/app/_locales/ms/messages.json @@ -1,33 +1,21 @@ { - "privacyModeDefault": { - "message": "Mod Privasi kini diaktifkan secara lalai" - }, "chartOnlyAvailableEth": { "message": "Carta hanya tersedia di rangkaian Ethereum." }, - "confirmClear": { - "message": "Adakah anda pasti mahu mengosongkan tapak web diluluskan?" - }, "contractInteraction": { "message": "Interaksi Kontrak" }, - "clearApprovalData": { + "clearPermissions": { "message": "Kosongkan Data Privasi" }, "reject": { "message": "Tolak" }, - "providerRequest": { - "message": "$1 ingin menyambung kepada akaun anda" - }, - "providerRequestInfo": { - "message": "Tapak ini meminta akses untuk melihat alamat akaun semasa anda. Sentiasa pastikan anda mempercayai tapak web yang anda berinteraksi." - }, "about": { "message": "Mengenai" }, "aboutSettingsDescription": { - "message": "Versi, pusat sokongan, dan maklumat perhubungan." + "message": "Versi, pusat sokongan, dan maklumat perhubungan" }, "acceleratingATransaction": { "message": "* Mempercepatkan transaksi menggunakan harga gas lebih tinggi akan meningkatkan peluang diproses oleh rangkaian lebih cepat, tetapi ini pun tidak sentiasa dijamin." @@ -63,7 +51,7 @@ "message": "Lanjutan" }, "advancedSettingsDescription": { - "message": "Akses ciri-ciri pembangun, muat turun Log Keadaan, Set Semula Akaun, sediakan jaringan ujian dan RPC tersuai." + "message": "Akses ciri-ciri pembangun, muat turun Log Keadaan, Set Semula Akaun, sediakan jaringan ujian dan RPC tersuai" }, "advancedOptions": { "message": "Pilihan Lanjutan" @@ -249,7 +237,7 @@ "connect": { "message": "Sambung" }, - "connectRequest": { + "permissionsRequest": { "message": "Sambungkan Permintaan" }, "connectingTo": { @@ -366,9 +354,6 @@ "directDepositEtherExplainer": { "message": "Jika anda sudah mempunyai Ether, cara paling cepat untuk mendapatkan Ether di dompet baru anda ialah dengan deposit langsung." }, - "dismiss": { - "message": "Singkirkan" - }, "done": { "message": "Selesai" }, @@ -1315,7 +1300,7 @@ "updatedWithDate": { "message": "Dikemaskini $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URI memerlukan awalan HTTP/HTTPS yang sesuai." }, "usedByClients": { diff --git a/app/_locales/nl/messages.json b/app/_locales/nl/messages.json index c1e2290fa..ecf6ee1d6 100644 --- a/app/_locales/nl/messages.json +++ b/app/_locales/nl/messages.json @@ -1,10 +1,4 @@ { - "confirmClear": { - "message": "Weet je zeker dat je goedgekeurde websites wilt wissen?" - }, - "clearPermissionsSuccess": { - "message": "Goedgekeurde websitegegevens zijn met succes gewist." - }, "permissionsSettings": { "message": "Goedkeuringsgegevens" }, @@ -17,9 +11,6 @@ "reject": { "message": "Afwijzen" }, - "providerRequestInfo": { - "message": "Het onderstaande domein probeert toegang tot de Ethereum API te vragen zodat deze kan communiceren met de Ethereum-blockchain. Controleer altijd eerst of u op de juiste site bent voordat u Ethereum-toegang goedkeurt." - }, "accountDetails": { "message": "Accountgegevens" }, @@ -487,7 +478,7 @@ "unknownNetwork": { "message": "Onbekend privénetwerk" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "Voor URI's is het juiste HTTP / HTTPS-voorvoegsel vereist." }, "usedByClients": { diff --git a/app/_locales/no/messages.json b/app/_locales/no/messages.json index 51f21abee..6c9226678 100644 --- a/app/_locales/no/messages.json +++ b/app/_locales/no/messages.json @@ -1,33 +1,21 @@ { - "privacyModeDefault": { - "message": "Personvernmodus er nå aktivert som standard" - }, "chartOnlyAvailableEth": { "message": "Diagram kun tilgjengelig på Ethereum-nettverk." }, - "confirmClear": { - "message": "Er du sikker på at du vil tømme godkjente nettsteder?" - }, "contractInteraction": { "message": "Kontraktssamhandling" }, - "clearApprovalData": { + "clearPermissions": { "message": "Tøm personvernsdata" }, "reject": { "message": "Avslå" }, - "providerRequest": { - "message": "$1 ønsker å forbindes med kontoen din " - }, - "providerRequestInfo": { - "message": "Dette nettstedet ber om tilgang til å vise din nåværende kontoadresse. Alltid kontroller at du stoler på nettstedene du samhandler med." - }, "about": { "message": "Info" }, "aboutSettingsDescription": { - "message": "Versjon, brukerstøtte og kontaktinformasjon." + "message": "Versjon, brukerstøtte og kontaktinformasjon" }, "acceleratingATransaction": { "message": "* Akselerering av en transaksjon ved å bruke en høyere datakraftspris øker sjansene for å bli behandlet av nettverket raskere, men det er ikke alltid garantert." @@ -63,7 +51,7 @@ "message": "Avansert" }, "advancedSettingsDescription": { - "message": "Få tilgang til utviklerfunksjoner, last ned tilstandslogger, tilbakestill konto, installer testnett og tilpasset RPC." + "message": "Få tilgang til utviklerfunksjoner, last ned tilstandslogger, tilbakestill konto, installer testnett og tilpasset RPC" }, "advancedOptions": { "message": "Avanserte valg" @@ -249,7 +237,7 @@ "connect": { "message": "Koble til" }, - "connectRequest": { + "permissionsRequest": { "message": "Kontaktforespørsel " }, "connectingTo": { @@ -372,9 +360,6 @@ "directDepositEtherExplainer": { "message": "Hvis du allerede har noe Ether, er den raskeste måten å få Ether i den nye lommeboken din på ved hjelp av direkte innskudd." }, - "dismiss": { - "message": "Lukk" - }, "done": { "message": "Ferdig" }, @@ -1319,7 +1304,7 @@ "updatedWithDate": { "message": "Oppdatert $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URI-er krever det aktuelle HTTP/HTTPS-prefikset." }, "usedByClients": { diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index 2bc94990d..287bb0fac 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -1,10 +1,4 @@ { - "confirmClear": { - "message": "Sigurado ka bang gusto mong i-clear ang mga naaprubahang website?" - }, - "clearPermissionsSuccess": { - "message": "Matagumpay na na-clear ang data ng aprubadong website." - }, "permissionsSettings": { "message": "Data ng Pag-apruba" }, @@ -24,9 +18,6 @@ "reject": { "message": "Tanggihan" }, - "providerRequestInfo": { - "message": "Ang domain na nakalista sa ibaba ay sinusubukang humiling ng access sa Ethereum API upang maaari itong makipag-ugnayan sa Ethereum blockchain. Laging i-double check na ikaw ay nasa tamang site bago aprubahan ang Ethereum access." - }, "accountDetails": { "message": "Detalye ng Account" }, diff --git a/app/_locales/pl/messages.json b/app/_locales/pl/messages.json index 47923983d..c55a9db08 100644 --- a/app/_locales/pl/messages.json +++ b/app/_locales/pl/messages.json @@ -1,33 +1,21 @@ { - "privacyModeDefault": { - "message": "Tryb prywatny jest domyślnie włączony" - }, "chartOnlyAvailableEth": { "message": "Wykres dostępny tylko w sieciach Ethereum" }, - "confirmClear": { - "message": "Czy na pewno chcesz usunąć zatwierdzone strony internetowe?" - }, "contractInteraction": { "message": "Interakcja z kontraktem" }, - "clearApprovalData": { + "clearPermissions": { "message": "Usuń dane poufne" }, "reject": { "message": "Odrzuć" }, - "providerRequest": { - "message": "$1 chce połączyć się z Twoim kontem" - }, - "providerRequestInfo": { - "message": "Ta strona prosi o dostęp, aby zobaczyć adres Twojego aktualnego konta. Zawsze upewnij się, że ufasz stronom, z którymi wchodzisz w interakcję." - }, "about": { "message": "Informacje" }, "aboutSettingsDescription": { - "message": "Wersja, centrum wsparcia i dane kontaktowe." + "message": "Wersja, centrum wsparcia i dane kontaktowe" }, "acceleratingATransaction": { "message": "* Przyspieszenie transakcji poprzez zastosowanie wyższej ceny gazu zwiększa szanse na jej szybsze przetworzenie przez sieć, jednak skuteczność tej operacji nie jest gwarantowana." @@ -63,7 +51,7 @@ "message": "Zaawansowane" }, "advancedSettingsDescription": { - "message": "Dostęp do funkcji programisty, pobieranie dzienników stanu, resetowanie konta, konfigurowanie sieci testowych i niestandardowe RPC." + "message": "Dostęp do funkcji programisty, pobieranie dzienników stanu, resetowanie konta, konfigurowanie sieci testowych i niestandardowe RPC" }, "advancedOptions": { "message": "Opcje zaawansowane" @@ -249,7 +237,7 @@ "connect": { "message": "Połącz" }, - "connectRequest": { + "permissionsRequest": { "message": "Potwierdź żądanie" }, "connectingTo": { @@ -372,9 +360,6 @@ "directDepositEtherExplainer": { "message": "Jeśli już masz Eter, najszybciej umieścisz go w swoim nowym portfelu przy pomocy bezpośredniego depozytu." }, - "dismiss": { - "message": "Zamknij" - }, "done": { "message": "Gotowe" }, @@ -1332,7 +1317,7 @@ "updatedWithDate": { "message": "Zaktualizowano $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URI wymaga prawidłowego prefiksu HTTP/HTTPS." }, "usedByClients": { diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 78f31df83..8362ab3ce 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -1,10 +1,4 @@ { - "confirmClear": { - "message": "Tem certeza de que deseja limpar sites aprovados?" - }, - "clearPermissionsSuccess": { - "message": "Dados aprovados do website foram limpos com sucesso." - }, "permissionsSettings": { "message": "Dados de aprovação" }, @@ -17,9 +11,6 @@ "reject": { "message": "Rejeitar" }, - "providerRequestInfo": { - "message": "O domínio listado abaixo está tentando solicitar acesso à API Ethereum para que ele possa interagir com o blockchain Ethereum. Sempre verifique se você está no site correto antes de aprovar o acesso à Ethereum." - }, "account": { "message": "Conta" }, @@ -497,7 +488,7 @@ "unknownNetwork": { "message": "Rede Privada Desconhecida" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "Links requerem o prefixo HTTP/HTTPS apropriado." }, "usedByClients": { diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index 0b6963d02..da360e476 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -1,17 +1,11 @@ { - "privacyModeDefault": { - "message": "O Modo de Privacidade está ativado por padrão" - }, "chartOnlyAvailableEth": { "message": "Tabela disponível apenas em redes de Ethereum." }, - "confirmClear": { - "message": "Tem certeza de que deseja limpar os sites aprovados?" - }, "contractInteraction": { "message": "Interação do Contrato" }, - "clearApprovalData": { + "clearPermissions": { "message": "Limpar Dados de Privacidade" }, "appName": { @@ -21,17 +15,11 @@ "reject": { "message": "Rejeitar" }, - "providerRequest": { - "message": "$1 gostaria de se conectar à sua conta" - }, - "providerRequestInfo": { - "message": "Este site está solicitando acesso para visualizar o seu endereço de conta atual. Certifique-se sempre de confiar nos sites com os quais você interage." - }, "about": { "message": "Sobre" }, "aboutSettingsDescription": { - "message": "Versão, centro de apoio e informações de contato." + "message": "Versão, centro de apoio e informações de contato" }, "acceleratingATransaction": { "message": "* Acelerar uma transação usando um preço de gás mais alto aumenta suas chances de a rede processá-la de forma mais rápida, mas isso nem sempre é garantido." @@ -67,7 +55,7 @@ "message": "Avançado" }, "advancedSettingsDescription": { - "message": "Acesse recursos do desenvolvedor, baixe registros de estado, redefina a conta, configure testnets e personalize RPC." + "message": "Acesse recursos do desenvolvedor, baixe registros de estado, redefina a conta, configure testnets e personalize RPC" }, "advancedOptions": { "message": "Opções avançadas" @@ -246,7 +234,7 @@ "connect": { "message": "Conectar-se" }, - "connectRequest": { + "permissionsRequest": { "message": "Solicitação de Conexão" }, "connectingTo": { @@ -369,9 +357,6 @@ "directDepositEtherExplainer": { "message": "Se você já tem Ether, a forma mais rápida de colocá-lo em sua nova carteira é o depósito direto." }, - "dismiss": { - "message": "Dispensar" - }, "done": { "message": "Concluído" }, @@ -1326,7 +1311,7 @@ "updatedWithDate": { "message": "$1 atualizado" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URIs exigem o devido prefixo HTTP/HTTPS." }, "usedByClients": { diff --git a/app/_locales/pt_PT/messages.json b/app/_locales/pt_PT/messages.json index ce35721de..080c02422 100644 --- a/app/_locales/pt_PT/messages.json +++ b/app/_locales/pt_PT/messages.json @@ -66,9 +66,6 @@ "details": { "message": "Detalhes" }, - "dismiss": { - "message": "Ignorar" - }, "done": { "message": "Concluído" }, diff --git a/app/_locales/ro/messages.json b/app/_locales/ro/messages.json index 5d81811e9..554816a8a 100644 --- a/app/_locales/ro/messages.json +++ b/app/_locales/ro/messages.json @@ -1,33 +1,21 @@ { - "privacyModeDefault": { - "message": "Modul de confidențialitate este activat acum în mod implicit" - }, "chartOnlyAvailableEth": { "message": "Grafic disponibil numai pe rețelele Ethereum." }, - "confirmClear": { - "message": "Sunteți sigur că doriți să ștergeți site-urile aprobate?" - }, "contractInteraction": { "message": "Interacțiune contract" }, - "clearApprovalData": { + "clearPermissions": { "message": "Ștergeți datele confidențiale" }, "reject": { "message": "Respingeți" }, - "providerRequest": { - "message": "$1 ar dori să se conecteze la contul dvs." - }, - "providerRequestInfo": { - "message": "Acest site solicită acces pentru a vedea adresa curentă a contului dvs. Asigurați-vă întotdeauna că aveți încredere în site-urile cu care interacționați." - }, "about": { "message": "Despre" }, "aboutSettingsDescription": { - "message": "Versiune, centru de asistență și date de contact." + "message": "Versiune, centru de asistență și date de contact" }, "acceleratingATransaction": { "message": "* Accelerarea unei tranzacții folosind un preț în gas mai mare îi crește șansele de a fi procesată mai rapid de rețea, însă acest lucru nu este garantat întotdeauna." @@ -63,7 +51,7 @@ "message": "Avansate" }, "advancedSettingsDescription": { - "message": "Accesați funcții pentru dezvoltatori, descărcați Jurnale de stare, resetați contul, configurați rețele de test și RPC personalizat." + "message": "Accesați funcții pentru dezvoltatori, descărcați Jurnale de stare, resetați contul, configurați rețele de test și RPC personalizat" }, "advancedOptions": { "message": "Opțiuni avansate" @@ -252,7 +240,7 @@ "connect": { "message": "Conectează-te" }, - "connectRequest": { + "permissionsRequest": { "message": "Solicitare de conectare" }, "connectingTo": { @@ -375,9 +363,6 @@ "directDepositEtherExplainer": { "message": "Dacă deja aveți Ether, cel mai rapid mod de a avea Ether în portofelul nou prin depunere directă." }, - "dismiss": { - "message": "Închide" - }, "done": { "message": "Efectuat" }, @@ -1328,7 +1313,7 @@ "updatedWithDate": { "message": "Actualizat $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URL necesită prefixul potrivit HTTP/HTTPS." }, "usedByClients": { diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 04107f6b7..38b9daee1 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -1,10 +1,4 @@ { - "confirmClear": { - "message": "Вы уверены, что хотите очистить утвержденные веб-сайты?Tem certeza de que deseja limpar sites aprovados?" - }, - "clearPermissionsSuccess": { - "message": "Утвержденные данные веб-сайта успешно удалены." - }, "permissionsSettings": { "message": "Данные об утверждении" }, @@ -17,9 +11,6 @@ "reject": { "message": "Отклонить" }, - "providerRequestInfo": { - "message": "Домен, указанный ниже, пытается запросить доступ к API-интерфейсу Ethereum, чтобы он мог взаимодействовать с блокчейном Ethereum. Всегда проверяйте, что вы находитесь на правильном сайте, прежде чем одобрять доступ к веб-сайту." - }, "account": { "message": "Счет" }, @@ -551,7 +542,7 @@ "unknownNetwork": { "message": "Неизвестная частная сеть" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "Для URI требуется соответствующий префикс HTTP/HTTPS." }, "usedByClients": { @@ -578,18 +569,12 @@ "youSign": { "message": "Вы подписываете" }, - "privacyModeDefault": { - "message": "Режим конфиденциальности теперь включен по умолчанию" - }, "chartOnlyAvailableEth": { "message": "Диаграмма доступна только в сетях Ethereum." }, "contractInteraction": { "message": "Взаимодействие с контрактом" }, - "providerRequest": { - "message": "$1 запрашивает доступ к вашему аккаунту" - }, "about": { "message": "О нас" }, @@ -740,7 +725,7 @@ "connect": { "message": "Подключиться" }, - "connectRequest": { + "permissionsRequest": { "message": "Запрос на подключение" }, "connectingTo": { @@ -785,9 +770,6 @@ "deleteAccount": { "message": "Удалить аккаунт" }, - "dismiss": { - "message": "Отклюнить" - }, "downloadGoogleChrome": { "message": "Скачать Google Chrome" }, diff --git a/app/_locales/sk/messages.json b/app/_locales/sk/messages.json index c2be7a75f..1aeb7d0c2 100644 --- a/app/_locales/sk/messages.json +++ b/app/_locales/sk/messages.json @@ -1,19 +1,10 @@ { - "privacyModeDefault": { - "message": "Režim súkromia je povolený. Je prednastavený automaticky" - }, "chartOnlyAvailableEth": { "message": "Graf je k dispozícii iba v sieťach Ethereum." }, - "confirmClear": { - "message": "Naozaj chcete vymazať schválené webové stránky?" - }, "contractInteraction": { "message": "Zmluvná interakcia" }, - "clearPermissionsSuccess": { - "message": "Schválené údaje webových stránek byly úspěšně zrušeny." - }, "permissionsSettings": { "message": "Údaje o schválení" }, @@ -26,17 +17,11 @@ "reject": { "message": "Odmítnout" }, - "providerRequest": { - "message": "$1 sa chce pripojiť k vášmu účtu" - }, - "providerRequestInfo": { - "message": "Níže uvedená doména se pokouší požádat o přístup k API Ethereum, aby mohla komunikovat s blokádou Ethereum. Před schválením přístupu Ethereum vždy zkontrolujte, zda jste na správném místě." - }, "about": { "message": "Informácie" }, "aboutSettingsDescription": { - "message": "Verzia, centrum podpory a kontaktné informácie." + "message": "Verzia, centrum podpory a kontaktné informácie" }, "acceleratingATransaction": { "message": "*Urýchlenie transakcie pomocou vyššej ceny za GAS zvyšuje šance na rýchlejšie spracovanie v sieti, nie je to však vždy zaručené." @@ -72,7 +57,7 @@ "message": "Rozšírené" }, "advancedSettingsDescription": { - "message": "Získajte prístup k vývojárskym funkciám, sťahujte si Stavové denníky, resetujte účet, nastavujte testovacie siete a vlastné RPC." + "message": "Získajte prístup k vývojárskym funkciám, sťahujte si Stavové denníky, resetujte účet, nastavujte testovacie siete a vlastné RPC" }, "advancedOptions": { "message": "Rozšírené nastavenia" @@ -252,7 +237,7 @@ "connect": { "message": "Pripojenie" }, - "connectRequest": { + "permissionsRequest": { "message": "Požiadavka na pripojenie" }, "connectingTo": { @@ -375,9 +360,6 @@ "directDepositEtherExplainer": { "message": "Pokud už vlastníte nějaký Ether, nejrychleji ho dostanete do peněženky přímým vkladem." }, - "dismiss": { - "message": "Zatvoriť" - }, "done": { "message": "Hotovo" }, @@ -1310,7 +1292,7 @@ "updatedWithDate": { "message": "Aktualizované $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URI vyžadují korektní HTTP/HTTPS prefix." }, "usedByClients": { diff --git a/app/_locales/sl/messages.json b/app/_locales/sl/messages.json index 6b34e70ba..fc6179800 100644 --- a/app/_locales/sl/messages.json +++ b/app/_locales/sl/messages.json @@ -1,19 +1,10 @@ { - "privacyModeDefault": { - "message": "Zasebnostni način je zdaj privzeto omogočen" - }, "chartOnlyAvailableEth": { "message": "Grafikon na voljo le v glavnih omrežjih." }, - "confirmClear": { - "message": "Ste prepričani da želite počistiti odobrene spletne strani?" - }, "contractInteraction": { "message": "Interakcija s pogodbo" }, - "clearPermissionsSuccess": { - "message": "Odobrene spletne strani uspešno počiščene." - }, "permissionsSettings": { "message": "Podatki o odobritvi" }, @@ -26,17 +17,11 @@ "reject": { "message": "Zavrni" }, - "providerRequest": { - "message": "$1 se želi povezati z vašim računom." - }, - "providerRequestInfo": { - "message": "Domena zahteva dostop do verige blokov in ogled vašega računa. Pred potrditvjo vedno preverite ali ste na želeni spletni strani." - }, "about": { "message": "O možnostih" }, "aboutSettingsDescription": { - "message": "Različica, center za podporo in podatki za stik." + "message": "Različica, center za podporo in podatki za stik" }, "acceleratingATransaction": { "message": "* Pospešitev transakcije z višjo gas ceno poveča njene možnosti za hitrejšo obdelavo v omrežju, vendar ni vedno zagotovljena." @@ -72,7 +57,7 @@ "message": "Napredno" }, "advancedSettingsDescription": { - "message": "Dostopite do funkcij razvijalca, prenesite dnevnike držav, ponastavite račun, nastavite testne mreže in RPC po meri." + "message": "Dostopite do funkcij razvijalca, prenesite dnevnike držav, ponastavite račun, nastavite testne mreže in RPC po meri" }, "advancedOptions": { "message": "Napredne možnosti" @@ -261,7 +246,7 @@ "connect": { "message": "Poveži" }, - "connectRequest": { + "permissionsRequest": { "message": "Zahteva za povezavo" }, "connectingTo": { @@ -384,9 +369,6 @@ "directDepositEtherExplainer": { "message": "Če že imate Ether, ga lahko najhitreje dobite v MetaMask z neposrednim vplačilom." }, - "dismiss": { - "message": "Opusti" - }, "done": { "message": "Končano" }, @@ -1338,7 +1320,7 @@ "updatedWithDate": { "message": "Posodobljeno $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URI zahtevajo ustrezno HTTP/HTTPS predpono." }, "usedByClients": { diff --git a/app/_locales/sr/messages.json b/app/_locales/sr/messages.json index 01056d9e3..90e0485ff 100644 --- a/app/_locales/sr/messages.json +++ b/app/_locales/sr/messages.json @@ -1,33 +1,21 @@ { - "privacyModeDefault": { - "message": "Režim privatnosti je podrazumevano omogućen" - }, "chartOnlyAvailableEth": { "message": "Grafikon dostupan jedino na mrežama Ethereum." }, - "confirmClear": { - "message": "Da li ste sigurni da želite da obrišete odobrene veb lokacije?" - }, "contractInteraction": { "message": "Ugovorna interakcija" }, - "clearApprovalData": { + "clearPermissions": { "message": "Obrišite privatne podatke" }, "reject": { "message": "Одбиј" }, - "providerRequest": { - "message": "$1 bi hteo da se poveže sa vašim nalogom" - }, - "providerRequestInfo": { - "message": "Ovaj sajt traži pristup kako bi video vašu trenutnu adresu naloga. Uvek budite sigurni da verujete sajtovima s kojima komunicirate." - }, "about": { "message": "Основни подаци" }, "aboutSettingsDescription": { - "message": "Verzija, centar za podršku i podaci za kontakt." + "message": "Verzija, centar za podršku i podaci za kontakt" }, "acceleratingATransaction": { "message": "* Time što se ubrzava transakcija koristeći veću gas cenu, povećavaju se šanse da se procesuira brže od strane mreže, ali to nije uvek zagarantovano." @@ -63,7 +51,7 @@ "message": "Напредне опције" }, "advancedSettingsDescription": { - "message": "Pristupite funkcijama za programere, preuzmite državne evidencije, resetujte nalog, postavite testne mreže i prilagođeni RPC." + "message": "Pristupite funkcijama za programere, preuzmite državne evidencije, resetujte nalog, postavite testne mreže i prilagođeni RPC" }, "advancedOptions": { "message": "Dodatne opcije" @@ -249,7 +237,7 @@ "connect": { "message": "Повезивање" }, - "connectRequest": { + "permissionsRequest": { "message": "Zahtev za povezivanjem" }, "connectingTo": { @@ -372,9 +360,6 @@ "directDepositEtherExplainer": { "message": "Ako već imate neki Ether, najbrži način da preuzmete Ether u svoj novi novčanik jeste direktnim deponovanjem." }, - "dismiss": { - "message": "Одбаци" - }, "done": { "message": "Gotovo" }, @@ -1332,7 +1317,7 @@ "updatedWithDate": { "message": "Ažuriran $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URI-ovi zahtevaju odgovarajući prefiks HTTP / HTTPS." }, "usedByClients": { diff --git a/app/_locales/sv/messages.json b/app/_locales/sv/messages.json index 67a9fb94b..1a84d46d3 100644 --- a/app/_locales/sv/messages.json +++ b/app/_locales/sv/messages.json @@ -1,17 +1,11 @@ { - "privacyModeDefault": { - "message": "Integritetsläge är nu aktiverat som standard" - }, "chartOnlyAvailableEth": { "message": "Tabellen är endast tillgänglig på Ethereum-nätverk." }, - "confirmClear": { - "message": "Är du säker på att du vill rensa godkända webbplatser?" - }, "contractInteraction": { "message": "Kontraktinteraktion" }, - "clearApprovalData": { + "clearPermissions": { "message": "Rensa personlig data" }, "appName": { @@ -21,17 +15,11 @@ "reject": { "message": "Avvisa" }, - "providerRequest": { - "message": "$1 vill ansluta till ditt konto" - }, - "providerRequestInfo": { - "message": "Den här sidan begär åtkomst till din aktuella kontoadress. Se till att du kan lita på de sidor du använder dig av." - }, "about": { "message": "Om" }, "aboutSettingsDescription": { - "message": "Version, supportcenter och kontaktinformation." + "message": "Version, supportcenter och kontaktinformation" }, "acceleratingATransaction": { "message": "* Att snabba upp en överföring genom att använda ett högre gaspris ökar chanserna för att överföringen ska hanteras snabbare av nätverket, men det är inte en garanti." @@ -67,7 +55,7 @@ "message": "Avancerat" }, "advancedSettingsDescription": { - "message": "Åtkomst till verktyg för utvecklare, ladda ner loggar, återställ konto, upprätta testnätverk och skräddarsy RPC." + "message": "Åtkomst till verktyg för utvecklare, ladda ner loggar, återställ konto, upprätta testnätverk och skräddarsy RPC" }, "advancedOptions": { "message": "Avancerade alternativ" @@ -246,7 +234,7 @@ "connect": { "message": "Ansluta" }, - "connectRequest": { + "permissionsRequest": { "message": "Anslutningsförfrågan" }, "connectingTo": { @@ -369,9 +357,6 @@ "directDepositEtherExplainer": { "message": "Om du redan har Ether är det snabbaste sättet att få Ether i din nya plånbok att göra en direktinsättning." }, - "dismiss": { - "message": "Ta bort permanent" - }, "done": { "message": "Klart" }, @@ -1322,7 +1307,7 @@ "updatedWithDate": { "message": "Uppdaterat $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URI:er kräver lämpligt HTTP/HTTPS-prefix." }, "usedByClients": { diff --git a/app/_locales/sw/messages.json b/app/_locales/sw/messages.json index 40aec5547..28bf79607 100644 --- a/app/_locales/sw/messages.json +++ b/app/_locales/sw/messages.json @@ -1,17 +1,11 @@ { - "privacyModeDefault": { - "message": "Hali ya Faragha sasa imewezeshwa kwa chaguomsingi" - }, "chartOnlyAvailableEth": { "message": "Zogoa inapatikana kwenye mitandao ya Ethereum pekee." }, - "confirmClear": { - "message": "Una uhakika unataka kufuta tovuti zilizodihinishwa?" - }, "contractInteraction": { "message": "Mwingiliono wa Mkataba" }, - "clearApprovalData": { + "clearPermissions": { "message": "Futa Data za Faragha" }, "appName": { @@ -21,17 +15,11 @@ "reject": { "message": "Kataa" }, - "providerRequest": { - "message": "$1 ingependa kuunganishwa kwenye akaunti yako" - }, - "providerRequestInfo": { - "message": "Tovuti hii inaomba idhini ya kuangalia anwani yako ya akaunti ya sasa. Daima hakikisha unaziamami tovuti ambazo unaingiliana nazo." - }, "about": { "message": "Kuhusu" }, "aboutSettingsDescription": { - "message": "Toleo, kituo cha msaada, na taarifa za mawasiliano." + "message": "Toleo, kituo cha msaada, na taarifa za mawasiliano" }, "acceleratingATransaction": { "message": "*Kuwezesha muamala kwa kutumia bei ya juu ya gesi huongeza uwezekano wake wa kushughulikiwa na mtandao haraka, lakini hauhakikishiwi siku zote." @@ -67,7 +55,7 @@ "message": "Mipangilio ya kina" }, "advancedSettingsDescription": { - "message": "Vipengele vya idhini ya msanidi, Kumbukumbu za Hali ya kupakua, Kufuta Akaunti, mitando ya majaribio ya kuweka mipangilio na RPC maalumu." + "message": "Vipengele vya idhini ya msanidi, Kumbukumbu za Hali ya kupakua, Kufuta Akaunti, mitando ya majaribio ya kuweka mipangilio na RPC maalumu" }, "advancedOptions": { "message": "Machaguo ya Juu" @@ -246,7 +234,7 @@ "connect": { "message": "Unganisha" }, - "connectRequest": { + "permissionsRequest": { "message": "Unganisha Ombi" }, "connectingTo": { @@ -369,9 +357,6 @@ "directDepositEtherExplainer": { "message": "Ikiwa tayari una sarafu kadhaa za Ether, njia rahisi ya kupata Ether kwenye waleti yako mpya kupitia kuweka moja kwa moja." }, - "dismiss": { - "message": "Ondoa" - }, "done": { "message": "Imekamilika" }, @@ -1325,7 +1310,7 @@ "updatedWithDate": { "message": "Imesasishwa $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URI huhitaji kiambishi sahihi cha HTTP/HTTPS." }, "usedByClients": { diff --git a/app/_locales/ta/messages.json b/app/_locales/ta/messages.json index 9dc4e8d93..49dff7b87 100644 --- a/app/_locales/ta/messages.json +++ b/app/_locales/ta/messages.json @@ -1,10 +1,4 @@ { - "confirmClear": { - "message": "அங்கீகரிக்கப்பட்ட வலைத்தளங்களை நிச்சயமாக நீக்க விரும்புகிறீர்களா?" - }, - "clearPermissionsSuccess": { - "message": "அங்கீகரிக்கப்பட்ட வலைத்தள தரவு வெற்றிகரமாக அழிக்கப்பட்டது." - }, "permissionsSettings": { "message": "ஒப்புதல் தரவு" }, @@ -20,9 +14,6 @@ "reject": { "message": "நிராகரி" }, - "providerRequestInfo": { - "message": "கீழே பட்டியலிடப்பட்டுள்ள டொமைன் Web3 ஏபிஐ அணுகலைக் கோருவதற்கு முயற்சிக்கிறது, எனவே இது Ethereum blockchain உடன் தொடர்பு கொள்ள முடியும். Web3 அணுகுமுறையை அங்கீகரிப்பதற்கு முன் சரியான தளத்தில் இருப்பதை எப்போதும் இருமுறை சரிபார்க்கவும்." - }, "account": { "message": "கணக்கு" }, @@ -563,7 +554,7 @@ "unknownNetwork": { "message": "அறியப்படாத தனியார் நெட்வொர்க்" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URI கள் சரியான HTTP / HTTPS முன்னொட்டு தேவை." }, "usedByClients": { @@ -611,9 +602,6 @@ "delete": { "message": "நீக்கு" }, - "dismiss": { - "message": "நிராகரி" - }, "fast": { "message": "வேகமான" }, diff --git a/app/_locales/te/messages.json b/app/_locales/te/messages.json index ba588ca46..7fdf1ab84 100644 --- a/app/_locales/te/messages.json +++ b/app/_locales/te/messages.json @@ -57,9 +57,6 @@ "details": { "message": "వివరాలు" }, - "dismiss": { - "message": "తొలగించు" - }, "done": { "message": "పూర్తయింది" }, diff --git a/app/_locales/th/messages.json b/app/_locales/th/messages.json index 16b608225..8e1744e81 100644 --- a/app/_locales/th/messages.json +++ b/app/_locales/th/messages.json @@ -1,10 +1,4 @@ { - "confirmClear": { - "message": "คุณแน่ใจหรือไม่ว่าต้องการล้างเว็บไซต์ที่ผ่านการอนุมัติ" - }, - "clearPermissionsSuccess": { - "message": "อนุมัติข้อมูลเว็บไซต์ที่ได้รับอนุมัติแล้ว" - }, "permissionsSettings": { "message": "ข้อมูลการอนุมัติ" }, @@ -17,12 +11,6 @@ "reject": { "message": "ปฏิเสธ" }, - "providerRequest": { - "message": "$1 ต้องการเชื่อมต่อกับบัญชีของคุณ" - }, - "providerRequestInfo": { - "message": "โดเมนที่แสดงด้านล่างกำลังพยายามขอเข้าถึง API ของ Ethereum เพื่อให้สามารถโต้ตอบกับบล็อค Ethereum ได้ ตรวจสอบว่าคุณอยู่ในไซต์ที่ถูกต้องก่อนที่จะอนุมัติการเข้าถึง Ethereum เสมอ" - }, "about": { "message": "เกี่ยวกับ" }, @@ -200,9 +188,6 @@ "directDepositEtherExplainer": { "message": "ถ้าคุณมีอีเธอร์อยู่แล้ววิธีการที่เร็วที่สุดในการเอาเงินเข้ากระเป๋าใหม่ก็คือการโอนตรงๆ" }, - "dismiss": { - "message": "ปิด" - }, "done": { "message": "เสร็จสิ้น" }, @@ -665,7 +650,7 @@ "updatedWithDate": { "message": "อัปเดต $1 แล้ว" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URI ต้องมีคำนำหน้าเป็น HTTP หรือ HTTPS" }, "usedByClients": { diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 4ffcc0663..c8fd839dd 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -1,10 +1,4 @@ { - "confirmClear": { - "message": "Onaylanmış web sitelerini silmek istediğinizden emin misiniz?" - }, - "clearPermissionsSuccess": { - "message": "Onaylanan web sitesi verileri başarıyla temizlendi." - }, "permissionsSettings": { "message": "Onay Verileri" }, @@ -17,9 +11,6 @@ "reject": { "message": "Reddetmek" }, - "providerRequestInfo": { - "message": "Aşağıda listelenen etki alanı, Ethereum API'sine erişim talep etmeye çalışmaktadır, böylece Ethereum blockchain ile etkileşime girebilir. Web3 erişimini onaylamadan önce her zaman doğru sitede olduğunuzu kontrol edin." - }, "account": { "message": "Hesap" }, @@ -556,7 +547,7 @@ "unknownNetwork": { "message": "Bilinmeyen özel ağ" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URIler için HTTP/HTTPS öneki gerekmektedir." }, "usedByClients": { diff --git a/app/_locales/uk/messages.json b/app/_locales/uk/messages.json index 61be52fbe..3126d445f 100644 --- a/app/_locales/uk/messages.json +++ b/app/_locales/uk/messages.json @@ -1,33 +1,21 @@ { - "privacyModeDefault": { - "message": "Режим конфіденційності тепер увімкнено за замовчуванням" - }, "chartOnlyAvailableEth": { "message": "Таблиця доступна тільки в мережах Ethereum." }, - "confirmClear": { - "message": "Ви впевнені, що хочете очистити затверджені веб-сайти?" - }, "contractInteraction": { "message": "Контрактна взаємодія" }, - "clearApprovalData": { + "clearPermissions": { "message": "Очистити приватні дані" }, "reject": { "message": "Відхилити" }, - "providerRequest": { - "message": "$1 бажає підключитися до вашого облікового запису" - }, - "providerRequestInfo": { - "message": "Цей сайт запитує доступ на перегляд вашої поточної адреси облікового запису. Завжди взаємодійте лише з веб-сайтами, яким довіряєте." - }, "about": { "message": "Про Google Chrome" }, "aboutSettingsDescription": { - "message": "Версія, центр підтримки та контактна інформація." + "message": "Версія, центр підтримки та контактна інформація" }, "acceleratingATransaction": { "message": "* Прискорення транзакції за допомогою вищих цін на газ підвищує її шанси бути обробленою мережею швидше, але це не завжди гарантовано." @@ -63,7 +51,7 @@ "message": "Розширені" }, "advancedSettingsDescription": { - "message": "Отримайте доступ до функцій розробника, завантажте Логи станів, перезапустіть обліковий запис, налаштуйте тестові сітки та персоніфіковані RPC." + "message": "Отримайте доступ до функцій розробника, завантажте Логи станів, перезапустіть обліковий запис, налаштуйте тестові сітки та персоніфіковані RPC" }, "advancedOptions": { "message": "Додаткові параметри" @@ -252,7 +240,7 @@ "connect": { "message": "Під’єднатися" }, - "connectRequest": { + "permissionsRequest": { "message": "Запит на з'єднання" }, "connectingTo": { @@ -375,9 +363,6 @@ "directDepositEtherExplainer": { "message": "Якщо ви вже маєте ефір, пряме переведення – найшвидший спосіб передати ефір у свій гаманець." }, - "dismiss": { - "message": "Відхилити" - }, "done": { "message": "Готово" }, @@ -1347,7 +1332,7 @@ "updatedWithDate": { "message": "Оновлено $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URIs вимагають відповідного префікса HTTP/HTTPS." }, "usedByClients": { diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index ab7eb1c11..b63bd058e 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -1,10 +1,4 @@ { - "confirmClear": { - "message": "Bạn có chắc chắn muốn xóa các trang web được phê duyệt không?" - }, - "clearPermissionsSuccess": { - "message": "Đã xóa thành công dữ liệu trang web được phê duyệt." - }, "permissionsSettings": { "message": "Dữ liệu phê duyệt" }, @@ -17,9 +11,6 @@ "reject": { "message": "Từ chối" }, - "providerRequestInfo": { - "message": "Miền được liệt kê bên dưới đang cố gắng yêu cầu quyền truy cập vào API Ethereum để nó có thể tương tác với chuỗi khối Ethereum. Luôn kiểm tra kỹ xem bạn có đang ở đúng trang web trước khi phê duyệt quyền truy cập Ethereum hay không." - }, "account": { "message": "Tài khoản" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 0639d6bdb..265f39e11 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -1,19 +1,10 @@ { - "privacyModeDefault": { - "message": "现已默认启用隐私模式" - }, "chartOnlyAvailableEth": { "message": "聊天功能仅对以太坊网络开放。" }, - "confirmClear": { - "message": "您确定要清除已批准的网站吗?" - }, "contractInteraction": { "message": "合约交互" }, - "clearPermissionsSuccess": { - "message": "已批准的网站数据已成功清除。" - }, "permissionsSettings": { "message": "审批数据" }, @@ -26,12 +17,6 @@ "reject": { "message": "拒绝" }, - "providerRequest": { - "message": "$1 希望关联您的账户" - }, - "providerRequestInfo": { - "message": "下面列出的域正在尝试请求访问Ethereum API,以便它可以与以太坊区块链进行交互。在批准Ethereum访问之前,请务必仔细检查您是否在正确的站点上。" - }, "about": { "message": "关于" }, @@ -261,7 +246,7 @@ "connect": { "message": "连接" }, - "connectRequest": { + "permissionsRequest": { "message": "关联请求" }, "connectingTo": { @@ -384,9 +369,6 @@ "directDepositEtherExplainer": { "message": "如果你已经有了一些 Ether,通过直接转入是你的新钱包获取 Ether 的最快捷方式。" }, - "dismiss": { - "message": "关闭" - }, "done": { "message": "完成" }, diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index 6e4c40034..71253f598 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -1,19 +1,10 @@ { - "privacyModeDefault": { - "message": "隱私模式現已根據預設開啟" - }, "chartOnlyAvailableEth": { "message": "圖表僅適用於以太坊網路。" }, - "confirmClear": { - "message": "您確定要清除已批准的網站紀錄?" - }, "contractInteraction": { "message": "合約互動" }, - "clearPermissionsSuccess": { - "message": "已批准的網站紀錄已成功清除。" - }, "permissionsSettings": { "message": "審核紀錄" }, @@ -26,12 +17,6 @@ "reject": { "message": "拒絕" }, - "providerRequest": { - "message": "$1 請求訪問帳戶權限" - }, - "providerRequestInfo": { - "message": "此網站希望能讀取您的帳戶資訊。請務必確認您信任這個網站、並了解後續可能的交易行為。" - }, "about": { "message": "關於" }, @@ -171,7 +156,7 @@ "description": "helper for inputting hex as decimal input" }, "blockExplorerUrl": { - "message": "封鎖 Explorer" + "message": "區塊鏈瀏覽器" }, "blockExplorerView": { "message": "在 $1 觀看帳號 ", @@ -208,7 +193,7 @@ "message": "開啟" }, "optionalBlockExplorerUrl": { - "message": "封鎖 Explorer URL(非必要)" + "message": "區塊鏈瀏覽器 URL(非必要)" }, "cancel": { "message": "取消" @@ -258,7 +243,7 @@ "connect": { "message": "連線" }, - "connectRequest": { + "permissionsRequest": { "message": "連線請求" }, "connectingTo": { @@ -381,9 +366,6 @@ "directDepositEtherExplainer": { "message": "如果您已經擁有乙太幣,直接存入功能是讓新錢包最快取得乙太幣的方式。" }, - "dismiss": { - "message": "關閉" - }, "done": { "message": "完成" }, @@ -650,7 +632,7 @@ "message": "無效的 RPC URI" }, "invalidBlockExplorerURL": { - "message": "無效的 Block Explorer URI" + "message": "無效的區塊鏈瀏覽器 URL" }, "invalidSeedPhrase": { "message": "無效的助憶詞" @@ -1329,7 +1311,7 @@ "updatedWithDate": { "message": "更新時間 $1" }, - "uriErrorMsg": { + "urlErrorMsg": { "message": "URIs 需要加入適當的 HTTP/HTTPS 前綴字" }, "usedByClients": { diff --git a/app/images/eth_logo.svg b/app/images/eth_logo.svg index c7bbbb282..42be3a2e0 100644 --- a/app/images/eth_logo.svg +++ b/app/images/eth_logo.svg @@ -1,8 +1,18 @@ - - - - - - - + + + + + + + + + + diff --git a/app/images/icon-128.png b/app/images/icon-128.png index ffeb4563da48ecfa753b0e09c512297f0f35f03e..8d593bc0c92188c81d467ba5e9a20d70831b5ed7 100644 GIT binary patch literal 4074 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 literal 4334 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 YzM9OGcjEsiFaSVX{efDQs#WNJ0Fo9nwEzGB diff --git a/app/images/icon-16.png b/app/images/icon-16.png index c7b3248b3007c59a9d57304cce6edb156d08b985..3343f97743f356461d08b014eb3a2d31f6c395a7 100644 GIT binary patch delta 582 zcmV-M0=fOS1KtFXBYy%VNkl~4}8k8q!$wr#h$TWi0L zYTLGL+h%H;n zvD|YvR@mQGlGkIHO)*{|);5lCt%tyyAsioziwOH3;FgwH*-((@$ADn27d$k)8#+t_ zF83_ae)LVP9<6n2@MFX5S`m(QjC_E5C+$Zc)zTEY|JeJ*ZW0grh&13qEbO%8PotPo zgI8md>{iUF6Mw$m-!8!In8RqdkPaWA6qmGFKTcdTht$!XVYIDLEn z`y@ZhJ`@+u^#bvHK#0@2G`4jtp9UQ4QWiJ*3A`BKcYnrrSU8qF?I+GW?H_@-ZTH{U zEqh0W-;!{=9m-4EEza)<&#=2X8tWJ7W)Fokt;^tkEb(GkdF*UofSv8~?Yu*)3ENiM zdA#4A&NDawuHjZvsJeC%nJmkHnzf5I6-t4-6)!c;rt7kVrG0yA`0BQpE Ur=gZW3IG5A07*qoM6N<$f*X4;IsgCw delta 414 zcmV;P0b%~$1h)f_BYy#YNkl@M)e8#Q#Bf zU6Ne!gL;Lu#|=ukk7|{dJ+4!EzARP&BqxVebB&v<6-XltPYaR%|EWvm|7Re+Tdnec za*+K0dUx4F_-tPQ(Tu=XE0q6#>rweXJ6r+GuXU5H#Am=+WPi>3vy}cnZ&U#r@T5WI z|HMF`3q0he;WPjka7D7hcEOxp`EWdVoBto)wEXO`tt$lvPZ!1m0F$c)P9g$T)&Kwi07*qo IM6N<$f*fGSqW}N^ diff --git a/app/images/icon-19.png b/app/images/icon-19.png index 6263cf3cf3585c56e2435c84ca2a3d74668b1a4b..5257d444aa668702214dd4eac7d58216c4a12cd5 100644 GIT binary patch delta 882 zcmV-&1C9KP1c(QaBYy)=Nkl9{okO#akoUx zWn+v=J;)q}8No`b3qq8iZI4&|e7jh6*N0m5jMp{l>Q@`I*{~S^5(mkDXi(3%RjjdV z8-Qza2)Nw};`nb)l8euLwr%^pO>Hb|uj*}$U3ayuRlDok)ql2ae&)?h&P_rUXtfwYx%d%n<_thHA!ZKN*WAUC+`U4sVm-@Ja{@IKV8VuJG9(%2)l0 z0Zu3^V7AlL?tcan`A_5X41D1`<;y;ps2g!&4gcjI@Y1*5zR3E>zq}b~s(O!0)xHQ| z&`HtxduacJWmktp1#yM{y5I7;)F9Qu&+CLW(;R|eM(*=G ztN))90HG$%VizXmpAzLu1X{|*{BC9QELc6&5e1+MLVqF5^Ho`WQ`TRQaXBvn-t4P< zjywCA&qkN^>YaNRdX8M~**@Et)+`8M21a3K#Ua4-tiLVej>z~unQqC{^v-;a1K6|J zFI?DTR3k{d&_5LLTYtb${UP;45P^DK_&Lje8weWxoNJ08*X@~Sivir=)$=;kyW@QE z4rs0@J%8<<_iI(2{;i|98!><^Z|A+q3f*m<=BhUMLg$XPTjq6y<fBI}i@l!k!RUO4Trhv2h_uh2GAT z>D8FP1w^3`ptNYh$YjJXgJD)PTAB@U5Q>xx?SFA`8Xfdren1^wg)kH>^KoqSrCr?h zzT@)eCb;VZM_1$RVlm5E; z0&e@bl{?>o+dpdMmo*n4%a2T3+oG?V%Ql|#?U_!^-kQ5*&G$}DeabuI(Yn{iQnL%x z{A+=vW7f8T#mAg4t-U{9f5}Z=>;SUAL9eD=@HwK=tQ>A@457oP{wM^??XVZa28VExnv0Pat+A4&h`>i_@%07*qo IM6N<$f-9iTEdT%j delta 498 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 l2d>}*k)M6}1N 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 RS^LLDU)w~hNYf$W{{TRNWrY9$ literal 35988 zcmY&;bzIZm7x!l{7)XP3g90kuJz6BC1Oy2YkVd*UKtPo421P=Ykd)j2rAxX)y1U2r z?E8D3f1bTwdu^}J=bm%#iF?lboO`36>pUg7$8Zk-0Fr0wD*6C`gZ+yG5a45f&iux2 z03gQfnTn#JA84;V6#EQ-7upzR+P4Ir(tYf)#!7DZdZK}N=rxs09LSXB09;iuTBun{21P!b!}~TeMDx~q{Tb$m`}yvc z_(K3LNB|Nkqg#6Ioj=`B^E<8VM#ch~E@M{oQ%<#a3jp2=az2mFaBkml^0!BvMc*~@ z-(a3^JTtuh)qJ}W)&E!{5+7iLZ$|~i8fm3X$7cUCF3bAr@!H}{RqsQu@HP+qJpc|A z)3Qhgo$IW0aUAY7xLlK!@&^2E)_6=&_{JRoz7-Qz`DJLwLk-V4}GtPQg;l}WuK6^?CSb3b`kuQ=o(miyTZ#{_5z6;-_ZQVMh zt>4LF6t^tm!jJ$81O)v3zj=Bo1I<>lPVya#0+j!wBLFGZtshd|1_i#tY>)81m6Z9s zh5!Kspmtt4B`t9`-DH{D0JDLyZ!;6*zUe&Iz5t-4ASiLxHQ7VeT)nUa#l2|@VSXyZ zC#M&aguuh9>~DK?hUibH&7qLz=~ZQR&!N5-x75ckcI%os0l@qoUFd?q>!9jCYK=8e zFb@V^0&zvBC9(cbNLF&>Q9@uDOstKESOVhbkUV<#uwvtOk{sJt7qy~|*(zQ%vbf9RD00MbeX$vDh~5468%erz;AmwUWk+&Fx4Y=Kqy6@n9tD$)4O z^UOvj(dqiO9<`Y#k-9}kC`;TaIOw@<&@ZjA5eB<^a6}a73tAFSx&xtN%}ulpb))L% zxsqB22ApMV)jj*Y2F%rY2YKEF4h#%v^<%%!IAwB1J(z|uvF3bh^?Se5?oO_Q?EU-i z)=65rL3B98LjrigcbyswrtbPTwB&odN(70zxPfm*AfY8MULLc|NuKV0`p3;tYL|Tn zc`r#!hm$uBTaT1Nbq^Ag_nHLgJb6n{PtE?m=Y`d!EvX^fvMr_C!MlNt92>M%Y3I(t zP{s+Qyk@OLC%TJl5%yU5c{wT~;ka!Mhd4BQvXZy;TP3|JCK>L<+w?}jieQm|{k@kD zj`GpVh5F+(7S|$t^Ha^%`JFybgV#s>A=Z6S@;3K#zFLq!YGqZdJ58VBcw_sdN^3MH z(g?GkorUUQFd)BrdRuGS%;$s`2`5*Y^c58ZC*dj=tnIa;8@hk$AgeGs`BiyL9(+wLnu*3cH9Bi=rMQBW0xa?u1wG+ z`_)CW7?!nue#I~93{-XJjm%}V?AYXZLPKn);4(Cuc2Y31#XMcRhY4RE-KrFqeYIJs zF9X1SS*1Bc3(o>IeTK9gxC=$>;9cZ~A~1H9nL`yToyU{#iQ}$-pW4S1db$fYJw9nv4_>@bK{E(>11DPq}(2;dzHx5&}erK!iNmV^sO+ za{-BUK312L2GpM`=0CMCh}BfYr%?7%pI_r5)qa2P6Jw^W(b^a`)b4P`5r!_frd76s z{5>xFT5CS&qKbq992)erW9QvK<$B}w+G+m6ZV_64`F8FaJ$XlZ8Xy_razF{llyly3h@xpXp(#BhL-%B$t)&MTEI>KGT)(Dd2|;X;tRb8%jd~m zxnsuLkN;dvz`kUOpXC)yzD2$9nXb>W0}k`MEBk#T?|N-^TGl~Uf7ZYKEj7|yj1f14 z=xH=R`q-&kRSazXIZav#fU{Ghb1|mZR1&c;t*&b9+LGNa^sO%WqX=-;yF|=t?VX)0 zwzSf-o1I1IUE-5@uEhPg8Z*F9s-R)+V zqn`aN^m^ouo-!+T#B!zDB-;$*BEyTs?$8*U$!R+o*Y%~3YB%p$yV}!26m;&Bku(*f zZvV93qWSuV;^L#n3jZ##5HN&QAtT0d4*+H4*iTkBhE2`-X?_!dH={Q)EG`*c(P?}x8DRXQRO1tmKSrV3)ss; zWpZ&xcCn$K^jNkxMRT&_QKdSeyR$=5Y@R%GM8SSp$mCq+KTT&b6w z4_mmlO?Q{;pEAdRm|Ff+CfLuCT>rUlw?&jwp?_MNe-$leC!nfJV7N%s1(e~|K2tvD z=iKehi-F|70OIyE3Z=Ef&|aC~g87qUIg>nz{fJN)Y2+Q^XVYSpeD=}s(cMnl{VUZ6 zPoBNv2wE1=7N%-BToNyf@8JnU0D@raFrsfp7Y(7&V|E80jWC@uA&a^ykI^pJ<(Bt1 zaQp1LirD+lOIGTdz+SYM5kt&ws4I>nXwk;4h`+2GHMwKDpJdCS(QGl}N{%U#oRjcW zSl*$blVzAMm!WEvpEc220)lV89ze#x&2lgGt!3O{OcXo&>{@$4lO_P<|^ouYg z+0C{wbF=8cro`sZJPIqiGe)NOYGx_7QV48aNTVO_Cwv3*8^ioLc@NcdgJ0(~bYIyk zDl4eBHbQISBdxTvh3dY`9oq@m3=YX`AgTgbj3!zcl6Pk|-g-ygZ7`yOj7Uyr+WR}c zZw$=;B2KIJ0Z0_5mz5l&aglz)u2xt5)p4f}Z5-|3>O0Lf-;m_w`rp4gww;n%5ll)( ze`xTijLfmgNv7IZy7^_qw0-sjwakpBOR_%g5$620)u1d+qSJg|@y}mTg^oEI zsvj4xbirZ|Daxb-@j``rE|-d;xHqhxTdYfMM3Q``L`(TE z5*kM!8c)%?^3x^^8_A1}L!V{oH5+Obz>0J$N{$h^qq?-)ga-#tyXFm1x&*TT9gb3m z3QiT&;+_`z52P<^9U{8+{_nq;$)zb^QMJXbaTHrtra=iQ^7=PuiCiBlUUW7MuWOdce0Gzn4!AHG+l=t5+(J` zBHqZUe;TPA9nvPZ$eF0L!qu2!+fj`N%fZJ=}}GQtpP&=-4yS0U#j;wzd+&L9Qi=jbFWoAjbWiVz_Wh9YcWMn7M!rx+Oh3o~&`w zq!M|L)&Js3C$Mf<8Mh+^)>tJv@HC}SfZzp2Jgmxqh>!y9(3QZDeLYpSwX1sx`ndHN ze6ShH&h|BDjYsfN5c!sO*5bGgqo>^575h*pOyPX=@<~uA=&fwIM`bfC@^E{aX$krM{ID-|$E_!8P&yOwX9Hlep_^ z&{WVbU6Y7qzBYTtkch);kb0i0$lB&+>Pq#}1~+CQ&T4c-R6+Pq%L;Xc$sPUs1FE1L zT&DB)JW$~GQZzhapB)A!ZtrIWMJn{lXuE}Q*|6l-#0n4-oU#yyAUd8Fi39}(Md*&- zqMM;Jq9Of=AE;MTUqdLuUKY^kugA%-nrzfgXV2Fzu0ME$`ksF6(~ zNg$GK&z*h3x$8Fc*zgQ9?-j&cGTAnvlS}66Znj9%=zGtGfPk>a0r>!Ec0D-W%M>qc zcak96^&GUNyNE9%?=11JuJm%KOsIJ@-VkA*{uq;=7u^}8lOLmdat`GvrFLXD()wB$ z@bqQ+ee_Df5P=?T*p2E(JI-WQJd)5PsxH?&mlnR^axH-d7L#o4o)w*bL{(H53hHt1 zY`=A{fy<^(V32I+mivWIJ?s`^2&Rg&0Fft-l9As>f3De%>&hn|C*xSc-FvtA_52vu z1kHK;NXnItr9nPcaiVbO$Ow8Tdl~+{>J+5TV#-2%nt2~jqP&u}M5W}ER}fqW(CT}e zTYWKw>U~|kk7no6|4GJPDyMRAPs;3xbTyYoKF*Dzf*ZAmFzA43Vz2J*4(G2 zZbP1(L+?&0WDzh2my-4Pu0hUwJ13_J3>&AXHFnQrq#C;e(0=D%Lj(cA0MI?_S4kl-LVZxD<4PxCK1nS;Nlo=>F z6>@MBlVA0k3NiHXG?t;k9(0SK4N&p`8+^`@O!s7G@SZwKfFS0{_rK*-?NhS$}7^6 zaL06wgw-|Y2Onl|2@U0BX~^j~js{(=~H8BV!o+k15OM1Ej1esj3v@=nnnq46R1V{Hg)*vqdp`XKvO zgRz7%RAoC6*Et1x{DFFiuLCNq!#Io7AMF_%)i&8R5gQeY2^{H`iC=%flFt3aGPw4 zSlsERL-U!JNTK=%n$>m(mP?mrk3w|o?L{}{8!zWQNl}Nns3k~XegMm1YvKBB9{OF# zM(I*{*-}HYiwjX}e#xo83UC@u(tE_U7%w2f)p;v?{+(;trqxnzsajp^H1kBoBIF|f zch9Hd<*1G?cxyImCL4%M`F2*}a!+lk5$d$S3bOvzqAKZ2b5vjnIym)b)Vb7*!=09I z3q#;PWwO98><{KP7#ii!2Hfd)tYVf!J)a(*+3MW%tcX*uk#znrR#wqt|sKhp!e~{^oFX zQ)#LDB!Cr>CRbW-TsMBwpI=crxOgsOBy6?0wAv8UQq}<_r=&Rbe%5aig#*3OC(&fF z)ccNO7}4J4MbKW~=M^t{dfnbARxQ-9Xo*+-aC#SYsF|Z3mPl)K5(TnPI#I1-Y`1hH zb;`EyS@?d%&!pJ+MUsrIq01bvAn+;771Xw5R;p zj>C4wwGzh;E7rk{y3PVO@%+f)1?JCWWQ2gyVmo=K?2cq~P~0~Q;k`ftCw(v10E1$mhpE=~vLmUc3@ z2K?rqk^!kj{IWUW9kF7gN5C|vrt-nJX?mdmy6u7iC_qWEnwBDlE~RC&uwL$U$&weR2fo853+c_Q%qySLkeQ}4QD9Od z&Z_XQHokE=;u<0^E<83sY*&{@r?fO{A8&t!+t6pK{Js+5e3f#Xtvo8+Of-ms+$RVa zs!)wU-Wvy-n4N%tv#M?erIvV$_4;3xwhhe3tTJ0vVTq)DN7^eTr$KFgzpo6dk##U2 zs_1p({6YlVE7w*Y0z>`BmH)`nrw*PRtnz0X4$Kd2ovMT-;`g1$hCWS(+2SqT;g-sc zLR$&yKz%L-&lqqOFV&AQOEpF!th8?3u~&ySUy~_e%3A?c*L#hhr^Kfq7c|)@NY&X} zWmm^P`U+AmZckJ>*v_ivXg0Bu7$k|Q;T*PYFq{aDsz|jxI>I|=p1#zA4RV7yXD7yLP20iy&LJWUrO?ve>a(6 zEn=sp^MspryeCpBL;#g*o0Rq~4RCbM4vG=g-*F?*8Q*H)EXqVf+^6@sA(2GjR9T>7 z^2HkH{>z!ee(uuqkYQA?EmObW`@}env)F=d1FsJ**U(ox9Z;a(p7G=}J}+L1GR(dW zFO(cLB^qISD_glfn}OOx#Qi^lt%7Bu9l|-KVHl6H3(&vp8ftQnC!8>h^{g6Z$(Y1q zK08oqqcJ|{H$z!*H>@sFbj3BYmN1-mxiR&1w?CbPsFcKgI8)J!m^a))6raIB!a#}= z+Qv3D(CrziFLX*9<7M>44J=FKPIhspEXB9^9$B(lXPbeiU{mljoGt4*==8qD;pOWi z`|XTgTqn%GVN5uflO*+Dwt~Sp?9JoIJj`qkIrsD|lq{&2gS zz`C8hr^lb*7R~*;_@}YDf}=AJ7Ky)|s!!{kdQ$U65Rwb6#S#rta)CP{ML*u6E=}9b zMOZ8QCDR%s{+4ehT7CW28-nZ3yQ=u(uG5eg4|Uv^MTw^{Jvrpt&aia1RLz5&N|A3A z?JdoG^R@X|3q55by_=CQh7=zVe7LVk!cS^g_FiG+C)Z0X4)jb2Ac+0tOIh#>__68cM~ zsS9fS8Q_};xg=XQ>9h@N@V#GiA7Io!AV&KN5caJLJU+nP&d5UNAK#n`u(K8h1KjLn zz5)*{IbK`jc=eVtu(h`Zsug??Zm(*UTdR;mA6K8)(Z<-6-U11PP76Su7I^ly)CS`576yI znjmPaq=|wHum(Ca{2PFQR+9FrSo_}fpyTNfc3?a2RwXZ8RBiD^}i0`){ScM1AH(sc%h6}Y3AUM1)-P$@oVSQE;1ZytFCk(w7 z$ItrGj1kkeo)H@6eLc&Xl$N=2dZDehiktI3{5F06XC4L^`har8RVcCt0%~^e>Z-T0 z$$Cmd_=aQ)H_}zYD452#1d!B&4_=ICRyvkn+KsWo!M7XVis$8AU9gr2*0G(sg@x!1 zKG?nHydM4;Qx{|Py08sESgLeHXGzR;sy=uCH%`aNtKlvq0T{ENObu){muT1jB6|?U zXkA3FsHh3JzcVSngT$%-If?==Zd9e%_Q_3g_UMk&mHy}l9AVH~)VpHmjsVOXTc!}# z4+DPZ5^=>2Fx*?gyM(CCBP&#w!sk)$+n-Q#3$u>%5cO`Y)FqD#Gu$ye&J2?Syzt^u zj541(8aGz*hiBUko#YGPJ*b}D=;jTgIUTIoj9|s4C{N$lKmAJ|P+}#1umVleTh+x9 zFjL`Ryle**!`Pgi4E%wIjvK4Qm6zP^3#l!~x*R)*c#1VL?oj5H(`dinqAz!FU!soz z-u&`D@Kl^0JGqaSRtQkAh#B};rS-DsL;%uuo9^@bgHTvlAPBHjkl3C*gCMOf+N{q3sz}F9LxByTGx%+G@ z=CP~_BA+Z#RxQ7*^zAso40Q#-q@?r=27k=|Cb$=#jJ)D#;-ywTVYQ-B(AGQLV|v9! zS6@q0Ph92mVCGQs)1WXu%>BM*D`^X67mFMz*0c}9p=dCrB{6Iyy*4Ix`UWC6FMO3J zg6rY-7cVRUGSZpnK-_GfvJfpaDT~)Dmi4g9?lekV&gp=*AZ8ix{^MXoZ$LC zSpQ)##>a@SqVV1`+Zs%45q9`2%at%IVKF`qLYM8DY#GjHlzUoZ8zw|RCxo1Q$i206 z__Se|X4Nxl-)_X`3ju2tFA$Km!s?ZhfC89s^3S%}@3K z%PSgvrxAmR5l9_o^UBimmR?CPtivI2Il9!E6B|AM3AZEKWF;xVpSxK|Q9IYZ>67jq zYxk)yL)+gBpoBPi@_Znu@h8s&9V^JZbb~I*;6;E;*eoE$m>u=xcHvzBd2fh;x|oa8 zM+Vk6;6Gz01ypI6TEh?n9{`Ii_&(_|vQ5!#-Pn})ZIDN1XbqgQ4iS7TSyz-vJKp;3 zjR7I)P4WGlO4OX;o3Jw9c{1>mw_u-`p2^?GIt z;>DeGfS}RdCSr(KM^DpIeJXpj9;b#O8mslRNJ!3y8`zG?Djp1QSIfe^yBM9lqgoo_ z`QYM8YI_Q_`JCLZC5vM37Ot0iI;Cal$3dv}Tr`;U%!4EUZswA?qnA^r8*vMPujeeF zYlxgAN0?2H?4Ca5C;uX#P)U_$DJS&2Jrj6tWR_bBeQ2gG820S%rh=~Qr{aejZQFy7 z`c*cd$Y1SC2}@83S`=9=dxR|q6>fltSuI zCZcykYZXR`^v$POL;6%@UD5$O!M(wo%IB!fTo4^NWhsIA7z%4VXJ{wj()}K;tuhcn z2LMbh)JOZ=cdVLJ8v!CVu$zgfqJIzQ+SeSs zt9-^`0(Jmkp`~qoIYWgHJF}PAWo57gk{|s(8EJd8I~w}oU4>_d44Jhd!~bZ;eV1tzfbpA6pZdn_ zy_WNxHsX;?7qXxc&}gzO!H1_Bk_1Qvu})23Cnp4nmiUcsP7~zTj`lgLZuYI4@rxg%<@%8plP`?@3jYj4s{4EFWH=Z+bpSzvl9(FoAd*BNi|CNXb zXnbK4lS?%)aVhZaRTejHh32|%UcfZjuWls+2YVbrf~o;6+1G7@ZE@g2R45H3vzHWz zsgz0kY|F`p%*C{7I(j?|(Jae$8EX+0WQ8qmYLXz)w*v(+=}m70@j>CTR7a#TI$?St z3fZ{YDuek_dc=SNqC8|t`l0a>LU!_D=jEafbT}ZFmB_mlA!U+r62wd%i4pV!-i+@S-|s6JkLACI|^J+rd=&A z0`591q56W-k|P@mG8+)fks^FR;a1k*V(kF+MB(lSC>8?D%_*x6=9Ff_8RNt>WeDKx zQO9h{b&PRY{B-TpvM_i)2PLazy-MvM(Ox?Mc#W7>?%W#lUWtr@m&+4W^CiRxmR)0t zT1f?|1beFS`qo?LDX*R59>E`Yc^n;u~h}m;{A0#I9v&e`PC#&8&s@y>F!`v zehLKa_vUwUr;{~MsI8B3gu}2slg?bI9T55OOP5^zQwV@1L9|HHqP{DbA1(llV@16P zzb}d;IPSr;bSM!NN=B_Yf;^@Rk_T0|-AZ_jiz=YS5-np|J-HkY46beAeC5``4Y)oj z{E*d9C*^AE3zaF5Brml}ZJ3+Rmsxz;>OKD#rmp^s@0?=%c_V z`u`fhMWM}SOu*j21}G)ObiM7=@y%Gs!|->TZh6?~uo)e`g<>L9)3|Lh9iyUv4Qvcx zyjmNZGX?H9RAO)?f7rBr#&P2RWhj(_&;Rsw@nia+DSAZO3YSov`nfa?h2Y)J5X7w^SpP(`*RGfFb2ew7Dqo*(DVWiH+{OmK^u-mj?9mB z8*Q;0;h{A7(aZI&z{?sc?l^{(1=JID8{3mBV1-p2qqLYXl`? zndUqR>FvCB&DZ02=?R9Qd`os>hgO<7M2z|RlVd}G@vK9ee#cj|SDy8CpLyM-(u~E6 zJ5DD5fL!JXxcz*5ACN6eL`xT@{SDw~=dpbL2QgjNjIApNM1G|qtlbMR_n?REx$;|v zOPKcEYJ=X2KW6`G`H1E>-}hzNd>-15?amhdd^l>NMgsmd_B|jN_Bv$$34L8jZ}(fJ z=dFkgW}&4ul|dhG-%6Vi$oM4;xq#gqPK-Y-_cxv~Wof7q+J@`x4g68H> z(4)d=MI*GneuC9+R_E-#e^Y#!WaKD}pQ=kmP5!Qn!(TrcD?EG?In=xq>+?gWtn{S6hHvS9-P46kpKqrwoQyYKaAehq}f#kAS|6guLS)3A%OhoY!4N{!qXsNWvo>YzmkD|otP45>}u_aKH6vb)QS4cV@FouRU7H}CWba{%` zS;<&6de@XN8Zla9N)-Xg7xe+l7k=Zv&Ta~!9CSkajXUYDg0R^(=pBOJf&Q{cR+t>y zxV{~(Ol-{~LYER2*vU_de_O3wV`Jk@A7qiE0-p5`R4sW0-^9wRX$4lG*H?uw+_|MQ z=`r-9?FxW71L{ytB*STU*ZL+6|G@k4Qq1;B(dRp9f)4f9%MJ9wzKfs3jzxamq$A2d zshsXLTwxtiBNga0Ph|uM(3iY~8?)U3ivikBIPUhp`|{l^e6DVd=)gRV#gY`~u<*Y- zoh5{7oyc>Ddo)`+&%3i2Xh0@|9q9;Rg4WGECZm-YA6ziO$q_Dgl?i9dyef9d6fJ3H{ z5CK-<*)$m*xrO#e_K!I&miFNWnXr}jHNC~h(tKfv8?#%S!sE~qvI<+NYr2`VYoBi6 z0D7Fi<3N9c1XGFc(eTXd7i-hyp?m8ftr4}W=8|vX9eIdT3=THJvOu!fSPNBh-^r)n z4*}JHo^OYs_dR11IiSc*MZ^L82Sbve(PFQJ?(rfU+Z2mDg^;TiI0NLDNEMgOg*~D; zg>v31K{9!tC>-V69~iGen#9TptAt)LhXZ0n-gEx|h~{A}-jEMY3|+c#AM0CRq_RAv z770#RobB1yz6@5ZO|`V|4jRxz3o(~nPR8`^f%UxZ-Z?NMznvHH#5j?@LD$2!;E2DM z%TP^uHJ#t)39WDq6BPm^Jv|@`wL6MpDP9m(FkXiMRCN>$=)_uv$XdvsYxzwr`WN( z&t%_E^Ow8O*XDy;P@owQr;MheNsj;f)@Q)ap2IOGG502(9=5q3y34HjO6!kq6i5Rl z@8`mvxoCnvJzF*>tZ;6@icRb5Nye`l*1CopuoWHFZ7Ee=n=(e<@_l|z3c2CDu0Sv7 z{9fkOTkQ{ehe-}cEzY<7HBVLeai{!~Db0$^{pe89O5j7E8tc}m6Hp~@+KOBKG(BQo zl1gw~dI|-?RC|ZU1*ne^@)nyIz>=I+W*+nr7Zr|@O(8nFeKnq`uXT(o z>8zS-iC*s`zjt^4bN2q;I5sdD!B`y0NAABf(t+Pdpjl&vE)%U%Sl!nJaw}k`iTxZK zPFF+dLZ9D~p9ID+Ixp4oo9MEnrb|o@D%$2lS+K#?OO@~5oz}k!&l;bi(C@N}DX<2F zh3RgwV`%1^0rREjnjGjf-afKlC+YQkt6we}Hx6oT`^Md?jPXs2^$TCXyQCsfceVuR zhi^^4zmkJHBJ2X_EazOe*)n>E4u(jX8{hC>)2&0y@A~KTFh!60=TlVrEi=NFG5w#M zG^#5ZYk-?n{Ke_LxX>BwsT*3nBQ6V6WylAJY(L#q{!4)VUABm%p)A`cmPQ#$8#6r9H}Km zcKlhhNvXy;T>kW>+1s_?4KoIye0-J9$g5lr^{R1k2^hpVlRB8K@E@jR0S|mz1i`6< zD(t~ozPK$Pd!~Y%pMT}pkjX7Qp7J6VLtA@L?lqU-y%7lWp825}S@)QvrLOL#4@Y-g z?J908T-d@mHCF#6+3;-c5xxu zO@S;ot$#g8{EZag7yRz}#Okr#h0wn!HrXS|X&couZ98GR`HIQdq_og=kmOqz%p6TN zH8FZ*cPkZA-~;t)cAru^>|sNGt8j!j)8Ap6&{=eE{YS7pQ?GU&y-kUnQh(3K;=<$~ ztkkUN9#pqOk-K@RXb6KD&aKS(W1^haHgjo$tqfz;34qT)Tr~_`e7$BPDl~Gl9PhvV zIZSUg2kFGz-}3ioWX&@nPAr(cUe#_KBGqlTaG4(eG2^2^OEwvB)#&1CBuduzBGfit z>eBo~D2wh0qu{Y3+Ft7KGq@7OUUw`yT46XaF8LH_M-1nc$-uULp3jn0@oyi{XN&PN zXWt-RrLGMQE)QA!gcoDU*DM6pC5YO$e$*_5sB`>tTMh+isYki4KoCsawc{YH?+~TO z@at7R2#`?zXXA6ZSy%svH}|m#j{6spj*>iGY}I8IBhXP059W*z_)#fHxHu)hG_ZJH zsY;&bfemWJz%ST8nd^#8vB@rn5T5*>d96q&}B^M zoB;dM36;gFC+}&Dxq`y-Wy^eSxcS)o_Xx-F5vakwp#uY7hK_sIOu zGb5fYTH1-8^_u>%yWzMDl9&QNY-=Tojg_-xD2UclL06EZV7m29Ui1g~jw^E_yw^9UfE>bjNWqE0H z@%0lE{E@F*$Vu_Tjbg9(HL!d7;OkF#3*Dt&3ZKGd*B`9ZY!3Npk9M#rbjx6|VA)-c=1l z0TJq5F;#sW=V&&-7me{0hrs5q1MPsSa_#tPWOmm1m8`@OvZZb@r=ZjF!e4PUi4(7I z6lb6G>Zx0B(K1C*Hw5(S6k#8I56yGD`*1gDnslp95#C4mHdkf?i-=5*c4Gi3;Gw7Q zk?j|ue=vc{N1s{X-J+_I;h3feVF+4WHCPFkZKxO>uqI6)KXpIt*{eQDLl59>xi>A;0hT(tO@pWP*2@$ zi=#+c>LF5Bi^&LtfC$h$Cxu21R(y7#TE>yC&dl@POpm9+LF2mr@^iWf`kdDAdL#+C z9aY9E=)v$#=}kr^RN_$ywORZql5vcEL4J;S^Kft5C1A?SpAXwpvyddb{gk|nq-3NA zE2Qpbxb-c9!w{}bnWSQ%`i9*);*s2*Awc%eRy{R#U;4-1;1Kh1TAOv@$r31RA|FqR zr-vibe9Er43XrqB~7L#h88cal&l;+orp zEvB?m?*)HRY!=f~aIkVYF&8hf?-PqYI{v@hkYw%SY|Hh1WH?CrN5{8*ouL?V9IqPf z!9`ZUS$+4||KM%z!~EDNc`y4Dk}!%CR-KOD7bG}e>>?l_^37!ovd-o(3V*Av6+w(N zWAQHCPuQ;U$G7@3ka@z0ZSB}bQ6)%Vt7Zx@bc07Y;B^tWB@Kqr!RW1`@QXsHy5ulY zcH>qR+|&Vg3)~40!8hu3eW zdq&Mm<4z#sK89{Wrva-@eM$)d`k0jOp+UV-1D1q;O9MZX6x7h{)F`6(2Y!CUntHJAgubj zldq=uyG$H9^|O`~dZgw>+!oWSlR@pdgTi*!G?D*o>qqa{4$Hamm;(HWm2I0tU&xss z1)C1IC0uV^fx<29)sxPWgVwef%rX$hP#}856U@R-)H0O!ZI)F+q>PE@pLUhK_thZ^ zBK7-0N_VlH=uK#cv%~QPo7MNI23%M5A;b07bipzvf>;?!I9fS^F2`0V|x%F z29nK#EKdx(8hq>r<~|RT9>}Q(mJ#zDksx93CH5_h4rEB^tQb9O0Dy-iXRvbnXR9gr zZL5euaL8erIH4LfND_42H~%z@qVmpd;2}ow`pkv~_*m5#QOTTw=aW64o*TZb!STQS z^BG%3JH^i#TsOnkaNaaPL2UHyX3Y?&NCi%5USQ5h|C+%AIwSn$2lwG6MKFmGqrc5C z9;*ea0&ID3bRi6fKpWV_aFJVq+{spk6mY5pvPLZUixsP68gRYq1PV@SfNNhI@Unx4 zGsJ5Nu;ME`$EJtJIPN$n6H9t?O(p*VF`qGTQvl$_b#^_UtTt(203=0Vy`|OI8wI+V zjP~*fCtJP)bcrhs&J#{bSnv?%O1Q8OC+9t(dUlYEj52G8wMKTV z0y}04^g}Zs@RM`=emMx30-5yvj~$sT-rdx#!Butq?9#+2#&Su$Id)0q?D0FBD+IwfmGWZ`SYo^TNPCU0q9_CVa z@#X+FKBV;dgt`pYylF6y(QHcs7k&igz@v{Q5f+AMWlJyjO<|-?JU=e6-&>hwgDIN)b02bHM+4j|Y*?r;PTMpOyTsZ9 zE7Rcy=!b4&2wO{fI)LseJ;7p7B@Brh6$&!hji32`r?wHV-R1SjV9QG?E2r7XW~ zCsy_w4RqR4DLDAodX%`#8{gNoMJV3L2@?U>qIYs*p&IGBRCe9MTu*Prf(55^CZ>VO zW(Vj*A~+x+w51u@m|7t*hkGf_Z;|(GC}mh|P&!j@y}eWbIT>dfW$)_VKMN(U+C0s~ z!Q;jCDv*3pH(_+!%Ahl;MRRJV@5;GRU`32Rm%+!wnF@u;87RRP4`6p=;Ts+Ri3m)qNs?~3B})&U)y zWGLs)nkQa#x_6#)mJK?X&a{o_Kro` zyR3H|g~EwOr>^LdRQQ}OfQZ*bh_G>Ceq(SKF-6+wN@%4$_5<3A!pz7Jm5Ns%#_Pgq zb{3YJMt`juvau_@BX|(eHyIpD^KFEN$NO{j38t|b`_WwRE^qQ?zl0qM%0yL*K6sOq z)|>4qZ}0Qn-;Jwv$ydy??|Dss`Wwd0XH_r7?!C8Bfz%V?Ncg@D>FGlEaniCS(8AmO zJabV=ph|Z8$x9KT$>NC4!5ab6{v1hOd#yj@PK#@xLbbtiyQP6HM)$3MlaKl?*tx^^D_ua))N{x zI67wDTH09vlT05d*ofV&lVwkVFXmm4690p?;os4ABuv~d9G0gwNSGY-J3rg@=u@M9 z4o!clxn>btON$#|k+81v(enQHA@Tej0j)osGgShkOkGK3ZU47(fmKlvFOb#qTXR$F zc8D&}JA6ITo5A|(vzkpDuW_fM=mvzrs0yKo7r1L!WuK`3Mqr`&r>_v~lGR}nv$_4Q zt$XZLmdtLNn^;WdQ!;NPEhTvzh5ze-^lV-(@bBV#xLsBi^r$r z`u5*IK#=YmE*yThqGYKGGO5w8p~_)G+{%j!z@h2=CdKrm@Yu(g8E@&gnMpFz{iYYS zGkWNPUL)!86bT0_wMcH36%Ha!J6h83k`Wl2EERc1lQbOoyAT`0C zAm6nEW3Rk8Iiu3Y2veTQMo>c13!EDBbiS^=rXeCia$H_W2-6~wB`lT zY0LG@_K=3Hqs6)nm)Rcz&5((aO?%zA)nBn-+DCN%ho!F$h_d;@eRo+pr9)r|=@L*H zmXa2w6)7ockj`DY8w8|7Ktk%1js-zLTBK{~?#{jZ?!E8-XJ*cs=giDGGv|4qH#-jk z52}}rrUci+Y9);)i!C&(Jz8vX&u;>6GZeC+z&a;D#GlGXjOgC;+{iGy{P-e3rs&!= z9_Lo`!*pIa@i4tmXOjN6=WEwFwQD-krqxu0LZa5hJX~M-7W@E@8hoxM(U6uVZC}op z^XM4vAe_AP%S1L(@biEZD=%EDLH;?d*y>p{%^A&um zdd7*iVwdNKZFY}wj{R_@k7&;}UCv%k&Q^U|0boEG?o6;9*{^6aQ})%@T#1KPegl_; zH2#WU&ELI@ngSeipb_|^{|9%T8yH0$)<)^OZ(8lUT!ODrU9Lw_4{S+O9Kq_A8;E1G z3tqNJaV0O@miBDWWqjSIvv~fRq0fbGC7lXklw>s3V25_)Wz$5*M00>&N1%@cn4GJo z{fRF#ZC5lK`dc_jLPfsKR*8K1j+&|UtKQHJa`oeUYDd7Y^J=L<+rjfSX3^7ZxA3X- zZ7yv?qg2hJHC?IFk?&8-=%sjTs;ha7^HJnDy$LDP?|JS5qgz^aF9(ORYqIZp6<`gK zfe!Dvf(yG@*&qd|0GSXhFJjujTwY;Q2l$!Hul#2PAW|{*UjoQ}-Lj)xs?Yy1(fo;u z>I?!S)Acdc*h8rB^15do82~ihR;W$375{&0SHf&*mo{~6J7#vqJpu!bHY%Z^^^mwZ z@<(IZL^ffXI8OPU{U&mVp@aYK8Cp<4kVF?TY|7Qqgms6(85jGnI$plO*UCHX^D)>d%qq1 zk7O%)#9A_U=fXzg^`Q`3Z-^3alP-(4{kLEx>p@1;F?lb`Q~CEF9XpS{9z0chFvnp= z{rGvOL+X_$OS{_rCVyGKOMmmdSw(r4jN*F|ZR1q!ko z5If>H_3=d$T5(lV4%%x(#6Fr_%YJr$=q;+9N%SydaW@-mUiMSZ*Rpozz<)4(*B{ze z{Yly5_Z=i{AQgE6Oiyg$w-5Ii&6Y8Y5SZvwQ@zAypjD9Z%zHZ&0%MHj&1Qg$E_L_D z#d`2NDGwSE;NDL&n*57iw;V&cY2HH<>gd*{DMee`3bY?&Y=7p}Un_u)_|(cB&iv&0 zIQQ}yu?e5;h)!6EQl!09G50WK&ii%RI1}j&G+F-o7rLmBHV_9kReRd?yojZkGCC4| zYjDC?D6uJWE-jXzibKqY+rxq+KrMlG2O2t$f)XxI><)`+bqZ0m$9o{lMcP)4`Sj zKCLfYqEFbQ{7f&>ZQk+4cW&hzrFge)CZ@Fhc|ub-fMxw{gc0Q?PDb)toceihDq;VVg#jrXU0;Y#vGwEt zKS6aBUkn?}o$J$&e$cp4uF*4L-OZa@E%Z`$!{iofitx+SKQUwZsKPs%GU&AM=(CJnnn*v)`d4ibxgKFD>{78w1@b)n#l-0yqA@ zEUH!rhdO<9SwmF?M)W+usJ`0vL(bIOp&!nHV%6;7W#H0}&{B}PXYdISSMd_u zIt5Z*ZH6cfUbkPqd-_6N_}RB~Y!p%e+3gjQj1@LdrQ8ocPMg(THQ4MxnK9=%kwM~r zK$)Ou$v`Q&&(q(Zsd}ORQ}1*3>B9+m2Jl2oyv0SAO(1{ZJcAtI+atOKkl%PuXv5sC z9G1EOUlQ9JZQT!}<=E^KZ`Wo1mf~)8iouT2^$6cXnTfI%(;;q6jghK1C8BK7WsHaZ zqoO}PZgFf57VYgdVK~S5O8(CdY?R*aEgKD2mRm4rl0Z5RG_F#C2|9idlyc+_CPepI zTP4ip5Fm=YqVSi!%tKDdXnTkdZ6TnwbA9gyWKwm!_0kfxHDNr(RM7Ut+ zP{cA7DfUCdl&x7mtFM_=z_(%BOR0QUUCEThL%N3;^VeE>lR+Rd_Q`J7dF99X0LlN3 z#^{i9@jp)xMGuVK%LzS>r1bk}yN@>xNbs)YOBu=+pC`fO4fqukr@F0rpZ-Sin-1^q z3uxg$zOCxGEFbWPC0QfBTQ*?v{TFqHoV+BlbB`*tHNFOGfG-2`A2S{T;SDTRY8cwM z>un#6_-XA{cnJ(`(?91c@ky8v^fuMdc#%m&eb&P#e$P+3@gP({NCpZSHL5nj1Ladx zkeau?TBN8TBY+oGLsv`t)mJ%YtCZD&m?i5KqMJs9l8~G1^TqE}xC3>V>n< zq#SxfPVrwEMB|5VopNEl{wn+g6WJ^pk_--up$h*7xAoHPjY(gt-a!b208Mg-bH#FJ z8J`|*(Uf+1op!tL4-_v;K@9` z{>zi9)jBL!mFoM$L-6!5@4_}HdF!O@^6pc6;)gLa-xqPXG+X&clkYQrVmjgu?J5LS zy9Dv3Y_1>5VaE}+`x_`r6Day+8xz<}WJ& zs6r8xPzq~wHtTaU({Cs*ceGEFzRm3vZIS)_1WMw-l(*7*8eo@$`Ma5$W0Jcw(nP$J zAI654Bu`j&Bo4vK2;{r8MR>jpe@TBN8uqAm${i?XM2@X@`f9)CrNO+ot4+WJd>pqH6HRnHZm9;w9dq9?u3Q$S{sH~0 zwpCpe)%PCL#wx+?1?v+OdLRcekPujv={!1k5Y`W{ffeE^*TS36D!=U(4D(!}d9;3= zeHKZbiFCcb#URH^f4f|q?zVI@!>@n&4#-ki6J|Qv|7kmIM0bZM8T%3SCMP-92hy?BUWb_|AMC!icbtE~59NB* zBIzv9J^9DiHkDkP$gXP1fTcjJkE!(bpJpU1*<{ym1oz0gNl2nqZ9#W|ydj}3>p zUohVd;|WCmyt`*4t93c~RECFR4u;}mWZ51VDeTKii*ToM$@@ILD{S!2uc&}L%-9BI zN^mT3IS)_l+Q}`t+Tyqc`&KLm4*osC<{gfdZ@~jXS}?G7F%ZurxQxFqv4D`KS@G>S zBq0dOIG)1y4+PY^TvVEldf|Q$gdz^+oonLjtrtrY*k9gYBad!{-%NzthwR)eo&pzz z&hJHR_UJVbeMfem+XTMVl03A{g0pib7cbXoKU?`n7*p@zTwzH!IPt&cAL8JLkFI5f zfb&Fa%F2>{f~~-L)<5dYr{-t-A7iR%a!_|uC6;LGBEeGGNJ#N_A`VW@<=EoM4T{$##mjHx6a>$$e6h7CkfZspF}t33DN#lvUh>@EMhG5kydKKn z-miIgJ7T&FtjvcEFY`Z!OUEI_%VOh>Ax3tUN5@pZVXNfqIM(BgGcE~h3(<=89aDlTC zzbDze(h}AREN8laYVD&{LUv}>1(}XwCjw7_IAM-_j{>sf+X{ex{BSJH@Uc`->e6I7 zPv)14U6P=ne{iiJ)e#u_+@%P-OufvK89G0mf4w~6L3wLEZ<6(n%IJx6!B5Yry>$;& z+wqeUG4-#QUW$ZAWq5Ip0-aTQ7RcW-O@3KZZw-icKJZ6^$0+REHIwiNy^W^@ym>7D zQ_BRX5#te0y8!*GdZg|2xt8f+|2m8Iif5v zc-faE^o*0+bs0#r)02FRUSuhwkz|^4L-dzbkK2)#L=-bhBeHlpk$ss|&e7@5Rha$+ z2Z2ttQ!I_Np#&Cfj=QX^3ular@0Ef)3yrmsn-r$@z`XN+`PP>*?wTn+PD}4gQZ*Qm z$85TXM&xN+&rW?>>z;K>0)rsYg<^?E$_>(F7b3tLSXr5U%#|?_)*3(_R^75D9v;t# zBt|7qGCq2SqE2zQSSv7PeupgwXJaKQboS;j;*Ymtn(}jm3tK-+Zx%j|w0MWQ+%PP4 z&33~@1eSx=k_&5pTz-b#TnpKK!^J#`MC6BlxMMeT$8PNQR{2v<+Z_`Vicv}^@ThXW z3Eb-nOy%j&GRw!IgY_7!$O0ecpSHa)qLjlvlPY>ROcCB#`W^=V&%kuxsf7ADrCj|S zOB9$K65+-S73m;&&FfzUMCe2)PR|95q+Xz~HO8t^<#clF2`lkXpQ0Z;Kb-tyEe!Yr zYnV$!uAujtSG%l-BMo<1`wY^M+OYY>@0eG4M11yRU5EO`1sv6~DmI)& z9#6oO-Lzo>(=Xl6vSaOObpkIVLP6cq_if%FWp#R!eC9x^2?eCcoZG`M-e!K8FXFso z7U?lkK-ub(I41i0i(m{Nbd}$gKSs@e+gy+F`I$&_ymBhT_AfwdgZ#>5?5!~<3QQjr zME)t~;L*}N3qC{GbkfknuE6HE78TvNh{V(CJ|*CTET7|F2nC*KgDkINas?`sGU_pl zJWI^!YQ=78F|x@GJWe0|{q?W$ZTNOD&hR~{PFc6dfLYG zGKR(J(%{2?)&(4W!N-c<^2nO0u`bAUy6V=iot3F6-CEy~y>&S!>S#E@#mq9S+Wy&g z^5CP26JyMbnzI|j7_IQ_kseA-$<-7BusHv3uQ$I|2fVImA2*eo=hiG7;6>vp;@Br+ zuP=v5UiET&E#-%|Ij~*5mS_9-+T-+FgmBDnL;4t zFV&L0HnwAh!fi9x;2S`Cj;yjX99@n&7t~sXlg=!mwjR zY}+mTVi?@&U~#*`;@L77qrv0MAoRblBXs2pxc1ky+xEyKA?0A>d@*&eS=?%Z)L|J} zn@`=3kG-_-lHxlKFU5Ki>BTMwvwcS>jIi|pp~iJQ@r8p&+7(T}`t_bJeI4*C)|Mu? zHs)iyNYj{C40?5uYEoFZH5&r63o%6+fjv9#M94#90ZLE#TaFj#eKyV77L&qG5y^w~pX6=m)uqh8i zCUiP&>1qe36LAaK`FJf$sdBj^6T2XTz~OKRk75xT3zoK!gQWaX&vf-gP3{=IlmS!6 z^n_8L5@?Ev&S?KaETP6p_KP}>ijwP{S`-E zUD4(IJ3zRn83$|aNksIA?MFUKk-?{b(LjX+mg7D(GT(`A&8dfZ1!baE+CWxM z@jyzeUp~byXb3=wmrS4?nViv5vjhD%R5C>XRInb$S&Aeul+RlUY_{92OcvS-DmAEX zgdd`CM88BF*J=Fr{ePXYDqgp^)vafs0pQh&A&qJ9Wo3HePjkU}4EgCem8x(pG+i86 z8A5(4E~HS~FAE4$^GlubQ9h~_T*V*PAyzQxPCZq5PRFOd00x*%h2PqX0O^k7OyDSC zgW1PK^g(UO(Y= z^!NZ&JE&*hOGsMV(Cn|}C9?CCS`1D83%uH#5eBydJY}-}$C4ekOmlM+dV8%a{^$5u zYpC7mLb^>VLB-n#3}6mrR)%ARxE>{0`z4q68VOpv1eW$01Py2|2`(Vc=UR zo$a~VEvN*!{3Z|T2MHV~1?WZ&x=$Im`6-Ni5xv3&A`qfs82!?x3@@3$d}oWJHTzA> zNcO3Ca2&1^qCiML1=Lq8k#o8PNyb0g54CJQuc}~3JkAFmZC>zg-W};x*YX0C-w$z% z_H{Omzxh>&(76wF2r8MydkXrm`ys>hGpM}NZYgEQz|CDyML}<@5@`F}JasLMh08}19rFL=htq4l6ntfil_uxm02G|m z4gLT!Qu8fQzZ3+G?=OoHL5Jrchml&bu+EN#PApNYgZh7N6L9j>k^TN-*6x@FGaQDZL;p>VJw;4! zUOQFebQXc`T$wyN*!LgMAZs+BY-(-zxJ42pztxX7Qp**KKQxS)a5LFee;%TFlCdKuLtQR2W#)wHCRH!dh0fBR2{a9CI zZVHlzKF5g&`Tl43U5M#l;GvmX6&>$A_h=pkqI{|GZ0`rCKxgm}Ig%O2 zz-xMPw+~75n%$JRzu{S&kYuc*ku5K4SDgwzN}Q0wn1u0WFGKi<-PiF*%J8DeTg z0MSiEu_K1RzAM~X#AN}wP$*DlNejJ4**uv*e+B`vCVT&}84B<8>g^|1dc3KLs#4GZ zx)D^y*DLcaPneO?qz0aqckDs^*jFpeTUgXR2PO=F?9$i%oU*B_aS2^+RMm(bRhCqR zQ#u*+{nV?>HjYOsJs{8GT?XQaz~d|V3sSh8z%?R@uiZ-U>uptsU*!_)PzK=JD>%`< zCBwB#;}2@v?`eNx_kJ#+$vdG&-C?7}!brkkYqr@p@9H5$9x~{ecx5jT@YVDLMA3n8 zYq$tkYco8##|G9g?`J$>i6`cSrg6FZBuwwKJ8!hWgf5N-(T=emKVv^2_~$WxF@eAM zRP1RFvQj5#_E0{U#c%$qA@D$d16L1FjjX!F=FXOEVBW2W)f@7fd$f5mI6c1t3u638 zl!wvc{AeB!cNul4sI=ATxtn zB)Cnn$ySWsx%gIV!e1R{n53bbf!mkJ^fY%nu06iN=o<+^UtvM?xT6(q3*6a^z{K_x>AuM&$jywn? zrepDz`@Z9>kiYTeloRv{qk34Y>YRt`A~{^QR+Zwr|C6(YRTVi1g*Pi5CV51NKW{gT zCiU1*f|Q!55vSVr*M|56)!jCRJ}1PL@l8$WHr*Xvv=nU=8a9!%P0@09J$!ETaGx@} zL^DKuqa|81vb<;Vh1y^#b@;zsNnR5I)4Vr0f*`kei|=fvW;N^{Y6pgM6hTe+rF{ji z4mb4^h12)?G#rb&M*NE`#fFMK4@d^QKi3XENla$TvZJ1q7#Cva>ft!ANice9ps&RA zvvKlP5lu}V_gW8}&*TIyfkpnCx0LyeL5C*$!9Xb+s-2zdLX*+Jb1AuLz!T?z{W8tx zf*+l3+B$=B?eZt749!en`faFcN1+~;$7Z@7@m#ffuZk0|a~|Z?OpV6UzGC6m+L90W zT!Y<)hWc6HJn2?235fEz?!CmhY-3kIR&jLM|ArI1%yHt*+U@pVQwYl|Oi7x1scnT) zL_QBXt`Z0}UB_rOaz=3CFYIXCaV$in-(kybZm`W?4nHO7@xA`ilB)Qj_j@p@2bwe< zw_>HiTfYau!utl!!c~s$LUyni499RlxQS8MK^cyfWH#;92v>hqqv$%nodNirqXgSvBiCKRnK~oP74+AY~|541IrSV>GUD^~H`s7m#dWd;NK41f!7 z@GXG~Nrfb$B>G#+S;GcH1m=zv#@0}-!_>8NuxhaDTEmtK;)mf+Pjo&sgCA?wCH{TU zRD52T>L8>DQ!w64V&wmhbO@! z$5LOV4n)paPv0+El}icE4MV za)iX(B{g@kDhxU;5pzp{0WV8F7rU;bpWtv*W8_F7`eY4Qez}&5f2d6@-pA{V!uF~s zxgX8eEWBjYfo{zupKZ*B_C6pF?u-JwXr}2d7T~y2N8JW1t-Y`?<#1yN$+Ko`tpCQD zTjrIjeyUZyyHDLE52C(y@Dn%Tuti*PteP+PwpC@*nSer8XpbJ*1<(dwYZ$=M#k{30%N_sE>`>3%VIf9U> z%Fm+4^S**h6w3L@EV+nPeBRlfyYWPvPGo3eJFq``@C?0yU-maia#HQ8Wo71i(7m@GN!LCvz;G&$HALRbv6^%767%AM2--<*ny)J%!?po&|hHOFw568DSekJV8|I+Ca9TmpH2>40WT>7^eKjU>XJSNZ;6DXqhZS$bw* zhB(6nzqpwP9%HuXzb~F z|Ax4}j2G~9j<@wDaNRYp>#Ua@GsGM>@jdZgA|e?F8pHvIZ)5oFnX={ov$M5m% z-*kghkLN5PxdZ4(;)Kw}^#;($PyLwF;Lr z;j7(snVSGgCWD`=GjiX(_SQwQ06|gzH=6?YxTGW)dUGz~KnBPbRAfh#hGVcT^tc_B z{dmR~OH%Rm(IjJ1>fW`>p>w6V9#4A#7#4n_+t!#h%Rord|2~~w@Ng>Bu$}|CF6*D=`ulx{aM)l~67w~3m zV;Dre0drU|Jq&w0`Htzn)_%wmue0pQ2;~aD^`AGVu1>US-FsZx4WKKg_qv_vZfS#u zu%F4M!e;vw_h~fgoLdYWKYY1NnGvdW#p(~^&mJ2f9HAhZv>hgcH_u+nwE4{YL>DT#dY)yzWEyI}%WZ?cit6a5h*LV+Uyx_A5gUdfP zqFJEzz(T-|{2m!q>cgfbpYgg%|-d>}kU&Dz5`djdm|4sZc!% z=wuS|IBqfrEI_Q_C(eEicmqpsf(MZnc9tdQmJv1sQUX`KAdjJ*Dv}9d$x^}Sq$CF# z!7)Ohh;()VZ4+~g=`Dn-Rs0N#Yg1a8vJbP$p`ZsR1;>Dr8O0Q&i=V@P)poD0@^eRI zR~mjuc__OQ?*4Y3nQB$HOvH zv0(IpYIR@UcepCYD_~8qn*ZrJAbj^}pal-Z2WEK0&tpj7MI+P`Hn0qs^Yndr(e#g4 zQ~o~M6~h@^oFBvfv0R=l3KZy-_yL%L5GF);0D*W-Y3Pc$o z1wnV6)Fx~Lri)1OpwNUre6L0N4vQtX9)FFa0=ecuT^V2Y-h*+1-VT`OY3H1x6x0IB z^8`fyKy185-;%0H-NZ^VM3V5>>o^{dR67J07bmg<5a0+hD~qiKk@O~xz3n+*RJN*k z_u7ytgBtT4#A513h1Gpm!~&a77l)FKD*&WZDk%Ohf(JMX)5iXXj6?+be`SBvb1&uI zt4`qQpR?vIhEG^}kczRNe8V17M)&)6^-4IDlw8fCXo2WaLeJ3wx~BkF@vlY`APQ<3 z1#ZVo>qZ(x_hDTbL3rHoK`Hg`F>c+48At2xm{tik#~fmwho+*D!SF|S&a{u@^%iA( zWa}_^SR@{2gag0wmm42F#nZ`%z~UtVdBH)*pE&XQE$PEdWIQQxp%fp+`8d&M4C%sqcvq3o?L8*8yv_ zJz|_$sCqq6PChm#0+8qS*q; zbh%;C_}5``d6+hGvka=_Tv>l{SorfNSLdHT98{-FobrnS538jyZ5%hME!xze)1}JvGO=aQ}KFgNo&BwSbxCIGr|-; z;PAo^8`HNw&&f9^e!gjm z1QyZf8m(!jvcE*A1g9&2dYU959AJ7lJMCz5Zz#tE2SeLL_0`ZG&v;>XCf_F*V+$Fu;S6x=+1@P}|Pvg2s z9AvOa4NhkttruP|kP2o-T{->?a)4LgRlbH|X`M;Ve35B3@RRyTiy4gOi43Ec*h3xK z)k4@W3JvojuorM-UVO->CjnFnKCT9MDjrJ#Bp4gby-1(5$E3{FdkFmPHd=CsZwntQ zg|LrQ$X579FldZL4wZ;T5jxGhw6`6hXsJ=P1Oe(n-D|M)2OU!2Im7CAub2@xg9V5j zB;gsq*hyp_E$nlMCD++VmG`Cu3#43l1wZ%smk23UqPH`u3tm8$*qXyz6q14B&^HzM z2(F9sio0rKtoGC!b8uC8nl^6f#Ut^bF7DTLdWWEpYiXe1rFT3{ z5y!w_%f;S@*z#sR5!9}Q+wkGmm*>|baH|9LRBRt^c3CGy&OqzBzYhLwZ$;>|M&P7_ zx-vm6GEy{{DF(C-xj;o+0{f1snijIX}6jxzao zni1snc2HM`l1s3z?5%q3I%v021{O_*~pEsm#C;AqVPiMR_% zkhM6}s~%s@TAaxvAA4iyE9TetcOYPx@M=Uj_b}UA3Ur6Wh$^ho^QSupIny9GdGf=h zg83fCdS~v1YUN&M_k9)Y2xC5=?22pT>2>@f@phg)p<3paNxAL8UAf{47w?vZWbPgQ zn0%FNqa91s4G1S&5|Tgez_Am~`O@8iZYRPS3ZXGnm?HxAd$C~g7#qG?jCKYXS1C-eQ&b4eV4d` zMCak7-;7uS#YaV4*xwmp6}SFctG?(#z${uU`+kvUHuc5DXZ9SmP8@ZNLKmj35r2pl zKb#?Hpua}RQ$()Foijv2MDA*f&ux!)?cWGv@8uKH3$?g=9bdiaNC}e5L<)e!&Wj33 zh^XQwQs`U&RNPF_tHk!jqusgx-c;isKG1mV)HmukJ)uxf46D-F9tapJYoQ}Nm4!c; zC-|LnCm1}KM>u(*K6ADEmS=}4F|ohxa8{XbsqI>{6@mo~KUS8Z?;Qqj7xh2mPXX7R z5;Mei^?GqUz%ThAxp*I}vssfYss|i{Y1fY7_~|&KqE#c-y{r3}SsK%SL!kTZOLMM= zXajRJr}#xUH0=9TU#I@tX%WL07pNX>)~(I>jmhamBGU1oNC|6To=;z>pbt#Xx_15S zSzNpA(p5Vm!lQ`_k@(RS1@PS+JMX|1B5%JvPM5@su|)HRkTw_4)`%a87JmR<(m(*> zg-2qPQ;@*q)Gwuae6x~bF{V8^)CglRN%2a5Wy9LkuC4g`S-Tr?d>ME zlB~xcQa~VP`OrxQXhK_DS5sdPTwxj;qHu|+b^mcX{Ts>uai!lL)Ab;)-XnukW8_X^ zV@Q;5?pyJn@74$6pl#3SoUGqX7f)UIt=?_|&|6E=$t?$xF-_CA%yT3>(BHfG@0MSQ zlX~vN@KcR%W=&4fox9fjegKW;q9F_BOLq_j;#LBt#4s7}i0)b2tlgT>?0KKe{YhL( zHzL6PlHV3h%p7GuD*L&b7>aBGSp?%Qb|&N<6$byk9w~5~n!!v7XsBvS)d(@JsM{U! zC)aZFBO|pt44baUbKB$pOMj7!-Ay49qIKlfQ@Ief;3I@W*61m$&UI`k842x{@EwZ% zd%Vwc5E_Po5I(^Uu|wZ`x1W>s z8s#Lir4bFIigo3NN1dPd_UMYxz8-I4=%J^VXmX2w!&lQgl^nLQcQe{I^Wlkwd$=h<$^fTQuZI z$58B2yU%R{8XU{snk9}Rms~KC8CO}0ycJOG7kUa5B`&FFJ~vMZW>9>!^#+0@`zV8` zC&=_`B!E66Ai8Y&aeH_^Aj)mr=+3)4jLBD&@&&0Qx=nN|#2B_#Zt^YeN+W)h{^HBC znbb<(Iu^^h(8f)-o^jE4hJ)|sm8hQrUb+Hq(TBc*S$BKT2=t{ ztEz&bK>gS|e^om1UWU9&ip+&S>~ClpI^t+ItqQ8)fR-q`G^f^aCPSU=-b%A!k@*Yp z3n`AZ?0e@{$=hTa46s}?d2vB?@0C71$THQKTMRFJn7Hm_Te?Y%h- zGy8E<^@0T0KpB@Hz-V#Ih}t~2R8?_8`obCUW4i^$dS4jcx!9t3BU-5PZCs0;_bC|^ zFbZeYUR+rB{F&r8cyXa{(QNqNx;xHggxE25=o`Zy*)LnN`{wp01%(Gps2}n2-^S-} zS?0asSQQr|QUC<80!vw%c>O?(A3J}P0QF?M%8Od(;+6mS`bYNf{@2Audq0J0GZ#Xt z(TEU#B?Q(#H%FM+61;NA@q*98WA|~aC+-jBW#YM0_e3k^5I=TxN}6j^L4S}5EtLW2 zpz5k+?Czn1n!BC&rZ{}OMoJ^2Z`nOfq$PWweC2iB$_PD0m+^e&!v97loOGsai6rXwrEAjjW4!jk3P62^MF~yk%j3Sxp9>~#z)tkoVz!Y zPe1Kv%!xLZ@pch<+xS&)LubST^vQdoaACBQ8d;L>fOnW3263#UhV#RQ4H+6bQAzSD zsOSGU4hMooQjc5iHmr#IVf)S+yQ#g9od3g0rEh@T>Q?Ju6-4jN5=5Nfp0%l;tV zV&?4btFy^>PneNdze}ojWvWM>3ZQ)dmb}j1ffg|T+2=o3%TWP;(o|GYH6p86Q~p}= z`rxREwM>K5^5HsN{y4U1#ASBlgd6kCt|}=^DViJO;SaR%!r4?^sq~~RJJEFLBP-ZQ zV}jIVSM9y+-+|2+fA&IPZ%>j`fxp$HeYRIUwMR^CpUP}^>X+dRo1@1&@9U6FR zvD9zIurk%WQV0~Thu-c_W-}6q@S4wP&dWzn+WhWjjVOl-#M{&Ut@S6f<1h(B%a^It)SBrpKj!LT)=w2|C2W*?UFtP@rNVI< zREM`Sx9@zare07+^mE`!ndSMB;Xu0YYZzV`C6@Y09lX+s_MGRrEodw+Uy5uDPCX!DMAG2B<{QRH;hAVc8#QDHuVIu-q_?- zetPa+)9m$1jz- z@h6Aby@3@mCOpLJ3%i24z&8Zz!tX7ku+;WwwYvcysUI|k^KqomFAu#ca*OgYd+JkC z;0Oo%N``StpBuZYwz1iT5BHK)cQuxI?1F?L#b&xeZ>;9>n%?teE55JMOJ84T$w+7FPyC>j$vto>GkkWdhp}sIY>|C-CKh>TK_1m=8Pf3#YjfXb#u2| zCE%_5U&=?oAL+QGUgXK0-_i(m&(jUi^y_pW^*Z&RtZf$Fp&v8yi-HeJMDP62vuP1DjzNr`Xvw z1Z)pyT1@ z{q3q-$z4nc=E+lfPG<3|O|Uys4S2kI4R*WP3OIzoBnEPD_Sw zKPDqe2(DseB|m#MakovZ&OTyh&)C}5EXx0rUEQ}p3pssB2Dufn9%PZ3D{#f z;qUm$$Jkmf7naj(!uQGR{ly@D|2iVq=C#2i!PX!YF4=e%Khe?QrFK{c?%TQQIT?gi zw{-mk%>kVFSdu*wZXbvbPev=#2v>H0QvY2|hV#;3Sa^Lb3Tm%)T9RgxVjzq#n?=Rf z@K&eub9GHkGYHUo%bD)<4RRstJ0oHQ^~moCm>NY+cCt5(sAWv>4Q;(8xP2*z?@rU{ zK0N<*y8!d(B`MH@xfXK@>?BtYx4O4OWV ziv|W>`{^Mw!!ewb0r!W(_grh-qi|z9w*;ku5AMA7FT=@cD1umikaFOCJu=SubTU;K z6+ij$5dJJ7#TaCPefN<#nDxf5XOq~XxGhlU#N%78xbvyyd=I>FY4y{**313cqPZ7V zl}=IV>-W>td1Ug-7N+ZE6SQx9T~}4Mf*#1FfixzBH9N-R+X6Wxcf+^oc%ubMdgEst zXCbiur%Dbl1714whg}P~cA0WBh5kk7D9G=B?=*b_%!E2HtbL7E0nDDgOC8Kblr)B` z-uk(_7Au*f^>XX>X6X1bCl6h+!Dn-y|LWMtpw)Awjl3N1s0-_{&@{)+M|Pibz7C9I zw}d4H3UBr1l2qr7MGoCI?5;G<<(W1Rl0TJM&#fY3xFR&T_WbRYnSb&T+ETNCasEYo z$#*3aG{MDKe%m>?W#sJ8or^ZB{rAG7p`i$8OKF`IL2k#v%}qv(xg(r+?LLw<@q{R# zmF4|}!=eVVfiljtDRrr7CC?GY!=d5V#mWfoP&r)b|C)#(>x;8L7dY`C_5D+v6+*Fb zP~@A^07?-)f=HWlF|wT~R_CHKq^owdpc@ZR-0PG;^61+>Yl!pVQ0Va|l-e|%!}EGv zXwEyaSY<(h3S4|$@G6$VsbjDng|Ne4RD+vGoA=WfCR@KR{~4UWhPKqM%M*Ihf3p=d zJy>Qg78W@=-db=l-c+Ohedcu66u-}r?ZHMe=iNpEyC~{wIUt2qR1U+X>v7uy%>#!- z^*-rg)oPMGE!ktM9^^2$r#m}R2iK|Qfy;i1-);(@pyRLq+Y#aER>AntdmoIx&LxL= zShisMoJo}6R7mo_aKv$oH>lQby>m>taBsH=M_;a+r=>TTB%X;bVp@FNysgI4F-TdL zdR(;KL&}C7@v%i|(x+(3U-Pe6KzBbaD|=rrV3o=p)ysOg=6^L#_YBOHxc%iw$$whFUMVeQ*oCaji@-O39mAD=Wig2T{1%S`@;8II+uZux!<14J%m#~ zS8Q6%@r97=MoGIeIkMjI>Fw4v#>5vPtT8QOSs32UXyc3Z@eI*gX8F$~in|NkO#Y|T z)WE`zA(JkXrU7ftp@ex@4TL92limwuGcF!zgYFDA_gS{o7GKFCm%>$5x%D1oW=veJ zIbrHpAy`EmPDMYO-(!34U}Yv2e=^h0iyO8ch<4^bp(CD``E=H`fvHyuQeuzpTO|H+P$_RzJ&+3;OZ4vptY!$G=M^KAk2wQ zA-tYYk2fMc89&v$+RxGaqGtFm^p!4aK6V;ksf6n(`ZgI$hvQK{t)ywUw$p-pk?&lv zi)%kt1@(iLnl;6WEB!(nr{4#eRbm(s=aTuq596dQ_u&tPpp2vZJ32qns^6pe@)_JQ zl3|0`I#1UOEFES0d*@rHtTHoQ z3B-g$S#@x`_lFU8oZIW@8mW_Q4NG%PaFVRXxQr~AgONk^vni~vtc*FY_&2*|@1HxR z$WNC1UvaR*f4ICz4jc=&dR3?G_eptdG_|iR91s=TiCLdXY-ed)sFM0{o#BgiPXoAw z>*2_Ns@3v2yiaVfq@{)5IRXDC9SY+0dQXK*#|Yj-n6qEi?=5~S5R(<8$rcho`Aku+ zce^zp6D}&cmMa52b3@#*tq+YO4)VJkZSj{~PQycrRu$k4i2S^`rO!HnAL5xwlSnRq z15~JN{c!SR5?eg3F?^>yuY_$qZy!|-Bsx5Sk4Nu52c^9;9n<>V#Tt-N))xj+6f0_J zX%SLNZMOeW5pN2fj$uIG#0U909v|Cvgtr#ve%pZgvu;3ac;yXHYo3>2k_4O`TV}E%vIO#bA1kjdCmo5!OxQbDin&c z!sQY`41xc=&S>SeNCJA6pd@cx=EB(6=g!R=5c54DSeFU>n4iGkyJhicYBc?I z0;LYUD1ZuC!8|Bd$$R+Nn+1b@${5IlmHsxL^LhNa2YP2Z-_c)w*=a!1OG18DFe-G4 zGSBA|vaFU-y#ASsQIh`=0_}Qi5oF5Mm8tv;Oy(%#YO}d8FK$3I6O+|#k=&2&&3yxB z3eT{+yXF2d^B;d)02R7|cu0K)2@JL=AF#;zd<16|e`0I_&uU>r^k{Hi-v)7=pZ8fW}DK$s1L`6m0M z0e`<-$>37_+V-p}LFdS?{IeVUEDdFUt^jpJtEK{FC{R!ji{RhW@6L|~z9eL(@Nwt< zUQ{O<@cn%=>2sb1#F{-tLjRLsG%^%LdG>$*6+jGCfw6oP48d+mM|7xjKQY2r80YqG z1HQU8a|0rp3)dfKF9B~gES*!Iope|oPx<3&&-;LeQwrum^=}E0oLTbGZN3vbdUr== z-abnEJbL%>UbpL`y8BZ_KtArBs8#5!Yv@rlO3A8?XjA(!5} za&k98?nfDSHDyK_w?(*wf25ZkWg7f~b_x7U=Y$T^9DjWGy_EuJIHlko7$b0Zjp8-I zdcWH!>2v3P3fA{|oOkwlORfZB`e7mDA!4K`$_Z3er2(<|VgES@gfox`^k9o1wXnH1 zhqo5yzDmI5{OnQ28^C-=qF}WNen))VuPx-i14wyw{E`d*pS^p4awPl00DfPMZCl-c zn`@!wwo%3rO`JQs`F$>}U?&{3L z82GBIo@FNdteu!T2E(^0Ah?CTcng?TqvZFf?08UzsL0i_L1je zb{z?M-SL$H`YJ6%lC~&oNDnN(#}4&f z0#~8*uA1rNV{gI!ei$#iq&V&0jez9WBxEXSK#Z~D%h)j?P0E`EP<4r`6$0il+cFkn znSF1_n_ssoaJ;ci_4vf2Nw$5Owxc70Aox_ zu`4%61G24>j<)G~909ZmgMnD6+#Rz`M&HK2yEg!_i08KkX! z>+c#47oP91AXyY6mgZy*hZO zLdv!dM>%2+e}Acd(u=RWC4lw~OUQ3pekHW!naTfN>1+d_D325RTVF_)=%c>@K3I{S zoV+w`NHZY!S)|EzB!C!WY=5M@SuDmCxHQ}2>KrtUdqZAh>Euyms`kGMuqa4i-nyC# zwW6x4qwUyhn?@wyQT6=Y@p~~;4=;)rK$ zJ@}rsAnX@}eEC&D%k9F)dfPUQ96Wt(z%nkfE#usv>H70Hk3KlyarGQk^j&Lw-WFp< zxEoh|I#~cUWUOrqAh!P!{s$ocr-`!jghEFWV~ow5IaBPGQXwT|#zXLt2_uRpsgl0o z+OCBb>}PQQgwyOOsy>$Oe7w4#RuIrCyq!QF?gTp22Kn|%U;b*$(5E5n!;?mr4a3Ly z5=-{)1S~xzU>eui9{nP$YMhSe|7w>TzYlrydyFgjuFcGm-!ukl$iV+yAb_MiTM&_$ zKi?yKzBe>IKVPg#NfEES@|r@29^)SZV~iaGq$D7xFQpH2X5;GZu%+OHH`Kl!A-@w=z=VqO0zZPi0B`d#o>$B2j|5HQ z#&MQ@>DZFIe~u~58#1=&`(A-^y}jROv>f3sZ4A^9DR&9^e!7sEnIZ)|9*>Yx)~l>k z0S~`<@x>QkeDTE>U(#Ko0V%fS`a{4pvN0>43#-t$XKv-N!U;jk@IzkE)IYRMJvZPn z?(>)FHwR4pLc7fPYtUDa94j2Y9&Zy4IFMel0nlc|EM>K#FRH$cW1 zW06Qi?AoZX zqq6|)+qPT+-YPi;00;m8KmY&$0ssII004jh000C403ZMW009612mk=!1VHe9(F~PC Q&;S4c07*qoM6N<$f>dyJm;e9( diff --git a/app/images/icon-64.png b/app/images/icon-64.png index 5f99e6a3fde91038e66e32fe65e19c3808ec8ecf..643c02b3108fb5b2c8eb86854b2e2f28866cf320 100644 GIT binary patch literal 4204 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 h90}k^0RPGU7t_wD%PDHLkV1kEDShoNG diff --git a/app/images/permissions-check.svg b/app/images/permissions-check.svg index c3df71f59..a45c5346b 100644 --- a/app/images/permissions-check.svg +++ b/app/images/permissions-check.svg @@ -1,20 +1,15 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + - diff --git a/app/images/user-check.svg b/app/images/user-check.svg new file mode 100644 index 000000000..8ba739338 --- /dev/null +++ b/app/images/user-check.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/manifest.json b/app/manifest.json index 19906b892..cbf2fc67c 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "__MSG_appName__", "short_name": "__MSG_appName__", - "version": "7.2.1", + "version": "7.6.1", "manifest_version": 2, "author": "https://metamask.io", "description": "__MSG_appDescription__", @@ -17,12 +17,17 @@ }, "icons": { "16": "images/icon-16.png", - "128": "images/icon-128.png" + "19": "images/icon-19.png", + "32": "images/icon-32.png", + "38": "images/icon-38.png", + "64": "images/icon-64.png", + "128": "images/icon-128.png", + "512": "images/icon-512.png" }, "applications": { "gecko": { "id": "webextension@metamask.io", - "strict_min_version": "56.2" + "strict_min_version": "56.0" } }, "default_locale": "en", @@ -36,8 +41,13 @@ }, "browser_action": { "default_icon": { + "16": "images/icon-16.png", "19": "images/icon-19.png", - "38": "images/icon-38.png" + "32": "images/icon-32.png", + "38": "images/icon-38.png", + "64": "images/icon-64.png", + "128": "images/icon-128.png", + "512": "images/icon-512.png" }, "default_title": "MetaMask", "default_popup": "popup.html" diff --git a/app/scripts/background.js b/app/scripts/background.js index 09691e5fb..3f152fbb7 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -2,13 +2,14 @@ * @file The entry point for the web extension singleton process. */ -// this needs to run before anything else + +// these need to run before anything else +require('./lib/freezeGlobals') require('./lib/setupFetchDebugging')() // polyfills import 'abortcontroller-polyfill/dist/polyfill-patch-fetch' -const urlUtil = require('url') const endOfStream = require('end-of-stream') const pump = require('pump') const debounce = require('debounce-stream') @@ -72,7 +73,7 @@ let versionedData initialize().catch(log.error) // setup metamask mesh testing container -setupMetamaskMeshMetrics() +const { submitMeshMetricsEntry } = setupMetamaskMeshMetrics() /** * An object representing a transaction, in whatever state it is in. @@ -251,9 +252,16 @@ function setupController (initState, initLangCode) { const provider = controller.provider setupEnsIpfsResolver({ provider }) + // submit rpc requests to mesh-metrics + controller.networkController.on('rpc-req', (data) => { + submitMeshMetricsEntry({ type: 'rpc', data }) + }) + // report failed transactions to Sentry controller.txController.on(`tx:status-update`, (txId, status) => { - if (status !== 'failed') return + if (status !== 'failed') { + return + } const txMeta = controller.txController.txStateManager.getTx(txId) try { reportFailedTxToSentry({ sentry, txMeta }) @@ -305,6 +313,7 @@ function setupController (initState, initLangCode) { // extension.runtime.onConnect.addListener(connectRemote) extension.runtime.onConnectExternal.addListener(connectExternal) + extension.runtime.onMessage.addListener(controller.onMessage.bind(controller)) const metamaskInternalProcessHash = { [ENVIRONMENT_TYPE_POPUP]: true, @@ -344,7 +353,10 @@ function setupController (initState, initLangCode) { const portStream = new PortStream(remotePort) // communication with popup controller.isClientOpen = true - controller.setupTrustedCommunication(portStream, 'MetaMask') + // construct fake URL for identifying internal messages + const metamaskUrl = new URL(window.location) + metamaskUrl.hostname = 'metamask' + controller.setupTrustedCommunication(portStream, metamaskUrl) if (processName === ENVIRONMENT_TYPE_POPUP) { popupIsOpen = true @@ -380,9 +392,13 @@ function setupController (initState, initLangCode) { // communication with page or other extension function connectExternal (remotePort) { - const originDomain = urlUtil.parse(remotePort.sender.url).hostname + const senderUrl = new URL(remotePort.sender.url) + let extensionId + if (remotePort.sender.id !== extension.runtime.id) { + extensionId = remotePort.sender.id + } const portStream = new PortStream(remotePort) - controller.setupUntrustedCommunication(portStream, originDomain) + controller.setupUntrustedCommunication(portStream, senderUrl, extensionId) } // @@ -412,7 +428,7 @@ function setupController (initState, initLangCode) { label = String(count) } extension.browserAction.setBadgeText({ text: label }) - extension.browserAction.setBadgeBackgroundColor({ color: '#506F8B' }) + extension.browserAction.setBadgeBackgroundColor({ color: '#037DD6' }) } return Promise.resolve() diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index f1eff568b..33b56d446 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -1,7 +1,9 @@ const fs = require('fs') const path = require('path') const pump = require('pump') +const log = require('loglevel') const querystring = require('querystring') +const { Writable } = require('readable-stream') const LocalMessageDuplexStream = require('post-message-stream') const ObjectMultiplex = require('obj-multiplex') const extension = require('extensionizer') @@ -18,7 +20,7 @@ const inpageBundle = inpageContent + inpageSuffix // If we create a FireFox-only code path using that API, // MetaMask will be much faster loading and performant on Firefox. -if (shouldInjectWeb3()) { +if (shouldInjectProvider()) { injectScript(inpageBundle) start() } @@ -37,7 +39,7 @@ function injectScript (content) { container.insertBefore(scriptTag, container.children[0]) container.removeChild(scriptTag) } catch (e) { - console.error('MetaMask script injection failed', e) + console.error('MetaMask provider injection failed.', e) } } @@ -85,6 +87,44 @@ async function setupStreams () { (err) => logStreamDisconnectWarning('MetaMask Background Multiplex', err) ) + const onboardingStream = pageMux.createStream('onboarding') + const addCurrentTab = new Writable({ + objectMode: true, + write: (chunk, _, callback) => { + if (!chunk) { + return callback(new Error('Malformed onboarding message')) + } + + const handleSendMessageResponse = (error, success) => { + if (!error && !success) { + error = extension.runtime.lastError + } + if (error) { + log.error(`Failed to send ${chunk.type} message`, error) + return callback(error) + } + callback(null) + } + + try { + if (chunk.type === 'registerOnboarding') { + extension.runtime.sendMessage({ type: 'metamask:registerOnboarding', location: window.location.href }, handleSendMessageResponse) + } else { + throw new Error(`Unrecognized onboarding message type: '${chunk.type}'`) + } + } catch (error) { + log.error(error) + return callback(error) + } + }, + }) + + pump( + onboardingStream, + addCurrentTab, + error => console.error('MetaMask onboarding channel traffic failed', error), + ) + // forward communication across inpage-background for these channels only forwardTrafficBetweenMuxers('provider', pageMux, extensionMux) forwardTrafficBetweenMuxers('publicConfig', pageMux, extensionMux) @@ -114,16 +154,18 @@ function forwardTrafficBetweenMuxers (channelName, muxA, muxB) { */ function logStreamDisconnectWarning (remoteLabel, err) { let warningMsg = `MetamaskContentscript - lost connection to ${remoteLabel}` - if (err) warningMsg += '\n' + err.stack + if (err) { + warningMsg += '\n' + err.stack + } console.warn(warningMsg) } /** - * Determines if Web3 should be injected + * Determines if the provider should be injected * - * @returns {boolean} {@code true} if Web3 should be injected + * @returns {boolean} {@code true} if the provider should be injected */ -function shouldInjectWeb3 () { +function shouldInjectProvider () { return doctypeCheck() && suffixCheck() && documentElementCheck() && !blacklistedDomainCheck() } @@ -146,8 +188,8 @@ function doctypeCheck () { * Returns whether or not the extension (suffix) of the current document is prohibited * * This checks {@code window.location.pathname} against a set of file extensions - * that should not have web3 injected into them. This check is indifferent of query parameters - * in the location. + * that we should not inject the provider into. This check is indifferent of + * query parameters in the location. * * @returns {boolean} whether or not the extension of the current document is prohibited */ @@ -225,7 +267,9 @@ function redirectToPhishingWarning () { */ async function domIsReady () { // already loaded - if (['interactive', 'complete'].includes(document.readyState)) return + 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/ab-test.js b/app/scripts/controllers/ab-test.js new file mode 100644 index 000000000..bcf25454e --- /dev/null +++ b/app/scripts/controllers/ab-test.js @@ -0,0 +1,57 @@ +const ObservableStore = require('obs-store') +const extend = require('xtend') +const { getRandomArrayItem } = require('../lib/util') + +/** + * a/b test descriptions: + * - `fullScreenVsPopup`: + * - description: tests whether showing tx confirmations in full screen in the browser will increase rates of successful + * confirmations + * - groups: + * - popup: this is the control group, which follows the current UX of showing tx confirmations in the notification + * window + * - fullScreen: this is the only test group, which will cause users to be shown tx confirmations in a full screen + * browser tab + */ + +class ABTestController { + /** + * @constructor + * @param opts + */ + constructor (opts = {}) { + const { initState } = opts + this.store = new ObservableStore(extend({ + abTests: { + fullScreenVsPopup: this._getRandomizedTestGroupName('fullScreenVsPopup'), + }, + }, initState)) + } + + /** + * Returns the name of the test group to which the current user has been assigned + * @param {string} abTestKey the key of the a/b test + * @return {string} the name of the assigned test group + */ + getAssignedABTestGroupName (abTestKey) { + return this.store.getState().abTests[abTestKey] + } + + /** + * Returns a randomly chosen name of a test group from a given a/b test + * @param {string} abTestKey the key of the a/b test + * @return {string} the name of the randomly selected test group + * @private + */ + _getRandomizedTestGroupName (abTestKey) { + const nameArray = ABTestController.abTestGroupNames[abTestKey] + return getRandomArrayItem(nameArray) + } +} + +ABTestController.abTestGroupNames = { + fullScreenVsPopup: ['control', 'fullScreen'], +} + +module.exports = ABTestController + diff --git a/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js index 9533fd458..8d67874ad 100644 --- a/app/scripts/controllers/app-state.js +++ b/app/scripts/controllers/app-state.js @@ -13,6 +13,7 @@ class AppStateController { this.onInactiveTimeout = onInactiveTimeout || (() => {}) this.store = new ObservableStore(extend({ timeoutMinutes: 0, + mkrMigrationReminderTimestamp: null, }, initState)) this.timer = null @@ -23,6 +24,12 @@ class AppStateController { this._setInactiveTimeout(preferences.autoLogoutTimeLimit) } + setMkrMigrationReminderTimestamp (timestamp) { + this.store.updateState({ + mkrMigrationReminderTimestamp: timestamp, + }) + } + /** * Sets the last active time to the current time * @return {void} diff --git a/app/scripts/controllers/detect-tokens.js b/app/scripts/controllers/detect-tokens.js index 78ed34e7b..0279b8108 100644 --- a/app/scripts/controllers/detect-tokens.js +++ b/app/scripts/controllers/detect-tokens.js @@ -30,8 +30,12 @@ class DetectTokensController { * */ async detectNewTokens () { - if (!this.isActive) { return } - if (this._network.store.getState().provider.type !== MAINNET) { return } + if (!this.isActive) { + return + } + if (this._network.store.getState().provider.type !== MAINNET) { + return + } const tokensToDetect = [] for (const contractAddress in contracts) { if (contracts[contractAddress].erc20 && !(this.tokenAddresses.includes(contractAddress.toLowerCase()))) { @@ -86,7 +90,9 @@ class DetectTokensController { * */ restartTokenDetection () { - if (!(this.isActive && this.selectedAddress)) { return } + if (!(this.isActive && this.selectedAddress)) { + return + } this.detectNewTokens() this.interval = DEFAULT_INTERVAL } @@ -96,8 +102,12 @@ class DetectTokensController { */ set interval (interval) { this._handle && clearInterval(this._handle) - if (!interval) { return } - this._handle = setInterval(() => { this.detectNewTokens() }, interval) + if (!interval) { + return + } + this._handle = setInterval(() => { + this.detectNewTokens() + }, interval) } /** @@ -105,9 +115,15 @@ class DetectTokensController { * @type {Object} */ set preferences (preferences) { - if (!preferences) { return } + if (!preferences) { + return + } this._preferences = preferences - preferences.store.subscribe(({ tokens = [] }) => { this.tokenAddresses = tokens.map((obj) => { return obj.address }) }) + preferences.store.subscribe(({ tokens = [] }) => { + this.tokenAddresses = tokens.map((obj) => { + return obj.address + }) + }) preferences.store.subscribe(({ selectedAddress }) => { if (this.selectedAddress !== selectedAddress) { this.selectedAddress = selectedAddress @@ -120,7 +136,9 @@ class DetectTokensController { * @type {Object} */ set network (network) { - if (!network) { return } + if (!network) { + return + } this._network = network this.ethersProvider = new ethers.providers.Web3Provider(network._provider) } @@ -130,12 +148,16 @@ class DetectTokensController { * @type {Object} */ set keyringMemStore (keyringMemStore) { - if (!keyringMemStore) { return } + if (!keyringMemStore) { + return + } this._keyringMemStore = keyringMemStore this._keyringMemStore.subscribe(({ isUnlocked }) => { if (this.isUnlocked !== isUnlocked) { this.isUnlocked = isUnlocked - if (isUnlocked) { this.restartTokenDetection() } + if (isUnlocked) { + this.restartTokenDetection() + } } }) } diff --git a/app/scripts/controllers/ens/ens.js b/app/scripts/controllers/ens/ens.js new file mode 100644 index 000000000..eb2586a7d --- /dev/null +++ b/app/scripts/controllers/ens/ens.js @@ -0,0 +1,25 @@ +const EthJsEns = require('ethjs-ens') +const ensNetworkMap = require('ethjs-ens/lib/network-map.json') + +class Ens { + static getNetworkEnsSupport (network) { + return Boolean(ensNetworkMap[network]) + } + + constructor ({ network, provider } = {}) { + this._ethJsEns = new EthJsEns({ + network, + provider, + }) + } + + lookup (ensName) { + return this._ethJsEns.lookup(ensName) + } + + reverse (address) { + return this._ethJsEns.reverse(address) + } +} + +module.exports = Ens diff --git a/app/scripts/controllers/ens/index.js b/app/scripts/controllers/ens/index.js new file mode 100644 index 000000000..81ba5d81e --- /dev/null +++ b/app/scripts/controllers/ens/index.js @@ -0,0 +1,94 @@ +const ethUtil = require('ethereumjs-util') +const ObservableStore = require('obs-store') +const punycode = require('punycode') +const log = require('loglevel') +const Ens = require('./ens') + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' +const ZERO_X_ERROR_ADDRESS = '0x' + +class EnsController { + constructor ({ ens, provider, networkStore } = {}) { + const initState = { + ensResolutionsByAddress: {}, + } + + this._ens = ens + if (!this._ens) { + const network = networkStore.getState() + if (Ens.getNetworkEnsSupport(network)) { + this._ens = new Ens({ + network, + provider, + }) + } + } + + this.store = new ObservableStore(initState) + networkStore.subscribe((network) => { + this.store.putState(initState) + if (Ens.getNetworkEnsSupport(network)) { + this._ens = new Ens({ + network, + provider, + }) + } else { + delete this._ens + } + }) + } + + reverseResolveAddress (address) { + return this._reverseResolveAddress(ethUtil.toChecksumAddress(address)) + } + + async _reverseResolveAddress (address) { + if (!this._ens) { + return undefined + } + + const state = this.store.getState() + if (state.ensResolutionsByAddress[address]) { + return state.ensResolutionsByAddress[address] + } + + let domain + try { + domain = await this._ens.reverse(address) + } catch (error) { + log.debug(error) + return undefined + } + + let registeredAddress + try { + registeredAddress = await this._ens.lookup(domain) + } catch (error) { + log.debug(error) + return undefined + } + + if (registeredAddress === ZERO_ADDRESS || registeredAddress === ZERO_X_ERROR_ADDRESS) { + return undefined + } + + if (ethUtil.toChecksumAddress(registeredAddress) !== address) { + return undefined + } + + this._updateResolutionsByAddress(address, punycode.toASCII(domain)) + return domain + } + + _updateResolutionsByAddress (address, domain) { + const oldState = this.store.getState() + this.store.putState({ + ensResolutionsByAddress: { + ...oldState.ensResolutionsByAddress, + [address]: domain, + }, + }) + } +} + +module.exports = EnsController diff --git a/app/scripts/controllers/incoming-transactions.js b/app/scripts/controllers/incoming-transactions.js index 6c7b626f1..10735b3f9 100644 --- a/app/scripts/controllers/incoming-transactions.js +++ b/app/scripts/controllers/incoming-transactions.js @@ -163,7 +163,9 @@ class IncomingTransactionsController { const newIncomingTransactions = { ...currentIncomingTxs, } - newTxs.forEach(tx => { newIncomingTransactions[tx.hash] = tx }) + newTxs.forEach(tx => { + newIncomingTransactions[tx.hash] = tx + }) this.store.updateState({ incomingTxLastFetchedBlocksByNetwork: { @@ -207,8 +209,8 @@ class IncomingTransactionsController { } } - _processTxFetchResponse ({ status, result, address, currentNetworkID }) { - if (status !== '0' && result.length > 0) { + _processTxFetchResponse ({ status, result = [], address, currentNetworkID }) { + if (status === '1' && Array.isArray(result) && result.length > 0) { const remoteTxList = {} const remoteTxs = [] result.forEach((tx) => { diff --git a/app/scripts/controllers/network/createInfuraClient.js b/app/scripts/controllers/network/createInfuraClient.js index 0a6e9ecb0..3ac7fd284 100644 --- a/app/scripts/controllers/network/createInfuraClient.js +++ b/app/scripts/controllers/network/createInfuraClient.js @@ -11,8 +11,11 @@ const BlockTracker = require('eth-block-tracker') module.exports = createInfuraClient -function createInfuraClient ({ network }) { - const infuraMiddleware = createInfuraMiddleware({ network, maxAttempts: 5, source: 'metamask' }) +function createInfuraClient ({ network, onRequest }) { + const infuraMiddleware = mergeMiddleware([ + createRequestHookMiddleware(onRequest), + createInfuraMiddleware({ network, maxAttempts: 5, source: 'metamask' }), + ]) const infuraProvider = providerFromMiddleware(infuraMiddleware) const blockTracker = new BlockTracker({ provider: infuraProvider }) @@ -62,3 +65,10 @@ function createNetworkAndChainIdMiddleware ({ network }) { net_version: netId, }) } + +function createRequestHookMiddleware (onRequest) { + return (req, _, next) => { + onRequest(req) + next() + } +} diff --git a/app/scripts/controllers/network/createMetamaskMiddleware.js b/app/scripts/controllers/network/createMetamaskMiddleware.js index 5dcd3a895..58ccb95a1 100644 --- a/app/scripts/controllers/network/createMetamaskMiddleware.js +++ b/app/scripts/controllers/network/createMetamaskMiddleware.js @@ -1,8 +1,7 @@ const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware') const createScaffoldMiddleware = require('json-rpc-engine/src/createScaffoldMiddleware') -const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware') const createWalletSubprovider = require('eth-json-rpc-middleware/wallet') - +const { createPendingNonceMiddleware, createPendingTxMiddleware } = require('./middleware/pending') module.exports = createMetamaskMiddleware function createMetamaskMiddleware ({ @@ -15,6 +14,7 @@ function createMetamaskMiddleware ({ processTypedMessageV4, processPersonalMessage, getPendingNonce, + getPendingTransactionByHash, }) { const metamaskMiddleware = mergeMiddleware([ createScaffoldMiddleware({ @@ -32,16 +32,7 @@ function createMetamaskMiddleware ({ processPersonalMessage, }), createPendingNonceMiddleware({ getPendingNonce }), + createPendingTxMiddleware({ getPendingTransactionByHash }), ]) return metamaskMiddleware } - -function createPendingNonceMiddleware ({ getPendingNonce }) { - return createAsyncMiddleware(async (req, res, next) => { - if (req.method !== 'eth_getTransactionCount') return next() - const address = req.params[0] - const blockRef = req.params[1] - if (blockRef !== 'pending') return next() - res.result = await getPendingNonce(address) - }) -} diff --git a/app/scripts/controllers/network/middleware/pending.js b/app/scripts/controllers/network/middleware/pending.js new file mode 100644 index 000000000..96a5d40be --- /dev/null +++ b/app/scripts/controllers/network/middleware/pending.js @@ -0,0 +1,36 @@ +const { formatTxMetaForRpcResult } = require('../util') +const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware') + +function createPendingNonceMiddleware ({ getPendingNonce }) { + return createAsyncMiddleware(async (req, res, next) => { + const {method, params} = req + if (method !== 'eth_getTransactionCount') { + return next() + } + const [param, blockRef] = params + if (blockRef !== 'pending') { + return next() + } + res.result = await getPendingNonce(param) + }) +} + +function createPendingTxMiddleware ({ getPendingTransactionByHash }) { + return createAsyncMiddleware(async (req, res, next) => { + const {method, params} = req + if (method !== 'eth_getTransactionByHash') { + return next() + } + const [hash] = params + const txMeta = getPendingTransactionByHash(hash) + if (!txMeta) { + return next() + } + res.result = formatTxMetaForRpcResult(txMeta) + }) +} + +module.exports = { + createPendingTxMiddleware, + createPendingNonceMiddleware, +} diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js index 2c68e4378..f1be914bb 100644 --- a/app/scripts/controllers/network/network.js +++ b/app/scripts/controllers/network/network.js @@ -81,7 +81,9 @@ module.exports = class NetworkController extends EventEmitter { verifyNetwork () { // Check network when restoring connectivity: - if (this.isNetworkLoading()) this.lookupNetwork() + if (this.isNetworkLoading()) { + this.lookupNetwork() + } } getNetworkState () { @@ -190,7 +192,10 @@ module.exports = class NetworkController extends EventEmitter { _configureInfuraProvider ({ type }) { log.info('NetworkController - configureInfuraProvider', type) - const networkClient = createInfuraClient({ network: type }) + const networkClient = createInfuraClient({ + network: type, + onRequest: (req) => this.emit('rpc-req', { network: type, req }), + }) this._setNetworkClient(networkClient) // setup networkConfig var settings = { diff --git a/app/scripts/controllers/network/util.js b/app/scripts/controllers/network/util.js index a6f848864..829c62582 100644 --- a/app/scripts/controllers/network/util.js +++ b/app/scripts/controllers/network/util.js @@ -29,6 +29,27 @@ const networkToNameMap = { const getNetworkDisplayName = key => networkToNameMap[key] +function formatTxMetaForRpcResult (txMeta) { + return { + 'blockHash': txMeta.txReceipt ? txMeta.txReceipt.blockHash : null, + 'blockNumber': txMeta.txReceipt ? txMeta.txReceipt.blockNumber : null, + 'from': txMeta.txParams.from, + 'gas': txMeta.txParams.gas, + 'gasPrice': txMeta.txParams.gasPrice, + 'hash': txMeta.hash, + 'input': txMeta.txParams.data || '0x', + 'nonce': txMeta.txParams.nonce, + 'to': txMeta.txParams.to, + 'transactionIndex': txMeta.txReceipt ? txMeta.txReceipt.transactionIndex : null, + 'value': txMeta.txParams.value || '0x0', + 'v': txMeta.v, + 'r': txMeta.r, + 's': txMeta.s, + } +} + + module.exports = { getNetworkDisplayName, + formatTxMetaForRpcResult, } diff --git a/app/scripts/controllers/onboarding.js b/app/scripts/controllers/onboarding.js index a29c8407a..5d00fb775 100644 --- a/app/scripts/controllers/onboarding.js +++ b/app/scripts/controllers/onboarding.js @@ -1,5 +1,6 @@ const ObservableStore = require('obs-store') const extend = require('xtend') +const log = require('loglevel') /** * @typedef {Object} InitState @@ -9,11 +10,12 @@ const extend = require('xtend') /** * @typedef {Object} OnboardingOptions * @property {InitState} initState The initial controller state + * @property {PreferencesController} preferencesController Controller for managing user perferences */ /** * Controller responsible for maintaining - * a cache of account balances in local storage + * state related to onboarding */ class OnboardingController { /** @@ -22,10 +24,28 @@ class OnboardingController { * @param {OnboardingOptions} [opts] Controller configuration parameters */ constructor (opts = {}) { - const initState = extend({ - seedPhraseBackedUp: true, - }, opts.initState) + const initialTransientState = { + onboardingTabs: {}, + } + const initState = extend( + { + seedPhraseBackedUp: true, + }, + opts.initState, + initialTransientState, + ) this.store = new ObservableStore(initState) + this.preferencesController = opts.preferencesController + this.completedOnboarding = this.preferencesController.store.getState().completedOnboarding + + this.preferencesController.store.subscribe(({ completedOnboarding }) => { + if (completedOnboarding !== this.completedOnboarding) { + this.completedOnboarding = completedOnboarding + if (completedOnboarding) { + this.store.updateState(initialTransientState) + } + } + }) } setSeedPhraseBackedUp (newSeedPhraseBackUpState) { @@ -38,6 +58,24 @@ class OnboardingController { return this.store.getState().seedPhraseBackedUp } + /** + * Registering a site as having initiated onboarding + * + * @param {string} location - The location of the site registering + * @param {string} tabId - The id of the tab registering + */ + async registerOnboarding (location, tabId) { + if (this.completedOnboarding) { + log.debug('Ignoring registerOnboarding; user already onboarded') + return + } + const onboardingTabs = Object.assign({}, this.store.getState().onboardingTabs) + if (!onboardingTabs[location] || onboardingTabs[location] !== tabId) { + log.debug(`Registering onboarding tab at location '${location}' with tabId '${tabId}'`) + onboardingTabs[location] = tabId + this.store.updateState({ onboardingTabs }) + } + } } module.exports = OnboardingController diff --git a/app/scripts/controllers/permissions/index.js b/app/scripts/controllers/permissions/index.js index 8f677ac7e..5f93cd711 100644 --- a/app/scripts/controllers/permissions/index.js +++ b/app/scripts/controllers/permissions/index.js @@ -1 +1,555 @@ -module.exports = require('./permissions') +const JsonRpcEngine = require('json-rpc-engine') +const asMiddleware = require('json-rpc-engine/src/asMiddleware') +const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware') +const ObservableStore = require('obs-store') +const RpcCap = require('rpc-cap').CapabilitiesController +const { ethErrors } = require('eth-json-rpc-errors') + +const { + getExternalRestrictedMethods, + pluginRestrictedMethodDescriptions, +} = require('./restrictedMethods') +const createMethodMiddleware = require('./methodMiddleware') +const createLoggerMiddleware = require('./loggerMiddleware') + +// Methods that do not require any permissions to use: +const SAFE_METHODS = require('./permissions-safe-methods.json') + +const METADATA_STORE_KEY = 'domainMetadata' +const LOG_STORE_KEY = 'permissionsLog' +const HISTORY_STORE_KEY = 'permissionsHistory' +const WALLET_METHOD_PREFIX = 'wallet_' +const CAVEAT_NAMES = { + exposedAccounts: 'exposedAccounts', +} +const ACCOUNTS_CHANGED_NOTIFICATION = 'wallet_accountsChanged' + +function prefix (method) { + return WALLET_METHOD_PREFIX + method +} + +class PermissionsController { + + constructor ({ + openPopup, closePopup, keyringController, assetsController, + setupProvider, pluginRestrictedMethods, getApi, notifyDomain, + notifyAllDomains, + } = {}, + restoredState = {} + ) { + this.store = new ObservableStore({ + [METADATA_STORE_KEY]: restoredState[METADATA_STORE_KEY] || {}, + [LOG_STORE_KEY]: restoredState[LOG_STORE_KEY] || [], + [HISTORY_STORE_KEY]: restoredState[HISTORY_STORE_KEY] || {}, + }) + this.notifyDomain = notifyDomain + this.notifyAllDomains = notifyAllDomains + this._openPopup = openPopup + this._closePopup = closePopup + this.keyringController = keyringController + this.assetsController = assetsController + this.setupProvider = setupProvider + this.externalRestrictedMethods = getExternalRestrictedMethods(this) + this.pluginRestrictedMethods = pluginRestrictedMethods + this.getApi = getApi + } + + createMiddleware ({ origin, extensionId, isPlugin }) { + + if (extensionId) { + this.store.updateState({ + [METADATA_STORE_KEY]: { + ...this.store.getState()[METADATA_STORE_KEY], + [origin]: { extensionId }, + }, + }) + } + + const engine = new JsonRpcEngine() + engine.push(this.createPluginMethodRestrictionMiddleware(isPlugin)) + engine.push(createMethodMiddleware({ + store: this.store, + storeKey: METADATA_STORE_KEY, + getAccounts: this.getAccounts.bind(this, origin), + requestAccountsPermission: this._requestPermissions.bind( + this, origin, { eth_accounts: {} } + ), + handleInstallPlugins: this.handleInstallPlugins.bind(this), + })) + engine.push(createLoggerMiddleware({ + walletPrefix: WALLET_METHOD_PREFIX, + restrictedMethods: ( + Object.keys(this.externalRestrictedMethods) + .concat(Object.keys(this.pluginRestrictedMethods)) + ), + store: this.store, + logStoreKey: LOG_STORE_KEY, + historyStoreKey: HISTORY_STORE_KEY, + })) + engine.push(this.permissions.providerMiddlewareFunction.bind( + this.permissions, { origin } + )) + return asMiddleware(engine) + } + + /** + * Create middleware for prevent non-plugins from accessing methods only available to plugins + */ + createPluginMethodRestrictionMiddleware (isPlugin) { + return createAsyncMiddleware(async (req, res, next) => { + if (typeof req.method !== 'string') { + res.error = ethErrors.rpc.invalidRequest({ data: req }) + return // TODO:json-rpc-engine + } + + if (pluginRestrictedMethodDescriptions[req.method] && !isPlugin) { + res.error = ethErrors.rpc.methodNotFound({ data: req.method }) + return + } + + return next() + }) + } + + /** + * @param {string} origin - The external domain id. + * @param {Array} requestedPlugins - The names of the requested plugin permissions. + */ + async handleInstallPlugins (origin, requestedPlugins) { + + const existingPerms = this.permissions.getPermissionsForDomain(origin).reduce( + (acc, p) => { + acc[p.parentCapability] = true + return acc + }, {} + ) + + requestedPlugins.forEach(p => { + if (!existingPerms[p]) { + throw ethErrors.provider.unauthorized(`Not authorized to install plugin '${p}'.`) + } + }) + + const installedPlugins = await this.pluginsController.processRequestedPlugins(requestedPlugins) + + if (installedPlugins.length === 0) { + // TODO:plugins reserve error in Ethereum error space? + throw ethErrors.provider.custom({ + code: 4301, + message: 'Failed to install all plugins.', + data: requestedPlugins, + }) + } + + return installedPlugins + } + + /** + * Returns the accounts that should be exposed for the given origin domain, + * if any. This method exists for when a trusted context needs to know + * which accounts are exposed to a given domain. + * + * Do not use in untrusted contexts; just send an RPC request. + * + * @param {string} origin + */ + getAccounts (origin) { + return new Promise((resolve, _) => { + const req = { method: 'eth_accounts' } + const res = {} + this.permissions.providerMiddlewareFunction( + { origin }, req, res, () => {}, _end + ) + + function _end () { + if (res.error || !Array.isArray(res.result)) { + resolve([]) + } else { + resolve(res.result) + } + } + }) + } + + /** + * Submits a permissions request to rpc-cap. Internal use only. + * + * @param {string} origin - The origin string. + * @param {IRequestedPermissions} permissions - The requested permissions. + */ + _requestPermissions (origin, permissions) { + return new Promise((resolve, reject) => { + + const req = { method: 'wallet_requestPermissions', params: [permissions] } + const res = {} + this.permissions.providerMiddlewareFunction( + { origin }, req, res, () => {}, _end + ) + + function _end (err) { + if (err || res.error) { + reject(err || res.error) + } else { + resolve(res.result) + } + } + }) + } + + /** + * User approval callback. + * @param {object} approved the approved request object + */ + async approvePermissionsRequest (approved) { + const { id } = approved.metadata + const approval = this.pendingApprovals[id] + this._closePopup && this._closePopup() + + try { + + // TODO:plugins: perform plugin preflight check? + // e.g., is the plugin valid? can its manifest be fetched? is the manifest valid? + // not strictly necessary, but probably good UX. + // const pluginNames = this.pluginsFromPerms(approved.permissions) + + const resolve = approval.resolve + resolve(approved.permissions) + delete this.pendingApprovals[id] + + } catch (reason) { + const { reject } = approval + reject(reason) + } + } + + pluginsFromPerms (permissions) { + const permStrings = Object.keys(permissions) + return permStrings.filter((perm) => { + return perm.indexOf('wallet_plugin_') === 0 + }) + .map(perm => perm.substr(14)) + } + + /** + * User rejection callback. + * @param {string} id the id of the rejected request + */ + async rejectPermissionsRequest (id) { + const approval = this.pendingApprovals[id] + const reject = approval.reject + reject(false) // TODO:lps:review should this be an error instead? + this._closePopup && this._closePopup() + delete this.pendingApprovals[id] + } + + /** + * Grants the given origin the eth_accounts permission for the given account(s). + * This method should ONLY be called as a result of direct user action in the UI, + * with the intention of supporting legacy dapps that don't support EIP 1102. + * + * @param {string} origin - The origin to expose the account(s) to. + * @param {Array} accounts - The account(s) to expose. + */ + async legacyExposeAccounts (origin, accounts) { + + const permissions = { + eth_accounts: {}, + } + + await this.finalizePermissionsRequest(permissions, accounts) + + let error + try { + error = await new Promise((resolve) => { + this.permissions.grantNewPermissions(origin, permissions, {}, err => resolve(err)) + }) + } catch (err) { + error = err + } + + if (error) { + if (error.code === 4001) { + throw error + } else { + throw ethErrors.rpc.internal({ + message: `Failed to add 'eth_accounts' to '${origin}'.`, + data: { + originalError: error, + accounts, + }, + }) + } + } + } + + /** + * Update the accounts exposed to the given origin. + * Throws error if the update fails. + * + * @param {string} origin - The origin to change the exposed accounts for. + * @param {string[]} accounts - The new account(s) to expose. + */ + async updateExposedAccounts (origin, accounts) { + + await this.validateExposedAccounts(accounts) + + this.permissions.updateCaveatFor( + origin, 'eth_accounts', CAVEAT_NAMES.exposedAccounts, accounts + ) + + this.notifyDomain(origin, { + method: ACCOUNTS_CHANGED_NOTIFICATION, + result: accounts, + }) + } + + /** + * Finalizes a permissions request. + * Throws if request validation fails. + * + * @param {Object} requestedPermissions - The requested permissions. + * @param {string[]} accounts - The accounts to expose, if any. + */ + async finalizePermissionsRequest (requestedPermissions, accounts) { + + const { eth_accounts: ethAccounts } = requestedPermissions + + if (ethAccounts) { + + await this.validateExposedAccounts(accounts) + + if (!ethAccounts.caveats) { + ethAccounts.caveats = [] + } + + // caveat names are unique, and we will only construct this caveat here + ethAccounts.caveats = ethAccounts.caveats.filter(c => ( + c.name !== CAVEAT_NAMES.exposedAccounts + )) + + ethAccounts.caveats.push( + { + type: 'filterResponse', + value: accounts, + name: CAVEAT_NAMES.exposedAccounts, + }, + ) + } + } + + /** + * Validate an array of accounts representing accounts to be exposed + * to a domain. Throws error if validation fails. + * + * @param {string[]} accounts - An array of addresses. + */ + async validateExposedAccounts (accounts) { + + if (!Array.isArray(accounts) || accounts.length === 0) { + throw new Error('Must provide non-empty array of account(s).') + } + + // assert accounts exist + const allAccounts = await this.keyringController.getAccounts() + accounts.forEach(acc => { + if (!allAccounts.includes(acc)) { + throw new Error(`Unknown account: ${acc}`) + } + }) + } + + /** + * Removes the given permissions for the given domain. + * @param {object} domains { origin: [permissions] } + */ + removePermissionsFor (domains) { + + Object.entries(domains).forEach(([origin, perms]) => { + + this.permissions.removePermissionsFor( + origin, + perms.map(methodName => { + + if (methodName === 'eth_accounts') { + this.notifyDomain( + origin, + { method: ACCOUNTS_CHANGED_NOTIFICATION, result: [] } + ) + } + + return { parentCapability: methodName } + }) + ) + }) + } + + /** + * Removes all permissions for the given domains. + * @param {Array} domainsToDelete - The domains to remove all permissions for. + */ + removeAllPermissionsFor (domainsToDelete) { + const domains = this.permissions.getDomains() + domainsToDelete.forEach(d => { + delete domains[d] + this.notifyDomain(d, { + method: ACCOUNTS_CHANGED_NOTIFICATION, + result: [], + }) + }) + this.permissions.setDomains(domains) + } + + /** + * Gets all caveats for the given origin and permission, or returns null + * if none exist. + * + * @param {string} permission - The name of the target permission. + * @param {string} origin - The origin that has the permission. + */ + getCaveatsFor (permission, origin) { + return this.permissions.getCaveats(origin, permission) || null + } + + /** + * Gets the caveat with the given name for the given permission of the + * given origin. + * + * @param {string} permission - The name of the target permission. + * @param {string} origin - The origin that has the permission. + * @param {string} caveatName - The name of the caveat to retrieve. + */ + getCaveat (permission, origin, caveatName) { + return this.permissions.getCaveat(origin, permission, caveatName) + } + + /** + * Removes all known domains and their related permissions. + */ + clearPermissions () { + this.permissions.clearDomains() + this.notifyAllDomains({ + method: ACCOUNTS_CHANGED_NOTIFICATION, + result: [], + }) + } + + /** + * Clears the permissions log. + */ + clearLog () { + this.store.updateState({ + [LOG_STORE_KEY]: [], + }) + } + + /** + * Clears the permissions history. + */ + clearHistory () { + this.store.updateState({ + [HISTORY_STORE_KEY]: {}, + }) + } + + /** + * Initializes the underlying CapabilitiesController. + * Exposed in case it must be called after constructor due to controller + * initialization order. + * + * @param {Object} opts - The CapabilitiesController options. + * @param {Object} opts.metamaskEventMethods - Plugin-related internal event methods. + * @param {Object} opts.pluginRestrictedMethods - Restricted methods for plugins, if any. + * @param {Object} opts.restoredState - The restored state, if any. + */ + initializePermissions ({ + pluginsController, + metamaskEventMethods = {}, + pluginRestrictedMethods = {}, + restoredState = {}, + } = {}) { + + this.pluginsController = pluginsController + this.metamaskEventMethods = metamaskEventMethods + this.pluginRestrictedMethods = pluginRestrictedMethods + + const initState = Object.keys(restoredState) + .filter(k => { + return ![ + 'permissionsDescriptions', + 'permissionsRequests', + ].includes(k) + }) + .reduce((acc, k) => { + acc[k] = restoredState[k] + return acc + }, {}) + + this.pendingApprovals = {} + + const api = this.getApi() + + const externalMethodsToAddToRestricted = { + ...this.pluginRestrictedMethods, + ...api, + ...this.metamaskEventMethods, + removePermissionsFor: this.removePermissionsFor.bind(this), + getApprovedAccounts: this.getAccounts.bind(this), + } + + const namespacedPluginRestrictedMethods = Object.keys( + externalMethodsToAddToRestricted + ).reduce((acc, methodKey) => { + const hasDescription = externalMethodsToAddToRestricted[methodKey] + if (!hasDescription) { + return acc + } + return { + ...acc, + ['metamask_' + methodKey]: { + description: pluginRestrictedMethodDescriptions[methodKey] || methodKey, + method: 'metamask_' + externalMethodsToAddToRestricted[methodKey], + }, + } + }, {}) + + this.permissions = new RpcCap({ + + // Supports passthrough methods: + safeMethods: SAFE_METHODS, + + // optional prefix for internal methods + methodPrefix: WALLET_METHOD_PREFIX, + + restrictedMethods: { + ...this.externalRestrictedMethods, ...namespacedPluginRestrictedMethods, + }, + + /** + * A promise-returning callback used to determine whether to approve + * permissions requests or not. + * + * Currently only returns a boolean, but eventually should return any specific parameters or amendments to the permissions. + * + * @param {string} domain - The requesting domain string + * @param {string} req - The request object sent in to the `requestPermissions` method. + * @returns {Promise} approved - Whether the user approves the request or not. + */ + requestUserApproval: async (options) => { + const { metadata } = options + const { id } = metadata + + this._openPopup && this._openPopup() + + return new Promise((resolve, reject) => { + this.pendingApprovals[id] = { resolve, reject } + }) + }, + }, initState) + } + +} + +module.exports = { + PermissionsController, + addInternalMethodPrefix: prefix, +} diff --git a/app/scripts/controllers/permissions/internalMethodMiddleware.js b/app/scripts/controllers/permissions/internalMethodMiddleware.js deleted file mode 100644 index 4acd9189c..000000000 --- a/app/scripts/controllers/permissions/internalMethodMiddleware.js +++ /dev/null @@ -1,91 +0,0 @@ - -const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware') -const { errors: rpcErrors } = require('eth-json-rpc-errors') - -/** - * Create middleware for preprocessing permissions requests. - */ -module.exports = function createRequestMiddleware ({ - store, storeKey, handleInstallPlugins, -}) { - return createAsyncMiddleware(async (req, res, next) => { - - if (typeof req.method !== 'string') { - res.error = rpcErrors.invalidRequest(null, req) - return - } - - const prefix = 'wallet_' - const pluginPrefix = prefix + 'plugin_' - - if (req.method.startsWith(prefix)) { - - switch (req.method.split(prefix)[1]) { - - case 'installPlugins': - - if ( - !Array.isArray(req.params) || typeof req.params[0] !== 'object' - ) { - res.error = rpcErrors.invalidParams(null, req) - return - } - - const requestedPlugins = Object.keys(req.params[0]).filter( - p => p.startsWith(pluginPrefix) - ) - - if (requestedPlugins.length === 0) { - res.error = rpcErrors.invalidParams('Must request at least one plugin.', req) - } - - try { - res.result = await handleInstallPlugins(req.origin, requestedPlugins) - } catch (err) { - res.error = err - } - return - - case 'sendDomainMetadata': - if ( - req.siteMetadata && - typeof req.domainMetadata.name === 'string' - ) { - addDomainMetadata(req.origin, req.domainMetadata) - } - res.result = true - return - - default: - break - } - - // plugin metadata is handled here - // TODO:plugin handle this better, rename siteMetadata to domainMetadata everywhere - } else if ( - req.origin !== 'MetaMask' && - !getOwnState().hasOwnProperty(req.origin) - ) { - let name = 'Unknown Domain' - try { - name = new URL(req.origin).hostname - } catch (err) {} // noop - addDomainMetadata(req.origin, { name }) - } - - return next() - }) - - function addDomainMetadata (origin, metadata) { - store.updateState({ - [storeKey]: { - ...getOwnState(), - [origin]: metadata, - }, - }) - } - - function getOwnState () { - return store.getState()[storeKey] - } -} diff --git a/app/scripts/controllers/permissions/loggerMiddleware.js b/app/scripts/controllers/permissions/loggerMiddleware.js index fd5d3e080..f2a7a4b22 100644 --- a/app/scripts/controllers/permissions/loggerMiddleware.js +++ b/app/scripts/controllers/permissions/loggerMiddleware.js @@ -53,7 +53,9 @@ module.exports = function createLoggerMiddleware ({ } function addResponse (activityEntry, response, time) { - if (!response) return + if (!response) { + return + } activityEntry.response = cloneObj(response) activityEntry.responseTime = time activityEntry.success = !response.error @@ -73,7 +75,9 @@ module.exports = function createLoggerMiddleware ({ !request.params || typeof request.params[0] !== 'object' || Array.isArray(request.params[0]) - ) return null + ) { + return null + } return Object.keys(request.params[0]) } @@ -139,12 +143,16 @@ function cloneObj (obj) { } function getAccountsFromPermission (perm) { - if (perm.parentCapability !== 'eth_accounts' || !perm.caveats) return [] + if (perm.parentCapability !== 'eth_accounts' || !perm.caveats) { + return [] + } const accounts = {} for (const c of perm.caveats) { if (c.type === 'filterResponse' && Array.isArray(c.value)) { for (const v of c.value) { - if (isValidAddress(v)) accounts[v] = true + if (isValidAddress(v)) { + accounts[v] = true + } } } } diff --git a/app/scripts/controllers/permissions/methodMiddleware.js b/app/scripts/controllers/permissions/methodMiddleware.js new file mode 100644 index 000000000..daa2558b4 --- /dev/null +++ b/app/scripts/controllers/permissions/methodMiddleware.js @@ -0,0 +1,141 @@ + +const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware') +const { ethErrors } = require('eth-json-rpc-errors') + +/** + * Create middleware for preprocessing permissions requests. + */ +module.exports = function createRequestMiddleware ({ + store, storeKey, getAccounts, requestAccountsPermission, handleInstallPlugins, +}) { + return createAsyncMiddleware(async (req, res, next) => { + + if (typeof req.method !== 'string') { + res.error = ethErrors.rpc.invalidRequest({ data: req }) + return + } + + const prefix = 'wallet_' + const pluginPrefix = prefix + 'plugin_' + + switch (req.method) { + + // intercepting eth_accounts requests for backwards compatibility, + // i.e. return an empty array instead of an error + case 'eth_accounts': + + res.result = await getAccounts() + return + + case 'eth_requestAccounts': + + // first, just try to get accounts + let accounts = await getAccounts() + if (accounts.length > 0) { + res.result = accounts + return + } + + // if no accounts, request the accounts permission + try { + await requestAccountsPermission() + } catch (err) { + res.error = err + return + } + + // get the accounts again + accounts = await getAccounts() + if (accounts.length > 0) { + res.result = accounts + } else { + // this should never happen + res.error = ethErrors.rpc.internal( + 'Accounts unexpectedly unavailable. Please report this bug.' + ) + } + + return + + case 'wallet_installPlugins': + + if ( + !Array.isArray(req.params) || typeof req.params[0] !== 'object' + ) { + res.error = ethErrors.rpc.invalidParams({ data: req }) + return + } + + const requestedPlugins = Object.keys(req.params[0]).filter( + p => p.startsWith(pluginPrefix) + ) + + if (requestedPlugins.length === 0) { + res.error = ethErrors.rpc.invalidParams({ + message: 'Must request at least one plugin.', data: req, + }) + } + + try { + res.result = await handleInstallPlugins(req.origin, requestedPlugins) + } catch (err) { + res.error = err + } + return + + // custom method for getting metadata from the requesting domain + case 'wallet_sendDomainMetadata': + + if ( + req.domainMetadata && + typeof req.domainMetadata.name === 'string' + ) { + addDomainMetadata(req.origin, req.domainMetadata) + } + + res.result = true + return + + default: + break + } + + if ( + req.origin !== 'metamask' && ( + getOwnState() && !getOwnState()[req.origin] + ) + ) { + // plugin metadata is handled here for now? + // TODO:plugin handle this better, rename siteMetadata to domainMetadata everywhere + let name = 'Unknown Domain' + try { + name = new URL(req.origin).hostname + } catch (err) {} // noop + addDomainMetadata(req.origin, { name }) + } + + return next() + }) + + function addDomainMetadata (origin, metadata) { + + // extensionId added higher up the stack, preserve it if it exists + const currentState = store.getState()[storeKey] + if (currentState[origin] && currentState[origin].extensionId) { + metadata.extensionId = currentState[origin].extensionId + } + + store.updateState({ + [storeKey]: { + ...currentState, + [origin]: { + ...metadata, + }, + }, + }) + } + + function getOwnState () { + return store.getState()[storeKey] + } +} diff --git a/app/scripts/controllers/permissions/permissions-safe-methods.json b/app/scripts/controllers/permissions/permissions-safe-methods.json index 37042422e..17b46b531 100644 --- a/app/scripts/controllers/permissions/permissions-safe-methods.json +++ b/app/scripts/controllers/permissions/permissions-safe-methods.json @@ -5,6 +5,7 @@ "net_version", "eth_blockNumber", "eth_call", + "eth_chainId", "eth_coinbase", "eth_estimateGas", "eth_gasPrice", diff --git a/app/scripts/controllers/permissions/permissions.js b/app/scripts/controllers/permissions/permissions.js deleted file mode 100644 index 4b76dca53..000000000 --- a/app/scripts/controllers/permissions/permissions.js +++ /dev/null @@ -1,347 +0,0 @@ -const JsonRpcEngine = require('json-rpc-engine') -const asMiddleware = require('json-rpc-engine/src/asMiddleware') -const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware') -const ObservableStore = require('obs-store') -const RpcCap = require('rpc-cap').CapabilitiesController -const { errors: rpcErrors } = require('eth-json-rpc-errors') - -const { - getExternalRestrictedMethods, - pluginRestrictedMethodDescriptions, -} = require('./restrictedMethods') -const createInternalMethodMiddleware = require('./internalMethodMiddleware') -const createLoggerMiddleware = require('./loggerMiddleware') - -// Methods that do not require any permissions to use: -const SAFE_METHODS = require('./permissions-safe-methods.json') - -const METADATA_STORE_KEY = 'siteMetadata' -const LOG_STORE_KEY = 'permissionsLog' -const HISTORY_STORE_KEY = 'permissionsHistory' -const WALLET_METHOD_PREFIX = 'wallet_' - -function prefix (method) { - return WALLET_METHOD_PREFIX + method -} - -// class PermissionsController extends SafeEventEmitter { -class PermissionsController { - - constructor ({ - openPopup, closePopup, keyringController, assetsController, - setupProvider, pluginRestrictedMethods, getApi, - } = {}, - restoredState = {} - ) { - this.store = new ObservableStore({ - [METADATA_STORE_KEY]: restoredState[METADATA_STORE_KEY] || {}, - [LOG_STORE_KEY]: restoredState[LOG_STORE_KEY] || [], - [HISTORY_STORE_KEY]: restoredState[HISTORY_STORE_KEY] || {}, - }) - this._openPopup = openPopup - this._closePopup = closePopup - this.keyringController = keyringController - this.assetsController = assetsController - this.setupProvider = setupProvider - this.externalRestrictedMethods = getExternalRestrictedMethods(this) - this.pluginRestrictedMethods = pluginRestrictedMethods - this.getApi = getApi - } - - createMiddleware (options) { - const { origin, isPlugin } = options - const engine = new JsonRpcEngine() - engine.push(this.createPluginMethodRestrictionMiddleware(isPlugin)) - engine.push(createInternalMethodMiddleware({ - store: this.store, - storeKey: METADATA_STORE_KEY, - handleInstallPlugins: this.handleInstallPlugins.bind(this), - })) - engine.push(createLoggerMiddleware({ - walletPrefix: WALLET_METHOD_PREFIX, - restrictedMethods: ( - Object.keys(this.externalRestrictedMethods) - .concat(Object.keys(this.pluginRestrictedMethods)) - ), - store: this.store, - logStoreKey: LOG_STORE_KEY, - historyStoreKey: HISTORY_STORE_KEY, - })) - engine.push(this.permissions.providerMiddlewareFunction.bind( - this.permissions, { origin } - )) - return asMiddleware(engine) - } - - /** - * Create middleware for prevent non-plugins from accessing methods only available to plugins - */ - createPluginMethodRestrictionMiddleware (isPlugin) { - return createAsyncMiddleware(async (req, res, next) => { - if (typeof req.method !== 'string') { - res.error = rpcErrors.invalidRequest(null, req) - return // TODO:json-rpc-engine - } - - if (pluginRestrictedMethodDescriptions[req.method] && !isPlugin) { - res.error = rpcErrors.methodNotFound(null, req.method) - return - } - - return next() - }) - } - - /** - * @param {string} origin - The external domain id. - * @param {Array} requestedPlugins - The names of the requested plugin permissions. - */ - async handleInstallPlugins (origin, requestedPlugins) { - - const existingPerms = this.permissions.getPermissionsForDomain(origin).reduce( - (acc, p) => { - acc[p.parentCapability] = true - return acc - }, {} - ) - - requestedPlugins.forEach(p => { - if (!existingPerms[p]) { - throw rpcErrors.eth.unauthorized(`Not authorized to install plugin '${p}'.`) - } - }) - - const installedPlugins = await this.pluginsController.processRequestedPlugins(requestedPlugins) - - if (installedPlugins.length === 0) { - // TODO:plugins reserve error in Ethereum error space? - throw rpcErrors.eth.custom(4301, 'Failed to install all plugins.', requestedPlugins) - } - - return installedPlugins - } - - /** - * Returns the accounts that should be exposed for the given origin domain, - * if any. This method exists for when a trusted context needs to know - * which accounts are exposed to a given domain. - * - * Do not use in untrusted contexts; just send an RPC request. - * - * @param {string} origin - */ - getAccounts (origin) { - return new Promise((resolve, _) => { - const req = { method: 'eth_accounts' } - const res = {} - this.permissions.providerMiddlewareFunction( - { origin }, req, res, () => {}, _end - ) - - function _end () { - if (res.error || !Array.isArray(res.result)) resolve([]) - else resolve(res.result) - } - }) - } - - /** - * Removes the given permissions for the given domains. - * @param {object} domains { origin: [permissions] } - */ - removePermissionsFor (domains) { - Object.entries(domains).forEach(([origin, perms]) => { - this.permissions.removePermissionsFor( - origin, - perms.map(methodName => { - return { parentCapability: methodName } - }) - ) - }) - } - - /** - * Removes all permissions for the given domains. - * @param {Array} domainsToDelete - The domains to remove all permissions for. - */ - removeAllPermissionsFor (domainsToDelete) { - const domains = this.permissions.getDomains() - domainsToDelete.forEach(d => { - delete domains[d] - }) - this.permissions.setDomains(domains) - } - - /** - * Removes all known domains and their related permissions. - */ - clearPermissions () { - this.permissions.clearDomains() - } - - /** - * Clears the permissions log. - */ - clearLog () { - this.store.updateState({ - [LOG_STORE_KEY]: [], - }) - } - - /** - * Clears the permissions history. - */ - clearHistory () { - this.store.updateState({ - [HISTORY_STORE_KEY]: {}, - }) - } - - /** - * User approval callback. - * @param {object} approved the approved request object - */ - async approvePermissionsRequest (approved) { - const { id } = approved.metadata - const approval = this.pendingApprovals[id] - this._closePopup && this._closePopup() - - try { - - // TODO:plugins: perform plugin preflight check? - // e.g., is the plugin valid? can its manifest be fetched? is the manifest valid? - // not strictly necessary, but probably good UX. - // const pluginNames = this.pluginsFromPerms(approved.permissions) - - const resolve = approval.resolve - resolve(approved.permissions) - delete this.pendingApprovals[id] - - } catch (reason) { - const { reject } = approval - reject(reason) - } - } - - pluginsFromPerms (permissions) { - const permStrings = Object.keys(permissions) - return permStrings.filter((perm) => { - return perm.indexOf('wallet_plugin_') === 0 - }) - .map(perm => perm.substr(14)) - } - - /** - * User rejection callback. - * @param {string} id the id of the rejected request - */ - async rejectPermissionsRequest (id) { - const approval = this.pendingApprovals[id] - const reject = approval.reject - reject(false) // TODO:lps:review should this be an error instead? - this._closePopup && this._closePopup() - delete this.pendingApprovals[id] - } - - /** - * Initializes the underlying CapabilitiesController. - * Exposed in case it must be called after constructor due to controller - * initialization order. - * - * @param {Object} opts - The CapabilitiesController options. - * @param {Object} opts.metamaskEventMethods - Plugin-related internal event methods. - * @param {Object} opts.pluginRestrictedMethods - Restricted methods for plugins, if any. - * @param {Object} opts.restoredState - The restored state, if any. - */ - initializePermissions ({ - pluginsController, - metamaskEventMethods = {}, - pluginRestrictedMethods = {}, - restoredState = {}, - } = {}) { - - this.pluginsController = pluginsController - this.metamaskEventMethods = metamaskEventMethods - this.pluginRestrictedMethods = pluginRestrictedMethods - - const initState = Object.keys(restoredState) - .filter(k => { - return ![ - 'permissionsDescriptions', - 'permissionsRequests', - ].includes(k) - }) - .reduce((acc, k) => { - acc[k] = restoredState[k] - return acc - }, {}) - - this.pendingApprovals = {} - - const api = this.getApi() - - const externalMethodsToAddToRestricted = { - ...this.pluginRestrictedMethods, - ...api, - ...this.metamaskEventMethods, - removePermissionsFor: this.removePermissionsFor.bind(this), - getApprovedAccounts: this.getAccounts.bind(this), - } - - const namespacedPluginRestrictedMethods = Object.keys( - externalMethodsToAddToRestricted - ).reduce((acc, methodKey) => { - const hasDescription = externalMethodsToAddToRestricted[methodKey] - if (!hasDescription) { - return acc - } - return { - ...acc, - ['metamask_' + methodKey]: { - description: pluginRestrictedMethodDescriptions[methodKey] || methodKey, - method: 'metamask_' + externalMethodsToAddToRestricted[methodKey], - }, - } - }, {}) - - this.permissions = new RpcCap({ - - // Supports passthrough methods: - safeMethods: SAFE_METHODS, - - // optional prefix for internal methods - methodPrefix: WALLET_METHOD_PREFIX, - - restrictedMethods: { - ...this.externalRestrictedMethods, ...namespacedPluginRestrictedMethods, - }, - - /** - * A promise-returning callback used to determine whether to approve - * permissions requests or not. - * - * Currently only returns a boolean, but eventually should return any specific parameters or amendments to the permissions. - * - * @param {string} domain - The requesting domain string - * @param {string} req - The request object sent in to the `requestPermissions` method. - * @returns {Promise} approved - Whether the user approves the request or not. - */ - requestUserApproval: async (options) => { - const { metadata } = options - const { id } = metadata - - this._openPopup && this._openPopup() - - return new Promise((resolve, reject) => { - this.pendingApprovals[id] = { resolve, reject } - }) - }, - }, initState) - } - -} - -module.exports = { - PermissionsController, - addInternalMethodPrefix: prefix, -} diff --git a/app/scripts/controllers/permissions/restrictedMethods.js b/app/scripts/controllers/permissions/restrictedMethods.js index df7185755..adc422bc6 100644 --- a/app/scripts/controllers/permissions/restrictedMethods.js +++ b/app/scripts/controllers/permissions/restrictedMethods.js @@ -1,5 +1,5 @@ -const { errors: rpcErrors } = require('eth-json-rpc-errors') +const { ethErrors } = require('eth-json-rpc-errors') const pluginRestrictedMethodDescriptions = { onNewTx: 'Take action whenever a new transaction is created', @@ -101,7 +101,9 @@ function getExternalRestrictedMethods (permissionsController) { res.result = assetsController.removeAsset(requestor, opts) return end() default: - res.error = rpcErrors.methodNotFound(null, `${req.method}:${method}`) + res.error = ethErrors.rpc.methodNotFound({ + data: `${req.method}:${method}`, + }) end(res.error) } } catch (err) { @@ -144,7 +146,9 @@ function getExternalRestrictedMethods (permissionsController) { // Here is where we would invoke the message on that plugin iff possible. const handler = permissionsController.pluginsController.rpcMessageHandlers.get(origin) if (!handler) { - res.error = rpcErrors.methodNotFound(`Plugin RPC message handler not found.`, req.method) + res.error = ethErrors.methodNotFound({ + message: `Plugin RPC message handler not found.`, data: req.method, + }) return end(res.error) } diff --git a/app/scripts/controllers/plugins.js b/app/scripts/controllers/plugins.js index 805a34769..4bd90752a 100644 --- a/app/scripts/controllers/plugins.js +++ b/app/scripts/controllers/plugins.js @@ -4,7 +4,7 @@ const extend = require('xtend') const { pluginRestrictedMethodDescriptions, } = require('./permissions/restrictedMethods') -const { errors: rpcErrors } = require('eth-json-rpc-errors') +const { ethErrors } = require('eth-json-rpc-errors') const isTest = process.env.IN_TEST === 'true' || process.env.METAMASK_ENV === 'test' const SES = ( @@ -64,7 +64,9 @@ class PluginsController extends EventEmitter { Object.values(plugins).forEach(({ pluginName, approvedPermissions, sourceCode }) => { console.log(`running: ${pluginName}`) - const ethereumProvider = this.setupProvider(pluginName, async () => { return {name: pluginName } }, true) + const ethereumProvider = this.setupProvider({ hostname: pluginName }, async () => { + return { name: pluginName } + }, true) try { this._startPlugin(pluginName, approvedPermissions, sourceCode, ethereumProvider) } catch (err) { @@ -152,7 +154,9 @@ class PluginsController extends EventEmitter { const plugin = await this.authorize(pluginName) const { sourceCode, approvedPermissions } = plugin const ethereumProvider = this.setupProvider( - pluginName, async () => { return {name: pluginName } }, true + { hostname: pluginName }, async () => { + return {name: pluginName } + }, true ) await this.run( pluginName, approvedPermissions, sourceCode, ethereumProvider @@ -249,7 +253,9 @@ class PluginsController extends EventEmitter { const pluginState = this.store.getState().plugins const plugin = pluginState[pluginName] const { sourceCode, initialPermissions } = plugin - const ethereumProvider = this.setupProvider(pluginName, async () => { return {name: pluginName } }, true) + const ethereumProvider = this.setupProvider({ hostname: pluginName }, async () => { + return {name: pluginName } + }, true) return new Promise((resolve, reject) => { @@ -264,7 +270,9 @@ class PluginsController extends EventEmitter { jsonrpc: '2.0', params: [ initialPermissions, { sourceCode, ethereumProvider }], }, (err1, res1) => { - if (err1) reject(err1) + if (err1) { + reject(err1) + } const approvedPermissions = res1.result.map(perm => perm.parentCapability) @@ -294,7 +302,9 @@ class PluginsController extends EventEmitter { async apiRequest (plugin, origin) { const handler = this.apiRequestHandlers.get(plugin) if (!handler) { - throw rpcErrors.methodNotFound('apiRequest: ' + plugin) + throw ethErrors.rpc.methodNotFound({ + message: 'Method Not Found: Plugin apiRequest: ' + plugin, + }) } return handler(origin) diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 3b1d3a703..409ce6876 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -18,6 +18,7 @@ class PreferencesController { * @property {object} store.accountTokens The tokens stored per account and then per network type * @property {object} store.assetImages Contains assets objects related to assets added * @property {boolean} store.useBlockie The users preference for blockie identicons within the UI + * @property {boolean} store.useNonceField The users preference for nonce field within the UI * @property {object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the * user wishes to see that feature. * @@ -36,6 +37,7 @@ class PreferencesController { tokens: [], suggestedTokens: {}, useBlockie: false, + useNonceField: false, // WARNING: Do not use feature flags for security-sensitive things. // Feature flag toggling is available in the global namespace @@ -43,7 +45,7 @@ class PreferencesController { // perform sensitive operations. featureFlags: { showIncomingTransactions: true, - threeBox: false, + transactionTime: false, }, knownMethodData: {}, participateInMetaMetrics: null, @@ -90,6 +92,16 @@ class PreferencesController { this.store.updateState({ useBlockie: val }) } + /** + * Setter for the `useNonceField` property + * + * @param {boolean} val Whether or not the user prefers to set nonce + * + */ + setUseNonceField (val) { + this.store.updateState({ useNonceField: val }) + } + /** * Setter for the `participateInMetaMetrics` property * @@ -208,6 +220,16 @@ class PreferencesController { return this.store.getState().useBlockie } + /** + * Getter for the `getUseNonceField` property + * + * @returns {boolean} this.store.getUseNonceField + * + */ + getUseNonceField () { + return this.store.getState().useNonceField + } + /** * Setter for the `currentLocale` property * @@ -284,7 +306,9 @@ class PreferencesController { const accountTokens = this.store.getState().accountTokens addresses.forEach((address) => { // skip if already exists - if (identities[address]) return + if (identities[address]) { + return + } // add missing identity const identityCount = Object.keys(identities).length @@ -316,7 +340,9 @@ class PreferencesController { if (Object.keys(newlyLost).length > 0) { // Notify our servers: - if (this.diagnostics) this.diagnostics.reportOrphans(newlyLost) + if (this.diagnostics) { + this.diagnostics.reportOrphans(newlyLost) + } // store lost accounts for (const key in newlyLost) { @@ -444,7 +470,9 @@ class PreferencesController { * @return {Promise} */ setAccountLabel (account, label) { - if (!account) throw new Error('setAccountLabel requires a valid address, got ' + String(account)) + if (!account) { + throw new Error('setAccountLabel requires a valid address, got ' + String(account)) + } const address = normalizeAddress(account) const {identities} = this.store.getState() identities[address] = identities[address] || {} @@ -481,7 +509,9 @@ class PreferencesController { updateRpc (newRpcDetails) { const rpcList = this.getFrequentRpcListDetail() - const index = rpcList.findIndex((element) => { return element.rpcUrl === newRpcDetails.rpcUrl }) + const index = rpcList.findIndex((element) => { + return element.rpcUrl === newRpcDetails.rpcUrl + }) if (index > -1) { const rpcDetail = rpcList[index] const updatedRpc = extend(rpcDetail, newRpcDetails) @@ -505,7 +535,9 @@ class PreferencesController { */ addToFrequentRpcList (url, chainId, ticker = 'ETH', nickname = '', rpcPrefs = {}) { const rpcList = this.getFrequentRpcListDetail() - const index = rpcList.findIndex((element) => { return element.rpcUrl === url }) + const index = rpcList.findIndex((element) => { + return element.rpcUrl === url + }) if (index !== -1) { rpcList.splice(index, 1) } @@ -529,7 +561,9 @@ class PreferencesController { */ removeFromFrequentRpcList (url) { const rpcList = this.getFrequentRpcListDetail() - const index = rpcList.findIndex((element) => { return element.rpcUrl === url }) + const index = rpcList.findIndex((element) => { + return element.rpcUrl === url + }) if (index !== -1) { rpcList.splice(index, 1) } @@ -661,10 +695,16 @@ class PreferencesController { */ _getTokenRelatedStates (selectedAddress) { const accountTokens = this.store.getState().accountTokens - if (!selectedAddress) selectedAddress = this.store.getState().selectedAddress + if (!selectedAddress) { + selectedAddress = this.store.getState().selectedAddress + } const providerType = this.network.providerStore.getState().type - if (!(selectedAddress in accountTokens)) accountTokens[selectedAddress] = {} - if (!(providerType in accountTokens[selectedAddress])) accountTokens[selectedAddress][providerType] = [] + if (!(selectedAddress in accountTokens)) { + accountTokens[selectedAddress] = {} + } + if (!(providerType in accountTokens[selectedAddress])) { + accountTokens[selectedAddress][providerType] = [] + } const tokens = accountTokens[selectedAddress][providerType] return { tokens, accountTokens, providerType, selectedAddress } } @@ -701,13 +741,19 @@ class PreferencesController { */ _validateERC20AssetParams (opts) { const { rawAddress, symbol, decimals } = opts - if (!rawAddress || !symbol || typeof decimals === 'undefined') throw new Error(`Cannot suggest token without address, symbol, and decimals`) - if (!(symbol.length < 7)) throw new Error(`Invalid symbol ${symbol} more than six characters`) + if (!rawAddress || !symbol || typeof decimals === 'undefined') { + throw new Error(`Cannot suggest token without address, symbol, and decimals`) + } + if (!(symbol.length < 7)) { + throw new Error(`Invalid symbol ${symbol} more than six characters`) + } const numDecimals = parseInt(decimals, 10) if (isNaN(numDecimals) || numDecimals > 36 || numDecimals < 0) { throw new Error(`Invalid decimals ${decimals} must be at least 0, and not over 36`) } - if (!isValidAddress(rawAddress)) throw new Error(`Invalid address ${rawAddress}`) + if (!isValidAddress(rawAddress)) { + throw new Error(`Invalid address ${rawAddress}`) + } } } diff --git a/app/scripts/controllers/recent-blocks.js b/app/scripts/controllers/recent-blocks.js index a2b5d1bae..9e5a384a9 100644 --- a/app/scripts/controllers/recent-blocks.js +++ b/app/scripts/controllers/recent-blocks.js @@ -90,7 +90,9 @@ class RecentBlocksController { async processBlock (newBlockNumberHex) { const newBlockNumber = Number.parseInt(newBlockNumberHex, 16) const newBlock = await this.getBlockByNumber(newBlockNumber, true) - if (!newBlock) return + if (!newBlock) { + return + } const block = this.mapTransactionsToPrices(newBlock) @@ -162,7 +164,9 @@ class RecentBlocksController { await Promise.all(targetBlockNumbers.map(async (targetBlockNumber) => { try { const newBlock = await this.getBlockByNumber(targetBlockNumber, true) - if (!newBlock) return + if (!newBlock) { + return + } this.backfillBlock(newBlock) } catch (e) { diff --git a/app/scripts/controllers/threebox.js b/app/scripts/controllers/threebox.js index 8e83c07ab..8226fb6b1 100644 --- a/app/scripts/controllers/threebox.js +++ b/app/scripts/controllers/threebox.js @@ -2,9 +2,9 @@ const ObservableStore = require('obs-store') const Box = process.env.IN_TEST ? require('../../../development/mock-3box') : require('3box') -// const Box = require(process.env.IN_TEST ? '../lib/mock-3box' : '3box/dist/3box.min') const log = require('loglevel') - +const migrations = require('../migrations/') +const Migrator = require('../lib/migrator') const JsonRpcEngine = require('json-rpc-engine') const providerFromEngine = require('eth-json-rpc-middleware/providerFromEngine') const createMetamaskMiddleware = require('./network/createMetamaskMiddleware') @@ -20,7 +20,6 @@ class ThreeBoxController { addressBookController, version, getKeyringControllerState, - getSelectedAddress, } = opts this.preferencesController = preferencesController @@ -29,27 +28,32 @@ class ThreeBoxController { this.provider = this._createProvider({ version, getAccounts: async ({ origin }) => { - if (origin !== '3Box') { return [] } + if (origin !== '3Box') { + return [] + } const isUnlocked = getKeyringControllerState().isUnlocked - const selectedAddress = getSelectedAddress() + const accounts = await this.keyringController.getAccounts() - if (isUnlocked && selectedAddress) { - return [selectedAddress] + if (isUnlocked && accounts[0]) { + const appKeyAddress = await this.keyringController.getAppKeyAddress(accounts[0], 'wallet://3box.metamask.io') + return [appKeyAddress] } else { return [] } }, - processPersonalMessage: (msgParams) => { - return Promise.resolve(keyringController.signPersonalMessage(msgParams, { + processPersonalMessage: async (msgParams) => { + const accounts = await this.keyringController.getAccounts() + return keyringController.signPersonalMessage({ ...msgParams, from: accounts[0] }, { withAppKeyOrigin: 'wallet://3box.metamask.io', - })) + }) }, }) const initState = { - threeBoxSyncingAllowed: true, - restoredFromThreeBox: null, + threeBoxSyncingAllowed: false, + showRestorePrompt: true, + threeBoxLastUpdated: 0, ...opts.initState, threeBoxAddress: null, threeBoxSynced: false, @@ -57,10 +61,9 @@ class ThreeBoxController { } this.store = new ObservableStore(initState) this.registeringUpdates = false + this.lastMigration = migrations.sort((a, b) => a.version - b.version).slice(-1)[0] - const threeBoxFeatureFlagTurnedOn = this.preferencesController.getFeatureFlags().threeBox - - if (threeBoxFeatureFlagTurnedOn) { + if (initState.threeBoxSyncingAllowed) { this.init() } } @@ -73,12 +76,19 @@ class ThreeBoxController { } } - async _update3Box ({ type }, newState) { + async _update3Box () { try { const { threeBoxSyncingAllowed, threeBoxSynced } = this.store.getState() if (threeBoxSyncingAllowed && threeBoxSynced) { - await this.space.private.set('lastUpdated', Date.now()) - await this.space.private.set(type, JSON.stringify(newState)) + const newState = { + preferences: this.preferencesController.store.getState(), + addressBook: this.addressBookController.state, + lastUpdated: Date.now(), + lastMigration: this.lastMigration, + } + + await this.space.private.set('metamaskBackup', JSON.stringify(newState)) + await this.setShowRestorePromptToFalse() } } catch (error) { console.error(error) @@ -105,11 +115,20 @@ class ThreeBoxController { async new3Box () { const accounts = await this.keyringController.getAccounts() - const address = accounts[0] - - if (this.getThreeBoxSyncingState()) { + this.address = await this.keyringController.getAppKeyAddress(accounts[0], 'wallet://3box.metamask.io') + let backupExists + try { + const threeBoxConfig = await Box.getConfig(this.address) + backupExists = threeBoxConfig.spaces && threeBoxConfig.spaces.metamask + } catch (e) { + if (e.message.match(/^Error: Invalid response \(404\)/)) { + backupExists = false + } else { + throw e + } + } + if (this.getThreeBoxSyncingState() || backupExists) { this.store.updateState({ threeBoxSynced: false }) - this.address = await this.keyringController.getAppKeyAddress(address, 'wallet://3box.metamask.io') let timedOut = false const syncTimeout = setTimeout(() => { @@ -121,13 +140,13 @@ class ThreeBoxController { }) }, SYNC_TIMEOUT) try { - this.box = await Box.openBox(address, this.provider) + this.box = await Box.openBox(this.address, this.provider) await this._waitForOnSyncDone() this.space = await this.box.openSpace('metamask', { onSyncDone: async () => { const stateUpdate = { threeBoxSynced: true, - threeBoxAddress: address, + threeBoxAddress: this.address, } if (timedOut) { log.info(`3Box sync completed after timeout; no longer disabled`) @@ -136,6 +155,7 @@ class ThreeBoxController { clearTimeout(syncTimeout) this.store.updateState(stateUpdate) + log.debug('3Box space sync done') }, }) @@ -147,15 +167,36 @@ class ThreeBoxController { } async getLastUpdated () { - return await this.space.private.get('lastUpdated') + const res = await this.space.private.get('metamaskBackup') + const parsedRes = JSON.parse(res || '{}') + return parsedRes.lastUpdated + } + + async migrateBackedUpState (backedUpState) { + const migrator = new Migrator({ migrations }) + + const formattedStateBackup = { + PreferencesController: backedUpState.preferences, + AddressBookController: backedUpState.addressBook, + } + const initialMigrationState = migrator.generateInitialState(formattedStateBackup) + const migratedState = await migrator.migrateData(initialMigrationState) + return { + preferences: migratedState.PreferencesController, + addressBook: migratedState.AddressBookController, + } } async restoreFromThreeBox () { - this.setRestoredFromThreeBoxToTrue() - const backedUpPreferences = await this.space.private.get('preferences') - backedUpPreferences && this.preferencesController.store.updateState(JSON.parse(backedUpPreferences)) - const backedUpAddressBook = await this.space.private.get('addressBook') - backedUpAddressBook && this.addressBookController.update(JSON.parse(backedUpAddressBook), true) + const backedUpState = await this.space.private.get('metamaskBackup') + const { + preferences, + addressBook, + } = await this.migrateBackedUpState(backedUpState) + this.store.updateState({ threeBoxLastUpdated: backedUpState.lastUpdated }) + preferences && this.preferencesController.store.updateState(JSON.parse(preferences)) + addressBook && this.addressBookController.update(JSON.parse(addressBook), true) + this.setShowRestorePromptToFalse() } turnThreeBoxSyncingOn () { @@ -166,12 +207,8 @@ class ThreeBoxController { this.box.logout() } - setRestoredFromThreeBoxToTrue () { - this.store.updateState({ restoredFromThreeBox: true }) - } - - setRestoredFromThreeBoxToFalse () { - this.store.updateState({ restoredFromThreeBox: false }) + setShowRestorePromptToFalse () { + this.store.updateState({ showRestorePrompt: false }) } setThreeBoxSyncingPermission (newThreeboxSyncingState) { @@ -201,9 +238,9 @@ class ThreeBoxController { _registerUpdates () { if (!this.registeringUpdates) { - const updatePreferences = this._update3Box.bind(this, { type: 'preferences' }) + const updatePreferences = this._update3Box.bind(this) this.preferencesController.store.subscribe(updatePreferences) - const updateAddressBook = this._update3Box.bind(this, { type: 'addressBook' }) + const updateAddressBook = this._update3Box.bind(this) this.addressBookController.subscribe(updateAddressBook) this.registeringUpdates = true } diff --git a/app/scripts/controllers/token-rates.js b/app/scripts/controllers/token-rates.js index 9b86a9ebf..7c8526c34 100644 --- a/app/scripts/controllers/token-rates.js +++ b/app/scripts/controllers/token-rates.js @@ -28,7 +28,9 @@ class TokenRatesController { * Updates exchange rates for all tokens */ async updateExchangeRates () { - if (!this.isActive) { return } + if (!this.isActive) { + return + } const contractExchangeRates = {} const nativeCurrency = this.currency ? this.currency.state.nativeCurrency.toLowerCase() : 'eth' const pairs = this._tokens.map(token => token.address).join(',') @@ -53,8 +55,12 @@ class TokenRatesController { */ set interval (interval) { this._handle && clearInterval(this._handle) - if (!interval) { return } - this._handle = setInterval(() => { this.updateExchangeRates() }, interval) + if (!interval) { + return + } + this._handle = setInterval(() => { + this.updateExchangeRates() + }, interval) } /** @@ -62,10 +68,14 @@ class TokenRatesController { */ set preferences (preferences) { this._preferences && this._preferences.unsubscribe() - if (!preferences) { return } + if (!preferences) { + return + } this._preferences = preferences this.tokens = preferences.getState().tokens - preferences.subscribe(({ tokens = [] }) => { this.tokens = tokens }) + preferences.subscribe(({ tokens = [] }) => { + this.tokens = tokens + }) } /** diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 9a1255a66..9a48b6045 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -3,7 +3,7 @@ const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') const Transaction = require('ethereumjs-tx') const EthQuery = require('ethjs-query') -const { errors: rpcErrors } = require('eth-json-rpc-errors') +const { ethErrors } = require('eth-json-rpc-errors') const abi = require('human-standard-token-abi') const abiDecoder = require('abi-decoder') abiDecoder.addABI(abi) @@ -54,6 +54,7 @@ const { hexToBn, bnToHex, BnMultiplyByFraction } = require('../../lib/util') @param {Object} opts.blockTracker - An instance of eth-blocktracker @param {Object} opts.provider - A network provider. @param {Function} opts.signTransaction - function the signs an ethereumjs-tx + @param {object} opts.getPermittedAccounts - get accounts that an origin has permissions for @param {Function} [opts.getGasPrice] - optional gas price calculator @param {Function} opts.signTransaction - ethTx signer that returns a rawTx @param {Number} [opts.txHistoryLimit] - number *optional* for limiting how many transactions are in state @@ -66,6 +67,7 @@ class TransactionController extends EventEmitter { this.networkStore = opts.networkStore || new ObservableStore({}) this.preferencesStore = opts.preferencesStore || new ObservableStore({}) this.provider = opts.provider + this.getPermittedAccounts = opts.getPermittedAccounts this.blockTracker = opts.blockTracker this.signEthTx = opts.signTransaction this.getGasPrice = opts.getGasPrice @@ -157,7 +159,7 @@ class TransactionController extends EventEmitter { async newUnapprovedTransaction (txParams, opts = {}) { log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`) - const initialTxMeta = await this.addUnapprovedTransaction(txParams) + const initialTxMeta = await this.addUnapprovedTransaction(txParams, opts.origin) initialTxMeta.origin = opts.origin this.txStateManager.updateTx(initialTxMeta, '#newUnapprovedTransaction - adding the origin') // listen for tx completion (success, fail) @@ -167,11 +169,11 @@ class TransactionController extends EventEmitter { case 'submitted': return resolve(finishedTxMeta.hash) case 'rejected': - return reject(cleanErrorStack(rpcErrors.eth.userRejectedRequest('MetaMask Tx Signature: User denied transaction signature.'))) + return reject(cleanErrorStack(ethErrors.provider.userRejectedRequest('MetaMask Tx Signature: User denied transaction signature.'))) case 'failed': - return reject(cleanErrorStack(rpcErrors.internal(finishedTxMeta.err.message))) + return reject(cleanErrorStack(ethErrors.rpc.internal(finishedTxMeta.err.message))) default: - return reject(cleanErrorStack(rpcErrors.internal(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(finishedTxMeta.txParams)}`))) + return reject(cleanErrorStack(ethErrors.rpc.internal(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(finishedTxMeta.txParams)}`))) } }) }) @@ -184,13 +186,29 @@ class TransactionController extends EventEmitter { @returns {txMeta} */ - async addUnapprovedTransaction (txParams) { + async addUnapprovedTransaction (txParams, origin) { + // validate const normalizedTxParams = txUtils.normalizeTxParams(txParams) - // Assert the from address is the selected address - if (normalizedTxParams.from !== this.getSelectedAddress()) { - throw new Error(`Transaction from address isn't valid for this account`) + + // TODO:lps once, I saw 'origin' with a value of 'chrome-extension://...' + // for a transaction initiated from the UI. + // When would this happen? It's a problem if it does. I don't think we want to + // hard-code 'chrome-extension://' here + if (origin === 'metamask') { + // Assert the from address is the selected address + if (normalizedTxParams.from !== this.getSelectedAddress()) { + throw ethErrors.rpc.internal(`Internally initiated transaction is using invalid account.`) + } + } else { + // Assert that the origin has permissions to initiate transactions from + // the specified address + const permittedAddresses = await this.getPermittedAccounts(origin) + if (!permittedAddresses.includes(normalizedTxParams.from)) { + throw ethErrors.provider.unauthorized() + } } + txUtils.validateTxParams(normalizedTxParams) // construct txMeta const { transactionCategory, getCodeResponse } = await this._determineTransactionCategory(txParams) @@ -373,14 +391,21 @@ class TransactionController extends EventEmitter { const txMeta = this.txStateManager.getTx(txId) const fromAddress = txMeta.txParams.from // wait for a nonce + let { customNonceValue = null } = txMeta + customNonceValue = Number(customNonceValue) nonceLock = await this.nonceTracker.getNonceLock(fromAddress) // add nonce to txParams // if txMeta has lastGasPrice then it is a retry at same nonce with higher // gas price transaction and their for the nonce should not be calculated const nonce = txMeta.lastGasPrice ? txMeta.txParams.nonce : nonceLock.nextNonce - txMeta.txParams.nonce = ethUtil.addHexPrefix(nonce.toString(16)) + const customOrNonce = customNonceValue || nonce + + txMeta.txParams.nonce = ethUtil.addHexPrefix(customOrNonce.toString(16)) // add nonce debugging information to txMeta txMeta.nonceDetails = nonceLock.nonceDetails + if (customNonceValue) { + txMeta.nonceDetails.customNonceValue = customNonceValue + } this.txStateManager.updateTx(txMeta, 'transactions#approveTransaction') // sign transaction const rawTx = await this.signTransaction(txId) @@ -395,7 +420,9 @@ class TransactionController extends EventEmitter { log.error(err) } // must set transaction to submitted/failed before releasing lock - if (nonceLock) nonceLock.releaseLock() + if (nonceLock) { + nonceLock.releaseLock() + } // continue with error chain throw err } finally { @@ -417,6 +444,15 @@ class TransactionController extends EventEmitter { const fromAddress = txParams.from const ethTx = new Transaction(txParams) await this.signEthTx(ethTx, fromAddress) + + // add r,s,v values for provider request purposes see createMetamaskMiddleware + // and JSON rpc standard for further explanation + txMeta.r = ethUtil.bufferToHex(ethTx.r) + txMeta.s = ethUtil.bufferToHex(ethTx.s) + txMeta.v = ethUtil.bufferToHex(ethTx.v) + + this.txStateManager.updateTx(txMeta, 'transactions#signTransaction: add r, s, v values') + // set state to signed this.txStateManager.setTxStatusSigned(txMeta.id) const rawTx = ethUtil.bufferToHex(ethTx.serialize()) @@ -433,8 +469,19 @@ class TransactionController extends EventEmitter { const txMeta = this.txStateManager.getTx(txId) txMeta.rawTx = rawTx this.txStateManager.updateTx(txMeta, 'transactions#publishTransaction') - const txHash = await this.query.sendRawTransaction(rawTx) + let txHash + try { + txHash = await this.query.sendRawTransaction(rawTx) + } catch (error) { + if (error.message.toLowerCase().includes('known transaction')) { + txHash = ethUtil.sha3(ethUtil.addHexPrefix(rawTx)).toString('hex') + txHash = ethUtil.addHexPrefix(txHash) + } else { + throw error + } + } this.setTxHash(txId, txHash) + this.txStateManager.setTxStatusSubmitted(txId) } @@ -577,7 +624,9 @@ class TransactionController extends EventEmitter { } }) this.pendingTxTracker.on('tx:retry', (txMeta) => { - if (!('retryCount' in txMeta)) txMeta.retryCount = 0 + if (!('retryCount' in txMeta)) { + txMeta.retryCount = 0 + } txMeta.retryCount++ this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:retry') }) @@ -597,21 +646,21 @@ class TransactionController extends EventEmitter { ].find(tokenMethodName => tokenMethodName === name && name.toLowerCase()) let result - let code - if (!txParams.data) { - result = SEND_ETHER_ACTION_KEY - } else if (tokenMethodName) { + if (txParams.data && tokenMethodName) { result = tokenMethodName - } else if (!to) { + } else if (txParams.data && !to) { result = DEPLOY_CONTRACT_ACTION_KEY - } else { + } + + let code + if (!result) { 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 @@ -631,10 +680,14 @@ class TransactionController extends EventEmitter { const txMeta = this.txStateManager.getTx(txId) const { nonce, from } = txMeta.txParams const sameNonceTxs = this.txStateManager.getFilteredTxList({nonce, from}) - if (!sameNonceTxs.length) return + if (!sameNonceTxs.length) { + return + } // mark all same nonce transactions as dropped and give i a replacedBy hash sameNonceTxs.forEach((otherTxMeta) => { - if (otherTxMeta.id === txId) return + if (otherTxMeta.id === txId) { + return + } otherTxMeta.replacedBy = txMeta.hash this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:confirmed reference to confirmed txHash with same nonce') this.txStateManager.setTxStatusDropped(otherTxMeta.id) diff --git a/app/scripts/controllers/transactions/lib/tx-state-history-helper.js b/app/scripts/controllers/transactions/lib/tx-state-history-helper.js index 76fc5c35b..3cc76e617 100644 --- a/app/scripts/controllers/transactions/lib/tx-state-history-helper.js +++ b/app/scripts/controllers/transactions/lib/tx-state-history-helper.js @@ -18,7 +18,9 @@ function migrateFromSnapshotsToDiffs (longHistory) { longHistory // convert non-initial history entries into diffs .map((entry, index) => { - if (index === 0) return entry + if (index === 0) { + return entry + } return generateHistoryEntry(longHistory[index - 1], entry) }) ) @@ -40,7 +42,9 @@ function generateHistoryEntry (previousState, newState, note) { const entry = jsonDiffer.compare(previousState, newState) // Add a note to the first op, since it breaks if we append it to the entry if (entry[0]) { - if (note) entry[0].note = note + if (note) { + entry[0].note = note + } entry[0].timestamp = Date.now() } diff --git a/app/scripts/controllers/transactions/lib/util.js b/app/scripts/controllers/transactions/lib/util.js index 0d2ddddef..86924e7fa 100644 --- a/app/scripts/controllers/transactions/lib/util.js +++ b/app/scripts/controllers/transactions/lib/util.js @@ -35,7 +35,9 @@ function normalizeTxParams (txParams, LowerCase) { // apply only keys in the normalizers const normalizedTxParams = {} for (const key in normalizers) { - if (txParams[key]) normalizedTxParams[key] = normalizers[key](txParams[key], LowerCase) + if (txParams[key]) { + normalizedTxParams[key] = normalizers[key](txParams[key], LowerCase) + } } return normalizedTxParams } @@ -64,8 +66,12 @@ function validateTxParams (txParams) { @param txParams {object} */ function validateFrom (txParams) { - if (!(typeof txParams.from === 'string')) throw new Error(`Invalid from address ${txParams.from} not a string`) - if (!isValidAddress(txParams.from)) throw new Error('Invalid from address') + if (!(typeof txParams.from === 'string')) { + throw new Error(`Invalid from address ${txParams.from} not a string`) + } + if (!isValidAddress(txParams.from)) { + throw new Error('Invalid from address') + } } /** diff --git a/app/scripts/controllers/transactions/pending-tx-tracker.js b/app/scripts/controllers/transactions/pending-tx-tracker.js index 1ef3be36e..4e2db5ead 100644 --- a/app/scripts/controllers/transactions/pending-tx-tracker.js +++ b/app/scripts/controllers/transactions/pending-tx-tracker.js @@ -56,7 +56,9 @@ class PendingTransactionTracker extends EventEmitter { resubmitPendingTxs (blockNumber) { const pending = this.getPendingTransactions() // only try resubmitting if their are transactions to resubmit - if (!pending.length) return + if (!pending.length) { + return + } pending.forEach((txMeta) => this._resubmitTx(txMeta, blockNumber).catch((err) => { /* Dont marked as failed if the error is a "known" transaction warning @@ -79,7 +81,9 @@ class PendingTransactionTracker extends EventEmitter { errorMessage.includes('nonce too low') ) // ignore resubmit warnings, return early - if (isKnownTx) return + if (isKnownTx) { + return + } // encountered real error - transition to error state txMeta.warning = { error: errorMessage, @@ -107,10 +111,14 @@ class PendingTransactionTracker extends EventEmitter { const retryCount = txMeta.retryCount || 0 // Exponential backoff to limit retries at publishing - if (txBlockDistance <= Math.pow(2, retryCount) - 1) return + if (txBlockDistance <= Math.pow(2, retryCount) - 1) { + return + } // Only auto-submit already-signed txs: - if (!('rawTx' in txMeta)) return this.approveTransaction(txMeta.id) + if (!('rawTx' in txMeta)) { + return this.approveTransaction(txMeta.id) + } const rawTx = txMeta.rawTx const txHash = await this.publishTransaction(rawTx) @@ -132,7 +140,9 @@ class PendingTransactionTracker extends EventEmitter { const txId = txMeta.id // Only check submitted txs - if (txMeta.status !== 'submitted') return + if (txMeta.status !== 'submitted') { + return + } // extra check in case there was an uncaught error during the // signature and submission process @@ -174,7 +184,7 @@ class PendingTransactionTracker extends EventEmitter { // get latest transaction status try { - const { blockNumber } = await this.query.getTransactionByHash(txHash) || {} + const { blockNumber } = await this.query.getTransactionReceipt(txHash) || {} if (blockNumber) { this.emit('tx:confirmed', txId) } @@ -196,7 +206,7 @@ class PendingTransactionTracker extends EventEmitter { async _checkIftxWasDropped (txMeta) { const { txParams: { nonce, from }, hash } = txMeta const nextNonce = await this.query.getTransactionCount(from) - const { blockNumber } = await this.query.getTransactionByHash(hash) || {} + const { blockNumber } = await this.query.getTransactionReceipt(hash) || {} if (!blockNumber && parseInt(nextNonce) > parseInt(nonce)) { return true } diff --git a/app/scripts/controllers/transactions/tx-gas-utils.js b/app/scripts/controllers/transactions/tx-gas-utils.js index 287fb6f44..517137f86 100644 --- a/app/scripts/controllers/transactions/tx-gas-utils.js +++ b/app/scripts/controllers/transactions/tx-gas-utils.js @@ -142,9 +142,13 @@ class TxGasUtil { const bufferedGasLimitBn = initialGasLimitBn.muln(1.5) // if initialGasLimit is above blockGasLimit, dont modify it - if (initialGasLimitBn.gt(upperGasLimitBn)) return bnToHex(initialGasLimitBn) + if (initialGasLimitBn.gt(upperGasLimitBn)) { + return bnToHex(initialGasLimitBn) + } // if bufferedGasLimit is below blockGasLimit, use bufferedGasLimit - if (bufferedGasLimitBn.lt(upperGasLimitBn)) return bnToHex(bufferedGasLimitBn) + if (bufferedGasLimitBn.lt(upperGasLimitBn)) { + return bnToHex(bufferedGasLimitBn) + } // otherwise use blockGasLimit return bnToHex(upperGasLimitBn) } diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js index 6a92c0601..fb9359c79 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -45,7 +45,9 @@ class TransactionStateManager extends EventEmitter { */ generateTxMeta (opts) { const netId = this.getNetwork() - if (netId === 'loading') throw new Error('MetaMask is having trouble connecting to the network') + if (netId === 'loading') { + throw new Error('MetaMask is having trouble connecting to the network') + } return extend({ id: createId(), time: (new Date()).getTime(), @@ -89,7 +91,9 @@ class TransactionStateManager extends EventEmitter { */ getApprovedTransactions (address) { const opts = { status: 'approved' } - if (address) opts.from = address + if (address) { + opts.from = address + } return this.getFilteredTxList(opts) } @@ -100,7 +104,9 @@ class TransactionStateManager extends EventEmitter { */ getPendingTransactions (address) { const opts = { status: 'submitted' } - if (address) opts.from = address + if (address) { + opts.from = address + } return this.getFilteredTxList(opts) } @@ -111,7 +117,9 @@ class TransactionStateManager extends EventEmitter { */ getConfirmedTransactions (address) { const opts = { status: 'confirmed' } - if (address) opts.from = address + if (address) { + opts.from = address + } return this.getFilteredTxList(opts) } @@ -236,10 +244,14 @@ class TransactionStateManager extends EventEmitter { // validate types switch (key) { case 'chainId': - if (typeof value !== 'number' && typeof value !== 'string') throw new Error(`${key} in txParams is not a Number or hex string. got: (${value})`) + if (typeof value !== 'number' && typeof value !== 'string') { + throw new Error(`${key} in txParams is not a Number or hex string. got: (${value})`) + } break default: - if (typeof value !== 'string') throw new Error(`${key} in txParams is not a string. got: (${value})`) + if (typeof value !== 'string') { + throw new Error(`${key} in txParams is not a string. got: (${value})`) + } break } }) diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index 6d7214291..be04b187f 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -63,8 +63,6 @@ const proxiedInpageProvider = new Proxy(inpageProvider, { // straight up lie that we deleted the property so that it doesnt // throw an error in strict mode deleteProperty: () => true, - // TODO:temp - isPluginsBeta: () => true, }) window.ethereum = proxiedInpageProvider diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js index c12ba2497..ff0e69c21 100644 --- a/app/scripts/lib/account-tracker.js +++ b/app/scripts/lib/account-tracker.js @@ -123,7 +123,9 @@ class AccountTracker { // save accounts state this.store.updateState({ accounts }) // fetch balances for the accounts if there is block number ready - if (!this._currentBlockNumber) return + if (!this._currentBlockNumber) { + return + } this._updateAccounts() } @@ -157,7 +159,9 @@ class AccountTracker { // block gasLimit polling shouldn't be in account-tracker shouldn't be here... const currentBlock = await this._query.getBlockByNumber(blockNumber, false) - if (!currentBlock) return + if (!currentBlock) { + return + } const currentBlockGasLimit = currentBlock.gasLimit this.store.updateState({ currentBlockGasLimit }) @@ -217,7 +221,9 @@ class AccountTracker { // update accounts state const { accounts } = this.store.getState() // only populate if the entry is still present - if (!accounts[address]) return + if (!accounts[address]) { + return + } accounts[address] = result this.store.updateState({ accounts }) } diff --git a/app/scripts/lib/buy-eth-url.js b/app/scripts/lib/buy-eth-url.js index 5cae83a9f..d4b74fa19 100644 --- a/app/scripts/lib/buy-eth-url.js +++ b/app/scripts/lib/buy-eth-url.js @@ -13,11 +13,13 @@ module.exports = getBuyEthUrl */ function getBuyEthUrl ({ network, amount, address, service }) { // default service by network if not specified - if (!service) service = getDefaultServiceForNetwork(network) + if (!service) { + service = getDefaultServiceForNetwork(network) + } switch (service) { case 'wyre': - return `https://dash.sendwyre.com/sign-up` + return `https://pay.sendwyre.com/?dest=ethereum:${address}&destCurrency=ETH&accountId=AC-7AG3W4XH4N2` case 'coinswitch': return `https://metamask.coinswitch.co/?address=${address}&to=eth` case 'coinbase': diff --git a/app/scripts/lib/createDnodeRemoteGetter.js b/app/scripts/lib/createDnodeRemoteGetter.js index b70d218f3..60a775383 100644 --- a/app/scripts/lib/createDnodeRemoteGetter.js +++ b/app/scripts/lib/createDnodeRemoteGetter.js @@ -8,7 +8,9 @@ function createDnodeRemoteGetter (dnode) { }) async function getRemote () { - if (remote) return remote + if (remote) { + return remote + } return await new Promise(resolve => dnode.once('remote', resolve)) } diff --git a/app/scripts/lib/createLoggerMiddleware.js b/app/scripts/lib/createLoggerMiddleware.js index 996c3477c..d95cdb465 100644 --- a/app/scripts/lib/createLoggerMiddleware.js +++ b/app/scripts/lib/createLoggerMiddleware.js @@ -13,7 +13,9 @@ function createLoggerMiddleware (opts) { if (res.error) { log.error('Error in RPC response:\n', res) } - if (req.isMetamaskInternal) return + if (req.isMetamaskInternal) { + return + } log.info(`RPC (${opts.origin}):`, req, '->', res) cb() }) diff --git a/app/scripts/lib/ens-ipfs/setup.js b/app/scripts/lib/ens-ipfs/setup.js index 86f3e7d47..f12a22238 100644 --- a/app/scripts/lib/ens-ipfs/setup.js +++ b/app/scripts/lib/ens-ipfs/setup.js @@ -10,7 +10,7 @@ function setupEnsIpfsResolver ({ provider }) { // install listener const urlPatterns = supportedTopLevelDomains.map(tld => `*://*.${tld}/*`) - extension.webRequest.onErrorOccurred.addListener(webRequestDidFail, { urls: urlPatterns }) + extension.webRequest.onErrorOccurred.addListener(webRequestDidFail, { urls: urlPatterns, types: ['main_frame']}) // return api object return { @@ -23,14 +23,18 @@ function setupEnsIpfsResolver ({ provider }) { async function webRequestDidFail (details) { const { tabId, url } = details // ignore requests that are not associated with tabs - if (tabId === -1) return + if (tabId === -1) { + return + } // parse ens name const urlData = urlUtil.parse(url) const { hostname: name, path, search } = urlData const domainParts = name.split('.') const topLevelDomain = domainParts[domainParts.length - 1] // if unsupported TLD, abort - if (!supportedTopLevelDomains.includes(topLevelDomain)) return + if (!supportedTopLevelDomains.includes(topLevelDomain)) { + return + } // otherwise attempt resolve attemptResolve({ tabId, name, path, search }) } @@ -45,7 +49,9 @@ function setupEnsIpfsResolver ({ provider }) { try { // check if ipfs gateway has result const response = await fetch(resolvedUrl, { method: 'HEAD' }) - if (response.status === 200) url = resolvedUrl + if (response.status === 200) { + url = resolvedUrl + } } catch (err) { console.warn(err) } @@ -53,6 +59,8 @@ function setupEnsIpfsResolver ({ provider }) { url = `https://swarm-gateways.net/bzz:/${hash}${path}${search || ''}` } else if (type === 'onion' || type === 'onion3') { url = `http://${hash}.onion${path}${search || ''}` + } else if (type === 'zeronet') { + url = `http://127.0.0.1:43110/${hash}${path}${search || ''}` } } catch (err) { console.warn(err) diff --git a/app/scripts/lib/freezeGlobals.js b/app/scripts/lib/freezeGlobals.js new file mode 100644 index 000000000..20f48ebf2 --- /dev/null +++ b/app/scripts/lib/freezeGlobals.js @@ -0,0 +1,41 @@ + +/** + * Freezes the Promise global and prevents its reassignment. + */ +const deepFreeze = require('deep-freeze-strict') + +if ( + process.env.IN_TEST !== 'true' && + process.env.METAMASK_ENV !== 'test' +) { + freeze(global, 'Promise') +} + +/** + * Makes a key:value pair on a target object immutable, with limitations. + * The key cannot be reassigned or deleted, and the value is recursively frozen + * using Object.freeze. + * + * Because of JavaScript language limitations, this is does not mean that the + * value is completely immutable. It is, however, better than nothing. + * + * @param {Object} target - The target object to freeze a property on. + * @param {String} key - The key to freeze. + * @param {any} [value] - The value to freeze, if different from the existing value on the target. + * @param {boolean} [enumerable=true] - If given a value, whether the property is enumerable. + */ +function freeze (target, key, value, enumerable = true) { + + const opts = { + configurable: false, writable: false, + } + + if (value !== undefined) { + opts.value = deepFreeze(value) + opts.enumerable = enumerable + } else { + target[key] = deepFreeze(target[key]) + } + + Object.defineProperty(target, key, opts) +} diff --git a/app/scripts/lib/local-store.js b/app/scripts/lib/local-store.js index fbcba09cd..49294c84d 100644 --- a/app/scripts/lib/local-store.js +++ b/app/scripts/lib/local-store.js @@ -20,7 +20,9 @@ module.exports = class ExtensionStore { * @return {Promise<*>} */ async get () { - if (!this.isSupported) return undefined + if (!this.isSupported) { + return undefined + } const result = await this._get() // extension.storage.local always returns an obj // if the object is empty, treat it as undefined @@ -49,7 +51,7 @@ module.exports = class ExtensionStore { const local = extension.storage.local return new Promise((resolve, reject) => { local.get(null, (/** @type {any} */ result) => { - const err = extension.runtime.lastError + const err = checkForError() if (err) { reject(err) } else { @@ -69,7 +71,7 @@ module.exports = class ExtensionStore { const local = extension.storage.local return new Promise((resolve, reject) => { local.set(obj, () => { - const err = extension.runtime.lastError + const err = checkForError() if (err) { reject(err) } else { @@ -88,3 +90,21 @@ module.exports = class ExtensionStore { function isEmpty (obj) { return Object.keys(obj).length === 0 } + +/** + * Returns an Error if extension.runtime.lastError is present + * this is a workaround for the non-standard error object thats used + * @returns {Error} + */ +function checkForError () { + const lastError = extension.runtime.lastError + if (!lastError) { + return + } + // if it quacks like an Error, its an Error + if (lastError.stack && lastError.message) { + return lastError + } + // repair incomplete error object (eg chromium v77) + return new Error(lastError.message) +} diff --git a/app/scripts/lib/message-manager.js b/app/scripts/lib/message-manager.js index 8e1ff34b7..83e502870 100644 --- a/app/scripts/lib/message-manager.js +++ b/app/scripts/lib/message-manager.js @@ -1,7 +1,7 @@ const EventEmitter = require('events') const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') -const { errors: rpcErrors } = require('eth-json-rpc-errors') +const { ethErrors } = require('eth-json-rpc-errors') const createId = require('./random-id') /** @@ -62,7 +62,9 @@ module.exports = class MessageManager extends EventEmitter { */ getUnapprovedMsgs () { return this.messages.filter(msg => msg.status === 'unapproved') - .reduce((result, msg) => { result[msg.id] = msg; return result }, {}) + .reduce((result, msg) => { + result[msg.id] = msg; return result + }, {}) } /** @@ -83,7 +85,7 @@ module.exports = class MessageManager extends EventEmitter { case 'signed': return resolve(data.rawSig) case 'rejected': - return reject(rpcErrors.eth.userRejectedRequest('MetaMask Message Signature: User denied message signature.')) + return reject(ethErrors.provider.userRejectedRequest('MetaMask Message Signature: User denied message signature.')) default: return reject(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) } @@ -102,7 +104,9 @@ module.exports = class MessageManager extends EventEmitter { */ addUnapprovedMessage (msgParams, req) { // add origin from request - if (req) msgParams.origin = req.origin + if (req) { + msgParams.origin = req.origin + } msgParams.data = normalizeMsgData(msgParams.data) // create txData obj with parameters and meta data var time = (new Date()).getTime() @@ -219,7 +223,9 @@ module.exports = class MessageManager extends EventEmitter { */ _setMsgStatus (msgId, status) { const msg = this.getMsg(msgId) - if (!msg) throw new Error('MessageManager - Message not found for id: "${msgId}".') + if (!msg) { + throw new Error('MessageManager - Message not found for id: "${msgId}".') + } msg.status = status this._updateMsg(msg) this.emit(`${msgId}:${status}`, msg) diff --git a/app/scripts/lib/migrator/index.js b/app/scripts/lib/migrator/index.js index 345ca8001..c1c225fb3 100644 --- a/app/scripts/lib/migrator/index.js +++ b/app/scripts/lib/migrator/index.js @@ -40,8 +40,12 @@ class Migrator extends EventEmitter { try { // attempt migration and validate const migratedData = await migration.migrate(versionedData) - if (!migratedData.data) throw new Error('Migrator - migration returned empty data') - if (migratedData.version !== undefined && migratedData.meta.version !== migration.version) throw new Error('Migrator - Migration did not update version number correctly') + if (!migratedData.data) { + throw new Error('Migrator - migration returned empty data') + } + if (migratedData.version !== undefined && migratedData.meta.version !== migration.version) { + throw new Error('Migrator - Migration did not update version number correctly') + } // accept the migration as good versionedData = migratedData } catch (err) { diff --git a/app/scripts/lib/nodeify.js b/app/scripts/lib/nodeify.js index a813ae679..99b96b356 100644 --- a/app/scripts/lib/nodeify.js +++ b/app/scripts/lib/nodeify.js @@ -1,5 +1,9 @@ const promiseToCallback = require('promise-to-callback') -const callbackNoop = function (err) { if (err) throw err } +const callbackNoop = function (err) { + if (err) { + throw err + } +} /** * A generator that returns a function which, when passed a promise, can treat that promise as a node style callback. diff --git a/app/scripts/lib/notification-manager.js b/app/scripts/lib/notification-manager.js index 721d109a1..85177cceb 100644 --- a/app/scripts/lib/notification-manager.js +++ b/app/scripts/lib/notification-manager.js @@ -18,7 +18,9 @@ class NotificationManager { */ showPopup () { this._getPopup((err, popup) => { - if (err) throw err + if (err) { + throw err + } // Bring focus to chrome popup if (popup) { @@ -28,7 +30,9 @@ class NotificationManager { const {screenX, screenY, outerWidth, outerHeight} = window const notificationTop = Math.round(screenY + (outerHeight / 2) - (NOTIFICATION_HEIGHT / 2)) const notificationLeft = Math.round(screenX + (outerWidth / 2) - (NOTIFICATION_WIDTH / 2)) - const cb = (currentPopup) => { this._popupId = currentPopup.id } + const cb = (currentPopup) => { + this._popupId = currentPopup.id + } // create new notification popup const creation = extension.windows.create({ url: 'notification.html', @@ -50,8 +54,12 @@ class NotificationManager { closePopup () { // closes notification popup this._getPopup((err, popup) => { - if (err) throw err - if (!popup) return + if (err) { + throw err + } + if (!popup) { + return + } extension.windows.remove(popup.id, console.error) }) } @@ -66,7 +74,9 @@ class NotificationManager { */ _getPopup (cb) { this._getWindows((err, windows) => { - if (err) throw err + if (err) { + throw err + } cb(null, this._getPopupIn(windows)) }) } diff --git a/app/scripts/lib/pending-balance-calculator.js b/app/scripts/lib/pending-balance-calculator.js index 0f1dc19a9..dd7fa6de4 100644 --- a/app/scripts/lib/pending-balance-calculator.js +++ b/app/scripts/lib/pending-balance-calculator.js @@ -32,7 +32,9 @@ class PendingBalanceCalculator { ]) const [ balance, pending ] = results - if (!balance) return undefined + if (!balance) { + return undefined + } const pendingValue = pending.reduce((total, tx) => { return total.add(this.calculateMaxCost(tx)) diff --git a/app/scripts/lib/personal-message-manager.js b/app/scripts/lib/personal-message-manager.js index 2a2ab481a..013ecd4a1 100644 --- a/app/scripts/lib/personal-message-manager.js +++ b/app/scripts/lib/personal-message-manager.js @@ -1,7 +1,7 @@ const EventEmitter = require('events') const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') -const { errors: rpcErrors } = require('eth-json-rpc-errors') +const { ethErrors } = require('eth-json-rpc-errors') const createId = require('./random-id') const hexRe = /^[0-9A-Fa-f]+$/g const log = require('loglevel') @@ -65,7 +65,9 @@ module.exports = class PersonalMessageManager extends EventEmitter { */ getUnapprovedMsgs () { return this.messages.filter(msg => msg.status === 'unapproved') - .reduce((result, msg) => { result[msg.id] = msg; return result }, {}) + .reduce((result, msg) => { + result[msg.id] = msg; return result + }, {}) } /** @@ -89,7 +91,7 @@ module.exports = class PersonalMessageManager extends EventEmitter { case 'signed': return resolve(data.rawSig) case 'rejected': - return reject(rpcErrors.eth.userRejectedRequest('MetaMask Message Signature: User denied message signature.')) + return reject(ethErrors.provider.userRejectedRequest('MetaMask Message Signature: User denied message signature.')) default: return reject(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) } @@ -110,7 +112,9 @@ module.exports = class PersonalMessageManager extends EventEmitter { addUnapprovedMessage (msgParams, req) { log.debug(`PersonalMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`) // add origin from request - if (req) msgParams.origin = req.origin + if (req) { + msgParams.origin = req.origin + } msgParams.data = this.normalizeMsgData(msgParams.data) // create txData obj with parameters and meta data var time = (new Date()).getTime() @@ -229,7 +233,9 @@ module.exports = class PersonalMessageManager extends EventEmitter { */ _setMsgStatus (msgId, status) { const msg = this.getMsg(msgId) - if (!msg) throw new Error(`PersonalMessageManager - Message not found for id: "${msgId}".`) + if (!msg) { + throw new Error(`PersonalMessageManager - Message not found for id: "${msgId}".`) + } msg.status = status this._updateMsg(msg) this.emit(`${msgId}:${status}`, msg) diff --git a/app/scripts/lib/setupFetchDebugging.js b/app/scripts/lib/setupFetchDebugging.js index 431340e2b..d5c01b1f2 100644 --- a/app/scripts/lib/setupFetchDebugging.js +++ b/app/scripts/lib/setupFetchDebugging.js @@ -7,7 +7,9 @@ module.exports = setupFetchDebugging // function setupFetchDebugging () { - if (!global.fetch) return + if (!global.fetch) { + return + } const originalFetch = global.fetch global.fetch = wrappedFetch diff --git a/app/scripts/lib/setupMetamaskMeshMetrics.js b/app/scripts/lib/setupMetamaskMeshMetrics.js index fd3b93fc4..b520ceaa7 100644 --- a/app/scripts/lib/setupMetamaskMeshMetrics.js +++ b/app/scripts/lib/setupMetamaskMeshMetrics.js @@ -6,7 +6,26 @@ module.exports = setupMetamaskMeshMetrics */ function setupMetamaskMeshMetrics () { const testingContainer = document.createElement('iframe') - testingContainer.src = 'https://metamask.github.io/mesh-testing/' + const targetOrigin = 'https://metamask.github.io' + const targetUrl = `${targetOrigin}/mesh-testing/` + testingContainer.src = targetUrl + + let didLoad = false + testingContainer.addEventListener('load', () => { + didLoad = true + }) + console.log('Injecting MetaMask Mesh testing client') document.head.appendChild(testingContainer) + + return { submitMeshMetricsEntry } + + function submitMeshMetricsEntry (message) { + // ignore if we haven't loaded yet + if (!didLoad) { + return + } + // submit the message + testingContainer.contentWindow.postMessage(message, targetOrigin) + } } diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index 704371cb0..912cd53bf 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -67,11 +67,15 @@ function simplifyErrorMessages (report) { function rewriteErrorMessages (report, rewriteFn) { // rewrite top level message - if (typeof report.message === 'string') report.message = rewriteFn(report.message) + if (typeof report.message === 'string') { + report.message = rewriteFn(report.message) + } // rewrite each exception message if (report.exception && report.exception.values) { report.exception.values.forEach(item => { - if (typeof item.value === 'string') item.value = rewriteFn(item.value) + if (typeof item.value === 'string') { + item.value = rewriteFn(item.value) + } }) } } @@ -91,7 +95,9 @@ function rewriteReportUrls (report) { function toMetamaskUrl (origUrl) { const filePath = origUrl.split(location.origin)[1] - if (!filePath) return origUrl + if (!filePath) { + return origUrl + } const metamaskUrl = `metamask${filePath}` return metamaskUrl } diff --git a/app/scripts/lib/stream-utils.js b/app/scripts/lib/stream-utils.js index 3bab61946..e84ee516f 100644 --- a/app/scripts/lib/stream-utils.js +++ b/app/scripts/lib/stream-utils.js @@ -45,7 +45,9 @@ function setupMultiplex (connectionStream) { mux, connectionStream, (err) => { - if (err) console.error(err) + if (err) { + console.error(err) + } } ) return mux diff --git a/app/scripts/lib/typed-message-manager.js b/app/scripts/lib/typed-message-manager.js index e4d3e842b..a21b4aff2 100644 --- a/app/scripts/lib/typed-message-manager.js +++ b/app/scripts/lib/typed-message-manager.js @@ -2,7 +2,7 @@ const EventEmitter = require('events') const ObservableStore = require('obs-store') const createId = require('./random-id') const assert = require('assert') -const { errors: rpcErrors } = require('eth-json-rpc-errors') +const { ethErrors } = require('eth-json-rpc-errors') const sigUtil = require('eth-sig-util') const log = require('loglevel') const jsonschema = require('jsonschema') @@ -58,7 +58,9 @@ module.exports = class TypedMessageManager extends EventEmitter { */ getUnapprovedMsgs () { return this.messages.filter(msg => msg.status === 'unapproved') - .reduce((result, msg) => { result[msg.id] = msg; return result }, {}) + .reduce((result, msg) => { + result[msg.id] = msg; return result + }, {}) } /** @@ -79,7 +81,7 @@ module.exports = class TypedMessageManager extends EventEmitter { case 'signed': return resolve(data.rawSig) case 'rejected': - return reject(rpcErrors.eth.userRejectedRequest('MetaMask Message Signature: User denied message signature.')) + return reject(ethErrors.provider.userRejectedRequest('MetaMask Message Signature: User denied message signature.')) case 'errored': return reject(new Error(`MetaMask Message Signature: ${data.error}`)) default: @@ -103,7 +105,9 @@ module.exports = class TypedMessageManager extends EventEmitter { msgParams.version = version this.validateParams(msgParams) // add origin from request - if (req) msgParams.origin = req.origin + if (req) { + msgParams.origin = req.origin + } log.debug(`TypedMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`) // create txData obj with parameters and meta data @@ -149,7 +153,9 @@ module.exports = class TypedMessageManager extends EventEmitter { assert.ok('from' in params, 'Params must include a from field.') assert.equal(typeof params.from, 'string', 'From field must be a string.') assert.equal(typeof params.data, 'string', 'Data must be passed as a valid JSON string.') - assert.doesNotThrow(() => { data = JSON.parse(params.data) }, 'Data must be passed as a valid JSON string.') + assert.doesNotThrow(() => { + data = JSON.parse(params.data) + }, 'Data must be passed as a valid JSON string.') const validation = jsonschema.validate(data, sigUtil.TYPED_MESSAGE_SCHEMA) assert.ok(data.primaryType in data.types, `Primary type of "${data.primaryType}" has no type definition.`) assert.equal(validation.errors.length, 0, 'Data must conform to EIP-712 schema. See https://git.io/fNtcx.') @@ -278,7 +284,9 @@ module.exports = class TypedMessageManager extends EventEmitter { */ _setMsgStatus (msgId, status) { const msg = this.getMsg(msgId) - if (!msg) throw new Error('TypedMessageManager - Message not found for id: "${msgId}".') + if (!msg) { + throw new Error('TypedMessageManager - Message not found for id: "${msgId}".') + } msg.status = status this._updateMsg(msg) this.emit(`${msgId}:${status}`, msg) diff --git a/app/scripts/lib/util.js b/app/scripts/lib/util.js index 2eb71c0a0..114203d7f 100644 --- a/app/scripts/lib/util.js +++ b/app/scripts/lib/util.js @@ -38,7 +38,7 @@ const getEnvironmentType = (url = window.location.href) => { const parsedUrl = new URL(url) if (parsedUrl.pathname === '/popup.html') { return ENVIRONMENT_TYPE_POPUP - } else if (parsedUrl.pathname === '/home.html') { + } else if (['/home.html', '/phishing.html'].includes(parsedUrl.pathname)) { return ENVIRONMENT_TYPE_FULLSCREEN } else if (parsedUrl.pathname === '/notification.html') { return ENVIRONMENT_TYPE_NOTIFICATION @@ -144,6 +144,10 @@ function removeListeners (listeners, emitter) { }) } +function getRandomArrayItem (array) { + return array[Math.floor((Math.random() * array.length))] +} + module.exports = { removeListeners, applyListeners, @@ -154,4 +158,5 @@ module.exports = { hexToBn, bnToHex, BnMultiplyByFraction, + getRandomArrayItem, } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 32898c67f..93939fa8d 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -4,10 +4,12 @@ * @license MIT */ +const assert = require('assert').strict const EventEmitter = require('events') const pump = require('pump') const Dnode = require('dnode') const Capnode = require('capnode').default +const extension = require('extensionizer') const ObservableStore = require('obs-store') const ComposableObservableStore = require('./lib/ComposableObservableStore') const asStream = require('obs-store/lib/asStream') @@ -23,6 +25,7 @@ const providerAsMiddleware = require('eth-json-rpc-middleware/providerAsMiddlewa const MetamaskInpageProvider = require('metamask-inpage-provider') const {setupMultiplex, makeDuplexPair} = require('./lib/stream-utils.js') const KeyringController = require('eth-keyring-controller') +const EnsController = require('./controllers/ens') const NetworkController = require('./controllers/network') const PreferencesController = require('./controllers/preferences') const AppStateController = require('./controllers/app-state') @@ -38,6 +41,7 @@ const TypedMessageManager = require('./lib/typed-message-manager') const TransactionController = require('./controllers/transactions') const TokenRatesController = require('./controllers/token-rates') const DetectTokensController = require('./controllers/detect-tokens') +const ABTestController = require('./controllers/ab-test') const { PermissionsController } = require('./controllers/permissions') const PluginsController = require('./controllers/plugins') const AssetsController = require('./controllers/assets') @@ -55,10 +59,9 @@ const seedPhraseVerifier = require('./lib/seed-phrase-verifier') const log = require('loglevel') const TrezorKeyring = require('eth-trezor-keyring') const LedgerBridgeKeyring = require('eth-ledger-bridge-keyring') -const HW_WALLETS_KEYRINGS = [TrezorKeyring.type, LedgerBridgeKeyring.type] const EthQuery = require('eth-query') const ethUtil = require('ethereumjs-util') -const sigUtil = require('eth-sig-util') +const nanoid = require('nanoid') const contractMap = require('eth-contract-metadata') const { AddressBookController, @@ -94,6 +97,10 @@ module.exports = class MetamaskController extends EventEmitter { // observable state store this.store = new ComposableObservableStore(initState) + // external connections by origin + // Do not modify directly. Use the associated methods. + this.connections = {} + // lock to ensure only one vault created at once this.createVaultMutex = new Mutex() @@ -142,6 +149,11 @@ module.exports = class MetamaskController extends EventEmitter { networkController: this.networkController, }) + this.ensController = new EnsController({ + provider: this.provider, + networkStore: this.networkController.networkStore, + }) + this.incomingTransactionsController = new IncomingTransactionsController({ blockTracker: this.blockTracker, networkController: this.networkController, @@ -175,6 +187,7 @@ module.exports = class MetamaskController extends EventEmitter { this.onboardingController = new OnboardingController({ initState: initState.OnboardingController, + preferencesController: this.preferencesController, }) // ensure accountTracker updates balances after network change @@ -182,7 +195,6 @@ module.exports = class MetamaskController extends EventEmitter { this.accountTracker._updateAccounts() }) - // key mgmt const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring] this.keyringController = new KeyringController({ keyringTypes: additionalKeyrings, @@ -190,16 +202,18 @@ module.exports = class MetamaskController extends EventEmitter { getNetwork: this.networkController.getNetworkState.bind(this.networkController), encryptor: opts.encryptor || undefined, }) - this.keyringController.memStore.subscribe((s) => this._onKeyringControllerUpdate(s)) - // detect tokens controller this.detectTokensController = new DetectTokensController({ preferences: this.preferencesController, network: this.networkController, keyringMemStore: this.keyringController.memStore, }) + this.abTestController = new ABTestController({ + initState: initState.ABTestController, + }) + this.addressBookController = new AddressBookController(undefined, initState.AddressBookController) this.threeBoxController = new ThreeBoxController({ @@ -208,13 +222,24 @@ module.exports = class MetamaskController extends EventEmitter { keyringController: this.keyringController, initState: initState.ThreeBoxController, getKeyringControllerState: this.keyringController.memStore.getState.bind(this.keyringController.memStore), - getSelectedAddress: this.preferencesController.getSelectedAddress.bind(this.preferencesController), version, }) - // tx mgmt + this.permissionsController = new PermissionsController({ + setupProvider: this.setupProvider.bind(this), + keyringController: this.keyringController, + assetsController: this.assetsController, + openPopup: opts.openPopup, + closePopup: opts.closePopup, + provider: this.provider, + getApi: this.getPluginsApi.bind(this), + notifyDomain: this.notifyConnections.bind(this), + notifyAllDomains: this.notifyAllConnections.bind(this), + }, initState.PermissionsController, initState.PermissionsMetadata) + this.txController = new TransactionController({ initState: initState.TransactionController || initState.TransactionManager, + getPermittedAccounts: this.permissionsController.getAccounts.bind(this.permissionsController), networkStore: this.networkController.networkStore, preferencesStore: this.preferencesController.store, txHistoryLimit: 40, @@ -274,17 +299,6 @@ module.exports = class MetamaskController extends EventEmitter { // For now handled by plugin persistence. }) - this.permissionsController = new PermissionsController({ - setupProvider: this.setupProvider.bind(this), - keyringController: this.keyringController, - assetsController: this.assetsController, - openPopup: opts.openPopup, - closePopup: opts.closePopup, - provider: this.provider, - getApi: this.getPluginsApi.bind(this), - }, - initState.PermissionsMetadata) - this.pluginsController = new PluginsController({ setupProvider: this.setupProvider.bind(this), _txController: this.txController, @@ -292,7 +306,6 @@ module.exports = class MetamaskController extends EventEmitter { _blockTracker: this.blockTracker, _getAccounts: this.keyringController.getAccounts.bind(this.keyringController), _removeAllPermissionsFor: this.permissionsController.removeAllPermissionsFor.bind(this.permissionsController), - onUnlock: this._onUnlock.bind(this), getApi: this.getPluginsApi.bind(this), initState: initState.PluginsController, getAppKeyForDomain: this.getAppKeyForDomain.bind(this), @@ -324,6 +337,7 @@ module.exports = class MetamaskController extends EventEmitter { CachedBalancesController: this.cachedBalancesController.store, OnboardingController: this.onboardingController.store, IncomingTransactionsController: this.incomingTransactionsController.store, + ABTestController: this.abTestController.store, // TODO:permissions permissionsRequests should be memStore only PermissionsController: this.permissionsController.permissions, PermissionsMetadata: this.permissionsController.store, @@ -357,6 +371,9 @@ module.exports = class MetamaskController extends EventEmitter { AssetsController: this.assetsController.store, AddressAuditController: this.addressAuditController.store, ThreeBoxController: this.threeBoxController.store, + ABTestController: this.abTestController.store, + // ENS Controller + EnsController: this.ensController.store, }) this.memStore.subscribe(this.sendUpdate.bind(this)) } @@ -373,8 +390,7 @@ module.exports = class MetamaskController extends EventEmitter { version, // account mgmt getAccounts: async ({ origin }) => { - // TODO: is this safe? - if (origin === 'MetaMask') { + if (origin === 'metamask') { return [this.preferencesController.getSelectedAddress()] } else if ( this.keyringController.memStore.getState().isUnlocked @@ -392,6 +408,7 @@ module.exports = class MetamaskController extends EventEmitter { processTypedMessageV4: this.newUnsignedTypedMessage.bind(this), processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), getPendingNonce: this.getPendingNonce.bind(this), + getPendingTransactionByHash: (hash) => this.txController.getFilteredTxList({ hash, status: 'submitted' })[0], } const providerProxy = this.networkController.initializeProvider(providerOpts) return providerProxy @@ -419,14 +436,12 @@ module.exports = class MetamaskController extends EventEmitter { publicConfigStore.putState(selectPublicState(memState)) } - function selectPublicState ({ isUnlocked, network, completedOnboarding, provider }) { - const result = { + function selectPublicState ({ isUnlocked, network, provider }) { + return { isUnlocked, networkVersion: network, - onboardingcomplete: completedOnboarding, chainId: selectChainId({ network, provider }), } - return result } } @@ -459,17 +474,20 @@ module.exports = class MetamaskController extends EventEmitter { */ getApi () { const keyringController = this.keyringController - const preferencesController = this.preferencesController - const txController = this.txController const networkController = this.networkController const onboardingController = this.onboardingController + const permissionsController = this.permissionsController + const preferencesController = this.preferencesController const threeBoxController = this.threeBoxController + const abTestController = this.abTestController + const txController = this.txController return { // etc getState: (cb) => cb(null, this.getState()), setCurrentCurrency: this.setCurrentCurrency.bind(this), setUseBlockie: this.setUseBlockie.bind(this), + setUseNonceField: this.setUseNonceField.bind(this), setParticipateInMetaMetrics: this.setParticipateInMetaMetrics.bind(this), setMetaMetricsSendCount: this.setMetaMetricsSendCount.bind(this), setFirstTimeFlowType: this.setFirstTimeFlowType.bind(this), @@ -533,6 +551,10 @@ module.exports = class MetamaskController extends EventEmitter { // AppStateController setLastActiveTime: nodeify(this.appStateController.setLastActiveTime, this.appStateController), + setMkrMigrationReminderTimestamp: nodeify(this.appStateController.setMkrMigrationReminderTimestamp, this.appStateController), + + // EnsController + tryReverseResolveAddress: nodeify(this.ensController.reverseResolveAddress, this.ensController), // KeyringController setLocked: nodeify(this.setLocked, this), @@ -551,6 +573,8 @@ module.exports = class MetamaskController extends EventEmitter { getFilteredTxList: nodeify(txController.getFilteredTxList, txController), isNonceTaken: nodeify(txController.isNonceTaken, txController), estimateGas: nodeify(this.estimateGas, this), + getPendingNonce: nodeify(this.getPendingNonce, this), + getNextNonce: nodeify(this.getNextNonce, this), // messageManager signMessage: nodeify(this.signMessage, this), @@ -570,19 +594,26 @@ module.exports = class MetamaskController extends EventEmitter { // 3Box setThreeBoxSyncingPermission: nodeify(threeBoxController.setThreeBoxSyncingPermission, threeBoxController), restoreFromThreeBox: nodeify(threeBoxController.restoreFromThreeBox, threeBoxController), - setRestoredFromThreeBoxToFalse: nodeify(threeBoxController.setRestoredFromThreeBoxToFalse, threeBoxController), + setShowRestorePromptToFalse: nodeify(threeBoxController.setShowRestorePromptToFalse, threeBoxController), getThreeBoxLastUpdated: nodeify(threeBoxController.getLastUpdated, threeBoxController), turnThreeBoxSyncingOn: nodeify(threeBoxController.turnThreeBoxSyncingOn, threeBoxController), initializeThreeBox: nodeify(this.initializeThreeBox, this), + // a/b test controller + getAssignedABTestGroupName: nodeify(abTestController.getAssignedABTestGroupName, abTestController), + // permissions - approvePermissionsRequest: nodeify(this.permissionsController.approvePermissionsRequest, this.permissionsController), - clearPermissions: this.permissionsController.clearPermissions.bind(this.permissionsController), - clearPermissionsHistory: this.permissionsController.clearHistory.bind(this.permissionsController), - clearPermissionsLog: this.permissionsController.clearLog.bind(this.permissionsController), - getApprovedAccounts: nodeify(this.permissionsController.getAccounts.bind(this.permissionsController)), - rejectPermissionsRequest: nodeify(this.permissionsController.rejectPermissionsRequest, this.permissionsController), - removePermissionsFor: this.permissionsController.removePermissionsFor.bind(this.permissionsController), + approvePermissionsRequest: nodeify(permissionsController.approvePermissionsRequest, permissionsController), + clearPermissions: permissionsController.clearPermissions.bind(permissionsController), + clearPermissionsHistory: permissionsController.clearHistory.bind(permissionsController), + clearPermissionsLog: permissionsController.clearLog.bind(permissionsController), + getApprovedAccounts: nodeify(permissionsController.getAccounts.bind(permissionsController)), + rejectPermissionsRequest: nodeify(permissionsController.rejectPermissionsRequest, permissionsController), + removePermissionsFor: permissionsController.removePermissionsFor.bind(permissionsController), + getCaveatsFor: permissionsController.getCaveatsFor.bind(permissionsController), + getCaveat: permissionsController.getCaveat.bind(permissionsController), + updateExposedAccounts: nodeify(permissionsController.updateExposedAccounts, permissionsController), + legacyExposeAccounts: nodeify(permissionsController.legacyExposeAccounts, permissionsController), // plugins removePlugin: this.pluginsController.removePlugin.bind(this.pluginsController), @@ -703,7 +734,6 @@ module.exports = class MetamaskController extends EventEmitter { } } - //============================================================================= // VAULT / KEYRING RELATED METHODS //============================================================================= @@ -902,16 +932,17 @@ module.exports = class MetamaskController extends EventEmitter { await this.preferencesController.syncAddresses(accounts) await this.txController.pendingTxTracker.updatePendingTxs() - const threeBoxFeatureFlagTurnedOn = this.preferencesController.getFeatureFlags().threeBox - - if (threeBoxFeatureFlagTurnedOn) { + try { const threeBoxSyncingAllowed = this.threeBoxController.getThreeBoxSyncingState() if (threeBoxSyncingAllowed && !this.threeBoxController.box) { - await this.threeBoxController.new3Box() + // 'await' intentionally omitted to avoid waiting for initialization + this.threeBoxController.init() this.threeBoxController.turnThreeBoxSyncingOn() } else if (threeBoxSyncingAllowed && this.threeBoxController.box) { this.threeBoxController.turnThreeBoxSyncingOn() } + } catch (error) { + log.error(error) } return this.keyringController.fullUpdate() @@ -1315,28 +1346,16 @@ module.exports = class MetamaskController extends EventEmitter { const version = msgParams.version try { const cleanMsgParams = await this.typedMessageManager.approveMessage(msgParams) - const address = sigUtil.normalize(cleanMsgParams.from) - const keyring = await this.keyringController.getKeyringForAccount(address) - let signature - // HW Wallet keyrings don't expose private keys - // so we need to handle it separately - if (!HW_WALLETS_KEYRINGS.includes(keyring.type)) { - const wallet = keyring._getWalletForAccount(address) - const privKey = ethUtil.toBuffer(wallet.getPrivateKey()) - switch (version) { - case 'V1': - signature = sigUtil.signTypedDataLegacy(privKey, { data: cleanMsgParams.data }) - break - case 'V3': - signature = sigUtil.signTypedData(privKey, { data: JSON.parse(cleanMsgParams.data) }) - break - case 'V4': - signature = sigUtil.signTypedData_v4(privKey, { data: JSON.parse(cleanMsgParams.data) }) - break + + // For some reason every version after V1 used stringified params. + if (version !== 'V1') { + // But we don't have to require that. We can stop suggesting it now: + if (typeof cleanMsgParams.data === 'string') { + cleanMsgParams.data = JSON.parse(cleanMsgParams.data) } - } else { - signature = await keyring.signTypedData(address, cleanMsgParams.data) } + + const signature = await this.keyringController.signTypedMessage(cleanMsgParams, { version }) this.typedMessageManager.setMsgStatusSigned(msgId, signature) return this.getState() } catch (error) { @@ -1487,14 +1506,16 @@ module.exports = class MetamaskController extends EventEmitter { * Used to create a multiplexed stream for connecting to an untrusted context * like a Dapp or other extension. * @param {*} connectionStream - The Duplex stream to connect to. - * @param {string} originDomain - The domain requesting the stream, which - * may trigger a blacklist reload. + * @param {URL} senderUrl - The URL of the resource requesting the stream, + * which may trigger a blacklist reload. + * @param {string} extensionId - The extension id of the sender, if the sender + * is an extension */ - setupUntrustedCommunication (connectionStream, originDomain, getSiteMetadata, isPlugin = false) { + setupUntrustedCommunication (connectionStream, senderUrl, extensionId, isPlugin = false) { // Check if new connection is blacklisted - if (this.phishingController.test(originDomain)) { - log.debug('MetaMask - sending phishing warning for', originDomain) - this.sendPhishingWarning(connectionStream, originDomain) + if (this.phishingController.test(senderUrl.hostname)) { + log.debug('MetaMask - sending phishing warning for', senderUrl.hostname) + this.sendPhishingWarning(connectionStream, senderUrl.hostname) return } @@ -1502,8 +1523,8 @@ module.exports = class MetamaskController extends EventEmitter { const mux = setupMultiplex(connectionStream) // messages between inpage and background - this.setupProviderConnection(mux.createStream('provider'), originDomain, getSiteMetadata, isPlugin) - this.setupCapnodeConnection(mux.createStream('cap'), originDomain) + this.setupProviderConnection(mux.createStream('provider'), senderUrl, extensionId, isPlugin) + this.setupCapnodeConnection(mux.createStream('cap'), senderUrl) this.setupPublicConfig(mux.createStream('publicConfig')) } @@ -1514,16 +1535,16 @@ module.exports = class MetamaskController extends EventEmitter { * functions, like the ability to approve transactions or sign messages. * * @param {*} connectionStream - The duplex stream to connect to. - * @param {string} originDomain - The domain requesting the connection, + * @param {URL} senderUrl - The URL requesting the connection, * used in logging and error reporting. */ - setupTrustedCommunication (connectionStream, originDomain) { + setupTrustedCommunication (connectionStream, senderUrl) { // setup multiplexing const mux = setupMultiplex(connectionStream) // connect features this.setupControllerConnection(mux.createStream('controller')) - this.setupProviderConnection(mux.createStream('provider'), originDomain) - this.setupCapnodeConnection(mux.createStream('cap'), originDomain) + this.setupProviderConnection(mux.createStream('provider'), senderUrl) + this.setupCapnodeConnection(mux.createStream('cap'), senderUrl) } /** @@ -1561,7 +1582,9 @@ module.exports = class MetamaskController extends EventEmitter { this.activeControllerConnections-- this.emit('controllerConnectionChanged', this.activeControllerConnections) // report any error - if (err) log.error(err) + if (err) { + log.error(err) + } } ) dnode.on('remote', (remote) => { @@ -1576,26 +1599,35 @@ module.exports = class MetamaskController extends EventEmitter { /** * A method for serving our ethereum provider over a given stream. * @param {*} outStream - The stream to provide over. - * @param {string} origin - The URI of the requesting resource. + * @param {URL} senderUrl - The URI of the requesting resource. + * @param {string} extensionId - The id of the extension, if the requesting + * resource is an extension. + * @param {object} publicApi - The public API */ - setupProviderConnection (outStream, origin, getSiteMetadata, isPlugin = false) { - const engine = this.setupProviderEngine(origin, getSiteMetadata, isPlugin) + setupProviderConnection (outStream, senderUrl, extensionId, isPlugin) { + const origin = senderUrl.hostname + const engine = this.setupProviderEngine(senderUrl, extensionId, isPlugin) // setup connection const providerStream = createEngineStream({ engine }) + const connectionId = this.addConnection(origin, { engine }) + pump( outStream, providerStream, outStream, (err) => { - // cleanup filter polyfill middleware + // handle any middleware cleanup engine._middleware.forEach((mid) => { if (mid.destroy && typeof mid.destroy === 'function') { mid.destroy() } }) - if (err) log.error(err) + connectionId && this.removeConnection(origin, connectionId) + if (err) { + log.error(err) + } } ) } @@ -1619,14 +1651,16 @@ module.exports = class MetamaskController extends EventEmitter { outStream, (err) => { // TODO: Any capServer deallocation steps. - if (err) log.error(err) + if (err) { + log.error(err) + } } ) } - setupProvider (origin, getSiteMetadata, isPlugin) { + setupProvider (senderUrl, getSiteMetadata, isPlugin) { const { clientSide, serverSide } = makeDuplexPair() - this.setupUntrustedCommunication(serverSide, origin, getSiteMetadata, isPlugin) + this.setupUntrustedCommunication(serverSide, senderUrl, getSiteMetadata, isPlugin) const provider = new MetamaskInpageProvider(clientSide) return provider } @@ -1634,7 +1668,9 @@ module.exports = class MetamaskController extends EventEmitter { /** * A method for creating a provider that is safely restricted for the requesting domain. **/ - setupProviderEngine (origin, _, isPlugin) { + setupProviderEngine (senderUrl, extensionId, isPlugin) { + + const origin = senderUrl.hostname // setup json rpc engine stack const engine = new RpcEngine() const provider = this.provider @@ -1655,7 +1691,7 @@ module.exports = class MetamaskController extends EventEmitter { engine.push(filterMiddleware) engine.push(subscriptionManager.middleware) // permissions - engine.push(this.permissionsController.createMiddleware({ origin, isPlugin })) + engine.push(this.permissionsController.createMiddleware({ origin, extensionId, isPlugin })) // watch asset engine.push(this.preferencesController.requestWatchAsset.bind(this.preferencesController)) @@ -1685,11 +1721,145 @@ module.exports = class MetamaskController extends EventEmitter { (err) => { configStore.destroy() configStream.destroy() - if (err) log.error(err) + if (err) { + log.error(err) + } } ) } + // manage external connections + + onMessage (message, sender, sendResponse) { + if (!message || !message.type) { + log.debug(`Ignoring invalid message: '${JSON.stringify(message)}'`) + return + } + + let handleMessage + + try { + if (message.type === 'metamask:registerOnboarding') { + assert(sender.tab, 'Missing tab from sender') + assert(sender.tab.id && sender.tab.id !== extension.tabs.TAB_ID_NONE, 'Missing tab ID from sender') + assert(message.location, 'Missing location from message') + + handleMessage = this.onboardingController.registerOnboarding(message.location, sender.tab.id) + } else { + throw new Error(`Unrecognized message type: '${message.type}'`) + } + } catch (error) { + console.error(error) + sendResponse(error) + return true + } + + if (handleMessage) { + handleMessage + .then(() => { + sendResponse(null, true) + }) + .catch((error) => { + console.error(error) + sendResponse(error) + }) + return true + } + } + + /** + * Adds a reference to a connection by origin. Ignores the 'metamask' origin. + * Caller must ensure that the returned id is stored such that the reference + * can be deleted later. + * + * @param {string} origin - The connection's origin string. + * @param {Object} options - Data associated with the connection + * @param {Object} options.engine - The connection's JSON Rpc Engine + * @returns {string} - The connection's id (so that it can be deleted later) + */ + addConnection (origin, { engine }) { + + if (origin === 'metamask') { + return null + } + + if (!this.connections[origin]) { + this.connections[origin] = {} + } + + const id = nanoid() + this.connections[origin][id] = { + engine, + } + + return id + } + + /** + * Deletes a reference to a connection, by origin and id. + * Ignores unknown origins. + * + * @param {string} origin - The connection's origin string. + * @param {string} id - The connection's id, as returned from addConnection. + */ + removeConnection (origin, id) { + + const connections = this.connections[origin] + if (!connections) { + return + } + + delete connections[id] + + if (Object.keys(connections.length === 0)) { + delete this.connections[origin] + } + } + + /** + * Causes the RPC engines associated with the connections to the given origin + * to emit a notification event with the given payload. + * Does nothing if the extension is locked or the origin is unknown. + * + * @param {string} origin - The connection's origin string. + * @param {any} payload - The event payload. + */ + notifyConnections (origin, payload) { + + const { isUnlocked } = this.getState() + const connections = this.connections[origin] + if (!isUnlocked || !connections) { + return + } + + Object.values(connections).forEach(conn => { + conn.engine && conn.engine.emit('notification', payload) + }) + } + + /** + * Causes the RPC engines associated with all connections to emit a + * notification event with the given payload. + * Does nothing if the extension is locked. + * + * @param {any} payload - The event payload. + */ + notifyAllConnections (payload) { + + const { isUnlocked } = this.getState() + if (!isUnlocked) { + return + } + + Object.values(this.connections).forEach(origin => { + Object.values(origin).forEach(conn => { + conn.engine && conn.engine.emit('notification', payload) + }) + }) + } + + // handlers + /** * Handle a KeyringController update * @param {object} state the KC state @@ -1718,14 +1888,7 @@ module.exports = class MetamaskController extends EventEmitter { } } - _onUnlock (cb) { - this.keyringController.memStore.subscribe(state => { - const { isUnlocked } = state - if (isUnlocked) { - return cb() - } - }) - } + // misc /** * A method for emitting the full MetaMask state to all registered listeners. @@ -1735,6 +1898,10 @@ module.exports = class MetamaskController extends EventEmitter { this.emit('update', this.getState()) } + //============================================================================= + // MISCELLANEOUS + //============================================================================= + /** * A method for estimating a good gas price at recent prices. * Returns the lowest price that would have been included in @@ -1775,13 +1942,28 @@ module.exports = class MetamaskController extends EventEmitter { * @returns Promise */ async getPendingNonce (address) { - const { nonceDetails, releaseLock} = await this.txController.nonceTracker.getNonceLock(address) + const { nonceDetails, releaseLock } = await this.txController.nonceTracker.getNonceLock(address) const pendingNonce = nonceDetails.params.highestSuggested releaseLock() return pendingNonce } + /** + * Returns the next nonce according to the nonce-tracker + * @param address {string} - The hex string address for the transaction + * @returns Promise + */ + async getNextNonce (address) { + let nonceLock + try { + nonceLock = await this.txController.nonceTracker.getNonceLock(address) + } finally { + nonceLock.releaseLock() + } + return nonceLock.nextNonce + } + //============================================================================= // CONFIG //============================================================================= @@ -1816,10 +1998,14 @@ module.exports = class MetamaskController extends EventEmitter { * @param {string} amount - The amount of ether desired, as a base 10 string. */ buyEth (address, amount) { - if (!amount) amount = '5' + if (!amount) { + amount = '5' + } const network = this.networkController.getNetworkState() const url = getBuyEthUrl({ network, address, amount }) - if (url) this.platform.openWindow({ url }) + if (url) { + this.platform.openWindow({ url }) + } } /** @@ -1878,7 +2064,7 @@ module.exports = class MetamaskController extends EventEmitter { } async initializeThreeBox () { - await this.threeBoxController.new3Box() + await this.threeBoxController.init() } /** @@ -1895,6 +2081,20 @@ module.exports = class MetamaskController extends EventEmitter { } } + /** + * Sets whether or not to use the nonce field. + * @param {boolean} val - True for nonce field, false for not nonce field. + * @param {Function} cb - A callback function called when complete. + */ + setUseNonceField (val, cb) { + try { + this.preferencesController.setUseNonceField(val) + cb(null) + } catch (err) { + cb(err) + } + } + /** * Sets whether or not the user will have usage data tracked with MetaMetrics * @param {boolean} bool - True for users that wish to opt-in, false for users that wish to remain out. diff --git a/app/scripts/migrations/004.js b/app/scripts/migrations/004.js index cd558300c..c0cf600c9 100644 --- a/app/scripts/migrations/004.js +++ b/app/scripts/migrations/004.js @@ -9,7 +9,9 @@ module.exports = { const safeVersionedData = clone(versionedData) safeVersionedData.meta.version = version try { - if (safeVersionedData.data.config.provider.type !== 'rpc') return Promise.resolve(safeVersionedData) + if (safeVersionedData.data.config.provider.type !== 'rpc') { + return Promise.resolve(safeVersionedData) + } switch (safeVersionedData.data.config.provider.rpcTarget) { case 'https://testrpc.metamask.io/': safeVersionedData.data.config.provider = { diff --git a/app/scripts/migrations/015.js b/app/scripts/migrations/015.js index 5e2f9e63b..3d20b58db 100644 --- a/app/scripts/migrations/015.js +++ b/app/scripts/migrations/015.js @@ -32,8 +32,11 @@ function transformState (state) { if (TransactionController && TransactionController.transactions) { const transactions = TransactionController.transactions newState.TransactionController.transactions = transactions.map((txMeta) => { - if (!txMeta.err) return txMeta - else if (txMeta.err.message === 'Gave up submitting tx.') txMeta.status = 'failed' + if (!txMeta.err) { + return txMeta + } else if (txMeta.err.message === 'Gave up submitting tx.') { + txMeta.status = 'failed' + } return txMeta }) } diff --git a/app/scripts/migrations/016.js b/app/scripts/migrations/016.js index 048c7a40e..76a106ca7 100644 --- a/app/scripts/migrations/016.js +++ b/app/scripts/migrations/016.js @@ -33,7 +33,9 @@ function transformState (state) { const transactions = newState.TransactionController.transactions newState.TransactionController.transactions = transactions.map((txMeta) => { - if (!txMeta.err) return txMeta + if (!txMeta.err) { + return txMeta + } if (txMeta.err === 'transaction with the same hash was already imported.') { txMeta.status = 'submitted' delete txMeta.err diff --git a/app/scripts/migrations/017.js b/app/scripts/migrations/017.js index 5f6d906d6..918df4ade 100644 --- a/app/scripts/migrations/017.js +++ b/app/scripts/migrations/017.js @@ -31,7 +31,9 @@ function transformState (state) { if (TransactionController && TransactionController.transactions) { const transactions = newState.TransactionController.transactions newState.TransactionController.transactions = transactions.map((txMeta) => { - if (!txMeta.status === 'failed') return txMeta + if (!txMeta.status === 'failed') { + return txMeta + } if (txMeta.retryCount > 0 && txMeta.retryCount < 2) { txMeta.status = 'submitted' delete txMeta.err diff --git a/app/scripts/migrations/019.js b/app/scripts/migrations/019.js index 7b726c3e8..f560f5579 100644 --- a/app/scripts/migrations/019.js +++ b/app/scripts/migrations/019.js @@ -35,7 +35,9 @@ function transformState (state) { const transactions = newState.TransactionController.transactions newState.TransactionController.transactions = transactions.map((txMeta, _, txList) => { - if (txMeta.status !== 'submitted') return txMeta + if (txMeta.status !== 'submitted') { + return txMeta + } const confirmedTxs = txList.filter((tx) => tx.status === 'confirmed') .filter((tx) => tx.txParams.from === txMeta.txParams.from) diff --git a/app/scripts/migrations/022.js b/app/scripts/migrations/022.js index 1fbe241e6..eeb142b7a 100644 --- a/app/scripts/migrations/022.js +++ b/app/scripts/migrations/022.js @@ -33,7 +33,9 @@ function transformState (state) { const transactions = newState.TransactionController.transactions newState.TransactionController.transactions = transactions.map((txMeta) => { - if (txMeta.status !== 'submitted' || txMeta.submittedTime) return txMeta + if (txMeta.status !== 'submitted' || txMeta.submittedTime) { + return txMeta + } txMeta.submittedTime = (new Date()).getTime() return txMeta }) diff --git a/app/scripts/migrations/023.js b/app/scripts/migrations/023.js index 18493a789..84532537e 100644 --- a/app/scripts/migrations/023.js +++ b/app/scripts/migrations/023.js @@ -33,7 +33,9 @@ function transformState (state) { if (TransactionController && TransactionController.transactions) { const transactions = newState.TransactionController.transactions - if (transactions.length <= 40) return newState + if (transactions.length <= 40) { + return newState + } const reverseTxList = transactions.reverse() let stripping = true @@ -44,8 +46,11 @@ function transformState (state) { txMeta.status === 'confirmed' || txMeta.status === 'dropped') }) - if (txIndex < 0) stripping = false - else reverseTxList.splice(txIndex, 1) + if (txIndex < 0) { + stripping = false + } else { + reverseTxList.splice(txIndex, 1) + } } newState.TransactionController.transactions = reverseTxList.reverse() diff --git a/app/scripts/migrations/024.js b/app/scripts/migrations/024.js index 5ffaea377..fda463e1a 100644 --- a/app/scripts/migrations/024.js +++ b/app/scripts/migrations/024.js @@ -25,7 +25,9 @@ module.exports = { function transformState (state) { const newState = state - if (!newState.TransactionController) return newState + if (!newState.TransactionController) { + return newState + } const transactions = newState.TransactionController.transactions newState.TransactionController.transactions = transactions.map((txMeta, _) => { if ( diff --git a/app/scripts/migrations/025.js b/app/scripts/migrations/025.js index fd4faa782..48bff962d 100644 --- a/app/scripts/migrations/025.js +++ b/app/scripts/migrations/025.js @@ -29,7 +29,9 @@ function transformState (state) { if (newState.TransactionController.transactions) { const transactions = newState.TransactionController.transactions newState.TransactionController.transactions = transactions.map((txMeta) => { - if (txMeta.status !== 'unapproved') return txMeta + if (txMeta.status !== 'unapproved') { + return txMeta + } txMeta.txParams = normalizeTxParams(txMeta.txParams) return txMeta }) @@ -54,7 +56,9 @@ function normalizeTxParams (txParams) { // apply only keys in the whiteList const normalizedTxParams = {} Object.keys(whiteList).forEach((key) => { - if (txParams[key]) normalizedTxParams[key] = whiteList[key](txParams[key]) + if (txParams[key]) { + normalizedTxParams[key] = whiteList[key](txParams[key]) + } }) return normalizedTxParams diff --git a/app/scripts/migrations/029.js b/app/scripts/migrations/029.js index e17479ccc..59724c154 100644 --- a/app/scripts/migrations/029.js +++ b/app/scripts/migrations/029.js @@ -24,4 +24,3 @@ module.exports = { return isApproved && now - createdTime > unacceptableDelay }), } - diff --git a/app/scripts/migrations/031.js b/app/scripts/migrations/031.js index 9c8cbeb09..927de98c4 100644 --- a/app/scripts/migrations/031.js +++ b/app/scripts/migrations/031.js @@ -3,7 +3,7 @@ const version = 31 const clone = require('clone') /* - * The purpose of this migration is to properly set the completedOnboarding flag baesd on the state + * The purpose of this migration is to properly set the completedOnboarding flag based on the state * of the KeyringController. */ module.exports = { diff --git a/app/scripts/migrations/037.js b/app/scripts/migrations/037.js new file mode 100644 index 000000000..9eeda1882 --- /dev/null +++ b/app/scripts/migrations/037.js @@ -0,0 +1,56 @@ +const version = 37 +const clone = require('clone') +const { + util, +} = require('gaba') + +/** + * The purpose of this migration is to update the address book state + * to the new schema with chainId as a key. + * and to add the isEns flag to all entries + */ +module.exports = { + version, + migrate: async function (originalVersionedData) { + const versionedData = clone(originalVersionedData) + versionedData.meta.version = version + const state = versionedData.data + versionedData.data = transformState(state) + return versionedData + }, +} + +function transformState (state) { + + if (state.AddressBookController) { + const ab = state.AddressBookController.addressBook + + const chainIds = new Set() + const newAddressBook = {} + + // add all of the chainIds to a set + for (const item in ab) { + chainIds.add(ab[item].chainId) + } + + // fill the chainId object with the entries with the matching chainId + for (const id of chainIds.values()) { + // make an empty object entry for each chainId + newAddressBook[id] = {} + for (const address in ab) { + if (ab[address].chainId === id) { + + ab[address].isEns = false + if (util.normalizeEnsName(ab[address].name)) { + ab[address].isEns = true + } + newAddressBook[id][address] = ab[address] + } + } + } + + state.AddressBookController.addressBook = newAddressBook + } + + return state +} diff --git a/app/scripts/migrations/038.js b/app/scripts/migrations/038.js new file mode 100644 index 000000000..25d8b79ac --- /dev/null +++ b/app/scripts/migrations/038.js @@ -0,0 +1,37 @@ +const version = 38 +const clone = require('clone') +const ABTestController = require('../controllers/ab-test') +const { getRandomArrayItem } = require('../lib/util') + +/** + * The purpose of this migration is to assign all users to a test group for the fullScreenVsPopup a/b test + */ +module.exports = { + version, + migrate: async function (originalVersionedData) { + const versionedData = clone(originalVersionedData) + versionedData.meta.version = version + const state = versionedData.data + versionedData.data = transformState(state) + return versionedData + }, +} + +function transformState (state) { + const { ABTestController: ABTestControllerState = {} } = state + const { abTests = {} } = ABTestControllerState + + if (!abTests.fullScreenVsPopup) { + state = { + ...state, + ABTestController: { + ...ABTestControllerState, + abTests: { + ...abTests, + fullScreenVsPopup: getRandomArrayItem(ABTestController.abTestGroupNames.fullScreenVsPopup), + }, + }, + } + } + return state +} diff --git a/app/scripts/migrations/039.js b/app/scripts/migrations/039.js new file mode 100644 index 000000000..60c013c58 --- /dev/null +++ b/app/scripts/migrations/039.js @@ -0,0 +1,67 @@ +const version = 39 +const clone = require('clone') +const ethUtil = require('ethereumjs-util') + +const DAI_V1_CONTRACT_ADDRESS = '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359' +const DAI_V1_TOKEN_SYMBOL = 'DAI' +const SAI_TOKEN_SYMBOL = 'SAI' + +function isOldDai (token = {}) { + return token && typeof token === 'object' && + token.symbol === DAI_V1_TOKEN_SYMBOL && + ethUtil.toChecksumAddress(token.address) === DAI_V1_CONTRACT_ADDRESS +} + +/** + * This migration renames the Dai token to Sai. + * + * As of 2019-11-18 Dai is now called Sai (refs https://git.io/JeooP) to facilitate + * Maker's upgrade to Multi-Collateral Dai and this migration renames the token + * at the old address. + */ +module.exports = { + version, + migrate: async function (originalVersionedData) { + const versionedData = clone(originalVersionedData) + versionedData.meta.version = version + const state = versionedData.data + versionedData.data = transformState(state) + return versionedData + }, +} + +function transformState (state) { + const { PreferencesController } = state + + if (PreferencesController) { + const tokens = PreferencesController.tokens || [] + if (Array.isArray(tokens)) { + for (const token of tokens) { + if (isOldDai(token)) { + token.symbol = SAI_TOKEN_SYMBOL + } + } + } + + const accountTokens = PreferencesController.accountTokens || {} + if (accountTokens && typeof accountTokens === 'object') { + for (const address of Object.keys(accountTokens)) { + const networkTokens = accountTokens[address] + if (networkTokens && typeof networkTokens === 'object') { + for (const network of Object.keys(networkTokens)) { + const tokensOnNetwork = networkTokens[network] + if (Array.isArray(tokensOnNetwork)) { + for (const token of tokensOnNetwork) { + if (isOldDai(token)) { + token.symbol = SAI_TOKEN_SYMBOL + } + } + } + } + } + } + } + } + + return state +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 93a4f0a0a..c2a9d8fe7 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -47,4 +47,7 @@ module.exports = [ require('./034'), require('./035'), require('./036'), + require('./037'), + require('./038'), + require('./039'), ] diff --git a/app/scripts/ui.js b/app/scripts/ui.js index 0fe92d47c..f9a8dc16a 100644 --- a/app/scripts/ui.js +++ b/app/scripts/ui.js @@ -1,3 +1,7 @@ + +// this must run before anything else +require('./lib/freezeGlobals') + // polyfills import 'abortcontroller-polyfill/dist/polyfill-patch-fetch' diff --git a/development/auto-changelog.sh b/development/auto-changelog.sh index 3ed059b3d..26ab8e93f 100755 --- a/development/auto-changelog.sh +++ b/development/auto-changelog.sh @@ -10,7 +10,7 @@ git fetch --tags most_recent_tag="$(git describe --tags "$(git rev-list --tags --max-count=1)")" -git rev-list "${most_recent_tag}"..HEAD | while read commit +git rev-list "${most_recent_tag}"..HEAD | while read -r commit do subject="$(git show -s --format="%s" "$commit")" diff --git a/development/metamaskbot-build-announce.js b/development/metamaskbot-build-announce.js index 2198d7e36..d5b05e03c 100755 --- a/development/metamaskbot-build-announce.js +++ b/development/metamaskbot-build-announce.js @@ -1,6 +1,6 @@ #!/usr/bin/env node const request = require('request-promise') -const VERSION = require('../dist/chrome/manifest.json').version +const VERSION = require('../dist/chrome/manifest.json').version // eslint-disable-line import/no-unresolved start().catch(console.error) diff --git a/development/mock-3box.js b/development/mock-3box.js index 781b32f3b..a26e02a82 100644 --- a/development/mock-3box.js +++ b/development/mock-3box.js @@ -28,7 +28,9 @@ class Mock3Box { static openBox (address) { this.address = address return Promise.resolve({ - onSyncDone: cb => { setTimeout(cb, 200) }, + onSyncDone: cb => { + setTimeout(cb, 200) + }, openSpace: async (spaceName, config) => { const { onSyncDone } = config this.spaceName = spaceName @@ -55,6 +57,13 @@ class Mock3Box { logout: () => {}, }) } + + static async getConfig (address) { + const backup = await loadFromMock3Box(`${address}-metamask-metamaskBackup`) + return backup + ? { spaces: { metamask: {} } } + : {} + } } module.exports = Mock3Box diff --git a/development/mock-dev.js b/development/mock-dev.js index 188c04678..8da625149 100644 --- a/development/mock-dev.js +++ b/development/mock-dev.js @@ -15,7 +15,6 @@ const h = require('react-hyperscript') const Root = require('../ui/app/pages') const configureStore = require('../ui/app/store/store') const actions = require('../ui/app/store/actions') -const states = require('./states') const backGroundConnectionModifiers = require('./backGroundConnectionModifiers') const Selector = require('./selector') const MetamaskController = require('../app/scripts/metamask-controller') @@ -23,6 +22,9 @@ const firstTimeState = require('../app/scripts/first-time-state') const ExtensionPlatform = require('../app/scripts/platforms/extension') const noop = function () {} +// the states file is generated before this file is run, but after `lint` is run +const states = require('./states') /* eslint-disable-line import/no-unresolved */ + const log = require('loglevel') window.log = log log.setLevel('debug') diff --git a/development/rollback.sh b/development/rollback.sh index 639d72a67..a3040e6f1 100755 --- a/development/rollback.sh +++ b/development/rollback.sh @@ -4,20 +4,20 @@ echo "Rolling back to version $1" # Checkout branch to increment version -git checkout -b version-increment-$1 +git checkout -b "version-increment-$1" yarn version:bump patch # Store the new version name -NEW_VERSION=$(cat app/manifest.json | jq -r .version) +NEW_VERSION=$(jq -r .version < app/manifest.json) # Make sure origin tags are loaded git fetch origin # check out the rollback branch -git checkout origin/v$1 +git checkout "origin/v$1" # Create the rollback branch. -git checkout -b Version-$NEW_VERSION-Rollback-to-$1 +git checkout -b "Version-$NEW_VERSION-Rollback-to-$1" # Set the version files to the next one. git checkout master CHANGELOG.md @@ -28,8 +28,8 @@ git commit -m "Version $NEW_VERSION (Rollback to $1)" git push -u origin HEAD # Create tag and push that up too -git tag v${NEW_VERSION} -git push origin v${NEW_VERSION} +git tag "v${NEW_VERSION}" +git push origin "v${NEW_VERSION}" # Cleanup version branch -git branch -D version-increment-$1 +git branch -D "version-increment-$1" diff --git a/development/sentry-publish.js b/development/sentry-publish.js index cab3d1ac8..8d9333a86 100644 --- a/development/sentry-publish.js +++ b/development/sentry-publish.js @@ -1,7 +1,7 @@ #!/usr/bin/env node const pify = require('pify') const exec = pify(require('child_process').exec, { multiArgs: true }) -const VERSION = require('../dist/chrome/manifest.json').version +const VERSION = require('../dist/chrome/manifest.json').version // eslint-disable-line import/no-unresolved start().catch(console.error) diff --git a/development/show-deps-install-scripts.js b/development/show-deps-install-scripts.js new file mode 100644 index 000000000..419c9d25f --- /dev/null +++ b/development/show-deps-install-scripts.js @@ -0,0 +1,38 @@ +// This script lists all dependencies that have package install scripts +const path = require('path') +const readInstalled = require('read-installed') + +const installScripts = ['preinstall', 'install', 'postinstall'] + +readInstalled('./', { dev: true }, function (err, data) { + if (err) { + throw err + } + + const deps = data.dependencies + Object.entries(deps).forEach(([packageName, packageData]) => { + const packageScripts = packageData.scripts || {} + const scriptKeys = Reflect.ownKeys(packageScripts) + + const hasInstallScript = installScripts.some(installKey => scriptKeys.includes(installKey)) + if (!hasInstallScript) { + return + } + + const matchingScripts = {} + if (packageScripts.preinstall) { + matchingScripts.preinstall = packageScripts.preinstall + } + if (packageScripts.install) { + matchingScripts.install = packageScripts.install + } + if (packageScripts.postinstall) { + matchingScripts.postinstall = packageScripts.postinstall + } + const scriptNames = Reflect.ownKeys(matchingScripts) + + const relativePath = path.relative(process.cwd(), packageData.path) + + console.log(`${packageName}: ${relativePath} ${scriptNames}`) + }) +}) diff --git a/development/sourcemap-validator.js b/development/sourcemap-validator.js index 546745f16..44336df9e 100644 --- a/development/sourcemap-validator.js +++ b/development/sourcemap-validator.js @@ -52,7 +52,9 @@ async function validateSourcemapForFile ({ buildName }) { const consumer = await new SourceMapConsumer(rawSourceMap) const hasContentsOfAllSources = consumer.hasContentsOfAllSources() - if (!hasContentsOfAllSources) console.warn('SourcemapValidator - missing content of some sources...') + if (!hasContentsOfAllSources) { + console.warn('SourcemapValidator - missing content of some sources...') + } console.log(` sampling from ${consumer.sources.length} files`) let sampleCount = 0 @@ -96,6 +98,8 @@ async function validateSourcemapForFile ({ buildName }) { function indicesOf (substring, string) { var a = [] var i = -1 - while ((i = string.indexOf(substring, i + 1)) >= 0) a.push(i) + while ((i = string.indexOf(substring, i + 1)) >= 0) { + a.push(i) + } return a } diff --git a/development/states/confirm-sig-requests.json b/development/states/confirm-sig-requests.json index ae7f3454d..a0f4cd470 100644 --- a/development/states/confirm-sig-requests.json +++ b/development/states/confirm-sig-requests.json @@ -23,6 +23,9 @@ "name": "Send Account 4" } }, + "abTests": { + "fullScreenVsPopup": "control" + }, "cachedBalances": {}, "conversionRate": 1200.88200327, "conversionDate": 1489013762, diff --git a/development/states/currency-localization.json b/development/states/currency-localization.json index dff527f5a..263ef89c5 100644 --- a/development/states/currency-localization.json +++ b/development/states/currency-localization.json @@ -23,6 +23,9 @@ "name": "Send Account 4" } }, + "abTests": { + "fullScreenVsPopup": "control" + }, "cachedBalances": {}, "unapprovedTxs": {}, "conversionRate": 19855, diff --git a/development/states/tx-list-items.json b/development/states/tx-list-items.json index 08d1cf263..fa5e93c6c 100644 --- a/development/states/tx-list-items.json +++ b/development/states/tx-list-items.json @@ -23,6 +23,9 @@ "name": "Send Account 4" } }, + "abTests": { + "fullScreenVsPopup": "control" + }, "cachedBalances": {}, "currentCurrency": "USD", "conversionRate": 1200.88200327, diff --git a/development/static-server.js b/development/static-server.js new file mode 100644 index 000000000..d8f22cabe --- /dev/null +++ b/development/static-server.js @@ -0,0 +1,92 @@ +const fs = require('fs') +const http = require('http') +const path = require('path') + +const chalk = require('chalk') +const pify = require('pify') +const serveHandler = require('serve-handler') + +const fsStat = pify(fs.stat) +const DEFAULT_PORT = 9080 + +const onResponse = (request, response) => { + if (response.statusCode >= 400) { + console.log(chalk`{gray '-->'} {red ${response.statusCode}} ${request.url}`) + } else if (response.statusCode >= 200 && response.statusCode < 300) { + console.log(chalk`{gray '-->'} {green ${response.statusCode}} ${request.url}`) + } else { + console.log(chalk`{gray '-->'} {green.dim ${response.statusCode}} ${request.url}`) + } +} +const onRequest = (request, response) => { + console.log(chalk`{gray '<--'} {blue [${request.method}]} ${request.url}`) + response.on('finish', () => onResponse(request, response)) +} + +const startServer = ({ port, rootDirectory }) => { + const server = http.createServer((request, response) => { + if (request.url.startsWith('/node_modules/')) { + request.url = request.url.substr(14) + return serveHandler(request, response, { + directoryListing: false, + public: path.resolve('./node_modules'), + }) + } + return serveHandler(request, response, { + directoryListing: false, + public: rootDirectory, + }) + }) + + server.on('request', onRequest) + + server.listen(port, () => { + console.log(`Running at http://localhost:${port}`) + }) +} + +const parsePort = (portString) => { + const port = Number(portString) + if (!Number.isInteger(port)) { + throw new Error(`Port '${portString}' is invalid; must be an integer`) + } else if (port < 0 || port > 65535) { + throw new Error(`Port '${portString}' is out of range; must be between 0 and 65535 inclusive`) + } + return port +} + +const parseDirectoryArgument = async (pathString) => { + const resolvedPath = path.resolve(pathString) + const directoryStats = await fsStat(resolvedPath) + if (!directoryStats.isDirectory()) { + throw new Error(`Invalid path '${pathString}'; must be a directory`) + } + return resolvedPath +} + +const main = async () => { + const args = process.argv.slice(2) + + const options = { + port: process.env.port || DEFAULT_PORT, + rootDirectory: path.resolve('.'), + } + + while (args.length) { + if (/^(--port|-p)$/i.test(args[0])) { + if (args[1] === undefined) { + throw new Error('Missing port argument') + } + options.port = parsePort(args[1]) + args.splice(0, 2) + } else { + options.rootDirectory = await parseDirectoryArgument(args[0]) + args.splice(0, 1) + } + } + + startServer(options) +} + +main() + .catch(console.error) diff --git a/development/verify-locale-strings.js b/development/verify-locale-strings.js index b9e8a90d4..aa0c4503f 100644 --- a/development/verify-locale-strings.js +++ b/development/verify-locale-strings.js @@ -4,7 +4,7 @@ // // usage: // -// node app/scripts/verify-locale-strings.js [] [--fix] +// node app/scripts/verify-locale-strings.js [] [--fix] [--quiet] // // This script will validate that locales have no unused messages. It will check // the English locale against string literals found under `ui/`, and it will check @@ -16,40 +16,44 @@ // The if the optional '--fix' parameter is given, locales will be automatically // updated to remove any unused messages. // +// The optional '--quiet' parameter reduces the verbosity of the output, printing +// just a single summary of results for each locale verified +// // ////////////////////////////////////////////////////////////////////////////// const fs = require('fs') const path = require('path') const { promisify } = require('util') +const log = require('loglevel') const matchAll = require('string.prototype.matchall').getPolyfill() const localeIndex = require('../app/_locales/index.json') const readdir = promisify(fs.readdir) const readFile = promisify(fs.readFile) const writeFile = promisify(fs.writeFile) -console.log('Locale Verification') +log.setDefaultLevel('info') let fix = false let specifiedLocale -if (process.argv[2] === '--fix') { - fix = true - specifiedLocale = process.argv[3] -} else { - specifiedLocale = process.argv[2] - if (process.argv[3] === '--fix') { +for (const arg of process.argv.slice(2)) { + if (arg === '--fix') { fix = true + } else if (arg === '--quiet') { + log.setLevel('error') + } else { + specifiedLocale = arg } } main(specifiedLocale, fix) .catch(error => { - console.error(error) + log.error(error) process.exit(1) }) async function main (specifiedLocale, fix) { if (specifiedLocale) { - console.log(`Verifying selected locale "${specifiedLocale}":\n\n`) + log.info(`Verifying selected locale "${specifiedLocale}":\n`) const locale = localeIndex.find(localeMeta => localeMeta.code === specifiedLocale) const failed = locale.code === 'en' ? await verifyEnglishLocale(fix) : @@ -58,16 +62,16 @@ async function main (specifiedLocale, fix) { process.exit(1) } } else { - console.log('Verifying all locales:\n\n') + log.info('Verifying all locales:\n') let failed = await verifyEnglishLocale(fix) const localeCodes = localeIndex .filter(localeMeta => localeMeta.code !== 'en') .map(localeMeta => localeMeta.code) for (const code of localeCodes) { + log.info() // Separate each locale report by a newline when not in '--quiet' mode const localeFailed = await verifyLocale(code, fix) failed = failed || localeFailed - console.log('\n') } if (failed) { @@ -87,9 +91,9 @@ async function getLocale (code) { return JSON.parse(fileContents) } catch (e) { if (e.code === 'ENOENT') { - console.log('Locale file not found') + log.error('Locale file not found') } else { - console.log(`Error opening your locale ("${code}") file: `, e) + log.error(`Error opening your locale ("${code}") file: `, e) } process.exit(1) } @@ -101,9 +105,9 @@ async function writeLocale (code, locale) { return writeFile(localeFilePath, JSON.stringify(locale, null, 2) + '\n', 'utf8') } catch (e) { if (e.code === 'ENOENT') { - console.log('Locale file not found') + log.error('Locale file not found') } else { - console.log(`Error writing your locale ("${code}") file: `, e) + log.error(`Error writing your locale ("${code}") file: `, e) } process.exit(1) } @@ -119,28 +123,26 @@ async function verifyLocale (code, fix = false) { const englishEntryCount = Object.keys(englishLocale).length const coveragePercent = 100 * (englishEntryCount - missingItems.length) / englishEntryCount - console.log(`Status of **${code}** ${coveragePercent.toFixed(2)}% coverage:`) - if (extraItems.length) { - console.log('\nExtra items that should not be localized:') + console.log(`**${code}**: ${extraItems.length} unused messages`) + log.info('Extra items that should not be localized:') extraItems.forEach(function (key) { - console.log(` - [ ] ${key}`) + log.info(` - [ ] ${key}`) }) } else { - // console.log(` all ${counter} strings declared in your locale ("${code}") were found in the english one`) + log.info(`**${code}**: ${extraItems.length} unused messages`) } + log.info(`${coveragePercent.toFixed(2)}% coverage`) if (missingItems.length) { - console.log(`\nMissing items not present in localized file:`) + log.info(`Missing items not present in localized file:`) missingItems.forEach(function (key) { - console.log(` - [ ] ${key}`) + log.info(` - [ ] ${key}`) }) - } else { - // console.log(` all ${counter} english strings were found in your locale ("${code}")!`) } if (!extraItems.length && !missingItems.length) { - console.log('Full coverage : )') + log.info('Full coverage : )') } if (extraItems.length > 0) { @@ -174,18 +176,18 @@ async function verifyEnglishLocale (fix = false) { const unusedMessages = englishMessages .filter(message => !messageExceptions.includes(message) && !usedMessages.has(message)) - console.log(`Status of **English (en)** ${unusedMessages.length} unused messages:`) - if (unusedMessages.length === 0) { - console.log('Full coverage : )') + if (unusedMessages.length) { + console.log(`**en**: ${unusedMessages.length} unused messages`) + log.info(`Messages not present in UI:`) + unusedMessages.forEach(function (key) { + log.info(` - [ ] ${key}`) + }) + } else { + log.info('Full coverage : )') return false } - console.log(`\nMessages not present in UI:`) - unusedMessages.forEach(function (key) { - console.log(` - [ ] ${key}`) - }) - if (unusedMessages.length > 0 && fix) { const newLocale = Object.assign({}, englishLocale) for (const key of unusedMessages) { diff --git a/gulpfile.js b/gulpfile.js index fa4c07db3..d5d74333a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -16,7 +16,6 @@ const manifest = require('./app/manifest.json') const sass = require('gulp-sass') const autoprefixer = require('gulp-autoprefixer') const gulpStylelint = require('gulp-stylelint') -const stylefmt = require('gulp-stylefmt') const terser = require('gulp-terser-js') const pify = require('pify') const rtlcss = require('gulp-rtlcss') @@ -357,12 +356,6 @@ gulp.task('lint-scss', function () { })) }) -gulp.task('fmt-scss', function () { - return gulp.src('ui/app/css/itcss/**/*.scss') - .pipe(stylefmt()) - .pipe(gulp.dest('ui/app/css/itcss')) -}) - // build js const buildJsFiles = [ @@ -435,7 +428,9 @@ function createTasksForBuildJs ({ rootDir, taskPrefix, bundleTaskOpts, destinati // compose into larger task const subtasks = [] subtasks.push(gulp.parallel(buildPhase1.map(file => `${taskPrefix}:${file}`))) - if (buildPhase2.length) subtasks.push(gulp.parallel(buildPhase2.map(file => `${taskPrefix}:${file}`))) + if (buildPhase2.length) { + subtasks.push(gulp.parallel(buildPhase2.map(file => `${taskPrefix}:${file}`))) + } gulp.task(taskPrefix, gulp.series(subtasks)) } @@ -454,18 +449,6 @@ gulp.task('zip', gulp.parallel('zip:chrome', 'zip:firefox', 'zip:opera')) // high level tasks -gulp.task('dev', - gulp.series( - 'clean', - 'dev:scss', - gulp.parallel( - 'dev:extension:js', - 'dev:copy', - 'dev:reload' - ) - ) -) - gulp.task('dev:test', gulp.series( 'clean', @@ -514,23 +497,10 @@ gulp.task('build:test', 'build:test:extension:js', 'copy' ), - 'optimize:images', 'manifest:testing' ) ) -gulp.task('build:extension', - gulp.series( - 'clean', - 'build:scss', - gulp.parallel( - 'build:extension:js', - 'copy' - ), - 'optimize:images' - ) -) - gulp.task('dist', gulp.series( 'build', diff --git a/package.json b/package.json index 6e36b07f3..ca6cf6086 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,14 @@ "start:test": "gulp dev:test", "build:test": "gulp build:test", "test": "yarn test:unit && yarn lint", - "dapp": "static-server test/e2e/contract-test --port 8080", - "dapp-chain": "GANACHE_ARGS='-b 2' concurrently -k -n ganache,dapp -p '[{time}][{name}]' 'yarn ganache:start' 'sleep 5 && static-server test/e2e/contract-test --port 8080'", + "dapp": "node development/static-server.js test/e2e/contract-test --port 8080", + "dapp-chain": "GANACHE_ARGS='-b 2' concurrently -k -n ganache,dapp -p '[{time}][{name}]' 'yarn ganache:start' 'sleep 5 && node development/static-server.js test/e2e/contract-test --port 8080'", + "forwarder": "node ./development/static-server.js ./node_modules/@metamask/forwarder/dist/ --port 9010", + "dapp-forwarder": "concurrently -k -n forwarder,dapp -p '[{time}][{name}]' 'yarn forwarder' 'yarn dapp'", "watch:test:unit": "nodemon --exec \"yarn test:unit\" ./test ./app ./ui", - "sendwithprivatedapp": "static-server test/e2e/send-eth-with-private-key-test --port 8080", + "sendwithprivatedapp": "node development/static-server.js test/e2e/send-eth-with-private-key-test --port 8080", "test:unit": "cross-env METAMASK_ENV=test mocha --exit --require test/setup.js --recursive \"test/unit/**/*.js\" \"ui/app/**/*.test.js\"", + "test:unit:global": "mocha test/unit-global/*", "test:single": "cross-env METAMASK_ENV=test mocha --require test/helper.js", "test:integration": "yarn test:integration:build && yarn test:flat", "test:integration:build": "gulp build:scss", @@ -36,6 +39,8 @@ "lint:fix": "eslint . --ext js,json --fix", "lint:changed": "{ git ls-files --others --exclude-standard ; git diff-index --name-only --diff-filter=d HEAD ; } | grep --regexp='[.]js$' --regexp='[.]json$' | tr '\\n' '\\0' | xargs -0 eslint", "lint:changed:fix": "{ git ls-files --others --exclude-standard ; git diff-index --name-only --diff-filter=d HEAD ; } | grep --regexp='[.]js$' --regexp='[.]json$' | tr '\\n' '\\0' | xargs -0 eslint --fix", + "lint:shellcheck": "shellcheck --version && find . -type f -name '*.sh' ! -path './node_modules/*' -print0 | xargs -0 shellcheck", + "verify-locales": "node ./development/verify-locale-strings.js", "mozilla-lint": "addons-linter dist/firefox", "watch": "cross-env METAMASK_ENV=test mocha --watch --require test/setup.js --reporter min --recursive \"test/unit/**/*.js\" \"ui/app/**/*.test.js\"", "devtools:react": "react-devtools", @@ -73,22 +78,23 @@ "capnode": "^4.0.1", "classnames": "^2.2.5", "clone": "^2.1.2", - "content-hash": "^2.4.3", + "content-hash": "^2.4.4", "copy-to-clipboard": "^3.0.8", "currency-formatter": "^1.4.2", "d3": "^5.7.0", "debounce": "1.1.0", "debounce-stream": "^2.0.0", "deep-extend": "^0.5.1", + "deep-freeze-strict": "1.1.1", "detect-node": "^2.0.3", "detectrtc": "^1.3.6", "dnode": "^1.2.2", "end-of-stream": "^1.1.0", "eth-block-tracker": "^4.4.2", - "eth-contract-metadata": "^1.9.2", + "eth-contract-metadata": "^1.11.0", "eth-ens-namehash": "^2.0.8", - "eth-json-rpc-errors": "^1.1.0", - "eth-json-rpc-filters": "^4.1.0", + "eth-json-rpc-errors": "^2.0.0", + "eth-json-rpc-filters": "^4.1.1", "eth-json-rpc-infura": "^4.0.1", "eth-json-rpc-middleware": "^4.2.0", "eth-keyring-controller": "^5.3.1", @@ -101,7 +107,7 @@ "eth-trezor-keyring": "^0.4.0", "ethereumjs-abi": "^0.6.4", "ethereumjs-tx": "1.3.7", - "ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", + "ethereumjs-util": "5.1.0", "ethereumjs-wallet": "^0.6.0", "ethers": "^4.0.39", "etherscan-link": "^1.0.2", @@ -114,7 +120,7 @@ "fast-deep-equal": "^2.0.1", "fast-json-patch": "^2.0.4", "fuse.js": "^3.2.0", - "gaba": "^1.6.0", + "gaba": "^1.9.0", "human-standard-token-abi": "^2.0.0", "jazzicon": "^1.2.0", "json-rpc-engine": "^5.1.5", @@ -128,6 +134,7 @@ "metamask-logo": "^2.1.4", "mkdirp": "^0.5.1", "multihashes": "^0.4.12", + "nanoid": "^2.1.6", "nonce-tracker": "^1.0.0", "number-to-bn": "^1.7.0", "obj-multiplex": "^1.0.0", @@ -141,6 +148,7 @@ "prop-types": "^15.6.1", "pubnub": "4.24.4", "pump": "^3.0.0", + "punycode": "^2.1.1", "qrcode-generator": "1.4.1", "ramda": "^0.24.1", "react": "^15.6.2", @@ -190,23 +198,26 @@ "@babel/preset-env": "^7.5.5", "@babel/preset-react": "^7.0.0", "@babel/register": "^7.5.5", + "@metamask/forwarder": "^1.0.0", + "@metamask/onboarding": "^0.1.2", "@sentry/cli": "^1.30.3", + "@storybook/addon-actions": "^5.2.6", "@storybook/addon-info": "^5.1.1", "@storybook/addon-knobs": "^3.4.2", "@storybook/react": "^5.1.1", - "addons-linter": "^1.10.0", + "addons-linter": "1.14.0", "babel-eslint": "^10.0.2", "babelify": "^10.0.0", "brfs": "^1.6.1", "browserify": "^16.2.3", "browserify-transform-tools": "^1.7.0", "chai": "^4.1.0", + "chalk": "^2.4.2", "chromedriver": "^2.41.0", "concurrently": "^4.1.1", "coveralls": "^3.0.0", "cross-env": "^5.1.4", "css-loader": "^2.1.1", - "deep-freeze-strict": "^1.1.1", "del": "^3.0.0", "deps-dump": "^1.1.0", "envify": "^4.0.0", @@ -214,6 +225,7 @@ "enzyme-adapter-react-15": "^1.0.6", "eslint": "^6.0.1", "eslint-plugin-chai": "0.0.1", + "eslint-plugin-import": "^2.18.2", "eslint-plugin-json": "^1.2.0", "eslint-plugin-mocha": "^5.0.0", "eslint-plugin-react": "^7.4.0", @@ -222,7 +234,7 @@ "fs-extra": "^6.0.1", "fs-promise": "^2.0.3", "ganache-cli": "^6.4.4", - "ganache-core": "^2.5.7", + "ganache-core": "2.8.0", "geckodriver": "^1.16.2", "gh-pages": "^1.2.0", "gulp": "^4.0.0", @@ -239,7 +251,6 @@ "gulp-rtlcss": "^1.4.0", "gulp-sass": "^4.0.0", "gulp-sourcemaps": "^2.6.0", - "gulp-stylefmt": "^1.1.0", "gulp-stylelint": "^7.0.0", "gulp-terser-js": "^5.0.0", "gulp-util": "^3.0.7", @@ -271,6 +282,7 @@ "radgrad-jsdoc-template": "^1.1.3", "react-devtools": "^3.6.1", "react-test-renderer": "^15.6.2", + "read-installed": "^4.0.3", "redux-mock-store": "^1.5.3", "redux-test-utils": "^0.2.2", "remote-redux-devtools": "^0.5.16", @@ -279,15 +291,15 @@ "rimraf": "^2.6.2", "sass-loader": "^7.0.1", "selenium-webdriver": "^3.5.0", + "serve-handler": "^6.1.2", "sesify": "^4.2.1", - "sesify-viz": "^2.0.1", + "sesify-viz": "^3.0.5", "sinon": "^5.0.0", "source-map": "^0.7.2", "source-map-explorer": "^2.0.1", - "static-server": "^2.2.1", "style-loader": "^0.21.0", + "stylelint": "^9.10.1", "stylelint-config-standard": "^18.2.0", - "tape": "^4.5.1", "testem": "^2.16.0", "vinyl-buffer": "^1.0.1", "vinyl-source-stream": "^2.0.0", diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 0be365249..2db918522 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -120,13 +120,17 @@ "currentCurrency": "usd", "nativeCurrency": "ETH", "conversionRate": 556.12, - "addressBook": [ - { - "address": "0xc42edfcc21ed14dda456aa0756c153f7985d8813", - "name": "", - "chainId": 4 + "addressBook": { + "4": { + "0xc42edfcc21ed14dda456aa0756c153f7985d8813": { + "address": "0xc42edfcc21ed14dda456aa0756c153f7985d8813", + "chainId": "4", + "isEns": false, + "memo": "", + "name": "" + } } - ], + }, "selectedTokenAddress": "0x108cf70c7d384c552f42c07c41c0e1e46d77ea0d", "unapprovedMsgs": {}, "unapprovedMsgCount": 0, diff --git a/test/e2e/address-book.spec.js b/test/e2e/address-book.spec.js index c7d846e5a..b4a709825 100644 --- a/test/e2e/address-book.spec.js +++ b/test/e2e/address-book.spec.js @@ -8,14 +8,13 @@ const { checkBrowserForConsoleErrors, findElement, findElements, - loadExtension, verboseReportOnFailure, setupFetchMocking, prepareExtensionForTesting, } = require('./helpers') +const enLocaleMessages = require('../../app/_locales/en/messages.json') describe('MetaMask', function () { - let extensionId let driver const testSeedPhrase = 'forum vessel pink push lonely enact gentle tail admit parrot grunt dress' @@ -29,7 +28,6 @@ describe('MetaMask', function () { before(async function () { const result = await prepareExtensionForTesting() driver = result.driver - extensionId = result.extensionId await setupFetchMocking(driver) }) @@ -54,7 +52,7 @@ describe('MetaMask', function () { describe('Going through the first time flow', () => { it('clicks the continue button on the welcome screen', async () => { await findElement(driver, By.css('.welcome-page__header')) - const welcomeScreenBtn = await findElement(driver, By.css('.first-time-flow__button')) + const welcomeScreenBtn = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`)) welcomeScreenBtn.click() await delay(largeDelayMs) }) @@ -99,7 +97,7 @@ describe('MetaMask', function () { assert.equal(seedPhrase.split(' ').length, 12) await delay(regularDelayMs) - const nextScreen = (await findElements(driver, By.css('button.first-time-flow__button')))[1] + const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.next.message}')]`)) await nextScreen.click() await delay(regularDelayMs) }) @@ -112,37 +110,12 @@ describe('MetaMask', function () { await delay(tinyDelayMs) } - async function retypeSeedPhrase (words, wasReloaded, count = 0) { - try { - if (wasReloaded) { - const byRevealButton = By.css('.reveal-seed-phrase__secret-blocker .reveal-seed-phrase__reveal-button') - await driver.wait(until.elementLocated(byRevealButton, 10000)) - const revealSeedPhraseButton = await findElement(driver, byRevealButton, 10000) - await revealSeedPhraseButton.click() - await delay(regularDelayMs) - - const nextScreen = await findElement(driver, By.css('button.first-time-flow__button')) - await nextScreen.click() - await delay(regularDelayMs) - } - - for (let i = 0; i < 12; i++) { - await clickWordAndWait(words[i]) - } - } catch (e) { - if (count > 2) { - throw e - } else { - await loadExtension(driver, extensionId) - await retypeSeedPhrase(words, true, count + 1) - } - } - } - it('can retype the seed phrase', async () => { const words = seedPhrase.split(' ') - await retypeSeedPhrase(words) + for (const word of words) { + await clickWordAndWait(word) + } const confirm = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) await confirm.click() @@ -151,7 +124,7 @@ describe('MetaMask', function () { it('clicks through the success screen', async () => { await findElement(driver, By.xpath(`//div[contains(text(), 'Congratulations')]`)) - const doneButton = await findElement(driver, By.css('button.first-time-flow__button')) + const doneButton = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`)) await doneButton.click() await delay(regularDelayMs) }) @@ -183,7 +156,7 @@ describe('MetaMask', function () { await passwordInputs[0].sendKeys('correct horse battery staple') await passwordInputs[1].sendKeys('correct horse battery staple') - await driver.findElement(By.css('.first-time-flow__button')).click() + await driver.findElement(By.xpath(`//button[contains(text(), '${enLocaleMessages.restore.message}')]`)).click() await delay(regularDelayMs) }) @@ -237,13 +210,13 @@ describe('MetaMask', function () { }) it('finds the transaction in the transactions list', async function () { - const transactions = await findElements(driver, By.css('.transaction-list-item')) - assert.equal(transactions.length, 1) + await driver.wait(async () => { + const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) + return confirmedTxes.length === 1 + }, 10000) - if (process.env.SELENIUM_BROWSER !== 'firefox') { - const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary')) - await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000) - } + const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary')) + await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000) }) }) @@ -278,13 +251,13 @@ describe('MetaMask', function () { }) it('finds the transaction in the transactions list', async function () { - const transactions = await findElements(driver, By.css('.transaction-list-item')) - assert.equal(transactions.length, 2) + await driver.wait(async () => { + const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) + return confirmedTxes.length === 2 + }, 10000) - if (process.env.SELENIUM_BROWSER !== 'firefox') { - const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary')) - await driver.wait(until.elementTextMatches(txValues, /-2\s*ETH/), 10000) - } + const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary')) + await driver.wait(until.elementTextMatches(txValues, /-2\s*ETH/), 10000) }) }) }) diff --git a/test/e2e/contract-test/contract.js b/test/e2e/contract-test/contract.js index 971523de2..a6b5f110b 100644 --- a/test/e2e/contract-test/contract.js +++ b/test/e2e/contract-test/contract.js @@ -1,4 +1,4 @@ -/*global ethereum*/ +/*global ethereum, MetamaskOnboarding */ /* The `piggybankContract` is compiled from: @@ -30,8 +30,14 @@ The `piggybankContract` is compiled from: } */ -web3.currentProvider.enable().then(() => { - var piggybankContract = web3.eth.contract([{'constant': false, 'inputs': [{'name': 'withdrawAmount', 'type': 'uint256'}], 'name': 'withdraw', 'outputs': [{'name': 'remainingBal', 'type': 'uint256'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': true, 'inputs': [], 'name': 'owner', 'outputs': [{'name': '', 'type': 'address'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': false, 'inputs': [], 'name': 'deposit', 'outputs': [{'name': '', 'type': 'uint256'}], 'payable': true, 'stateMutability': 'payable', 'type': 'function'}, {'inputs': [], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'constructor'}]) +const forwarderOrigin = 'http://localhost:9010' + +const isMetaMaskInstalled = () => { + return Boolean(window.ethereum && window.ethereum.isMetaMask) +} + +const initialize = () => { + const onboardButton = document.getElementById('connectButton') const deployButton = document.getElementById('deployButton') const depositButton = document.getElementById('depositButton') const withdrawButton = document.getElementById('withdrawButton') @@ -41,147 +47,296 @@ web3.currentProvider.enable().then(() => { const approveTokens = document.getElementById('approveTokens') const transferTokensWithoutGas = document.getElementById('transferTokensWithoutGas') const approveTokensWithoutGas = document.getElementById('approveTokensWithoutGas') + const signTypedData = document.getElementById('signTypedData') + const signTypedDataResults = document.getElementById('signTypedDataResult') - deployButton.addEventListener('click', async function () { - document.getElementById('contractStatus').innerHTML = 'Deploying' - - var piggybank = await piggybankContract.new( - { - from: web3.eth.accounts[0], - data: '0x608060405234801561001057600080fd5b5033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506000808190555061023b806100686000396000f300608060405260043610610057576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e1a7d4d1461005c5780638da5cb5b1461009d578063d0e30db0146100f4575b600080fd5b34801561006857600080fd5b5061008760048036038101908080359060200190929190505050610112565b6040518082815260200191505060405180910390f35b3480156100a957600080fd5b506100b26101d0565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100fc6101f6565b6040518082815260200191505060405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614151561017057600080fd5b8160008082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc839081150290604051600060405180830381858888f193505050501580156101c5573d6000803e3d6000fd5b506000549050919050565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60003460008082825401925050819055506000549050905600a165627a7a72305820f237db3ec816a52589d82512117bc85bc08d3537683ffeff9059108caf3e5d400029', - gas: '4700000', - }, function (e, contract) { - if (e) { - throw e - } - if (typeof contract.address !== 'undefined') { - console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash) + const contractStatus = document.getElementById('contractStatus') + const tokenAddress = document.getElementById('tokenAddress') + const networkDiv = document.getElementById('network') + const chainIdDiv = document.getElementById('chainId') + const accountsDiv = document.getElementById('accounts') - document.getElementById('contractStatus').innerHTML = 'Deployed' + let onboarding + try { + onboarding = new MetamaskOnboarding({ forwarderOrigin }) + } catch (error) { + console.error(error) + } + let accounts + let piggybankContract - depositButton.addEventListener('click', function () { - document.getElementById('contractStatus').innerHTML = 'Deposit initiated' - contract.deposit({ from: web3.eth.accounts[0], value: '0x3782dace9d900000' }, function (result) { - console.log(result) - document.getElementById('contractStatus').innerHTML = 'Deposit completed' - }) - }) + const accountButtons = [ + deployButton, + depositButton, + withdrawButton, + sendButton, + createToken, + transferTokens, + approveTokens, + transferTokensWithoutGas, + approveTokensWithoutGas, + ] - withdrawButton.addEventListener('click', function () { - contract.withdraw('0xde0b6b3a7640000', { from: web3.eth.accounts[0] }, function (result) { - console.log(result) - document.getElementById('contractStatus').innerHTML = 'Withdrawn' - }) - }) + for (const button of accountButtons) { + button.disabled = true + } + + const isMetaMaskConnected = () => accounts && accounts.length > 0 + + const onClickInstall = () => { + onboardButton.innerText = 'Onboarding in progress' + onboardButton.disabled = true + onboarding.startOnboarding() + } + + const onClickConnect = async () => { + await window.ethereum.enable() + } + + const updateButtons = () => { + const accountButtonsDisabled = !isMetaMaskInstalled() || !isMetaMaskConnected() + if (accountButtonsDisabled) { + for (const button of accountButtons) { + button.disabled = true + } + } else { + deployButton.disabled = false + sendButton.disabled = false + createToken.disabled = false + } + + if (!isMetaMaskInstalled()) { + onboardButton.innerText = 'Click here to install MetaMask!' + onboardButton.onclick = onClickInstall + } else if (isMetaMaskConnected()) { + onboardButton.innerText = 'Connected' + onboardButton.disabled = true + if (onboarding) { + onboarding.stopOnboarding() + } + } else { + onboardButton.innerText = 'Connect' + onboardButton.onclick = onClickConnect + } + } + + const initializeAccountButtons = () => { + piggybankContract = web3.eth.contract([{'constant': false, 'inputs': [{'name': 'withdrawAmount', 'type': 'uint256'}], 'name': 'withdraw', 'outputs': [{'name': 'remainingBal', 'type': 'uint256'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': true, 'inputs': [], 'name': 'owner', 'outputs': [{'name': '', 'type': 'address'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': false, 'inputs': [], 'name': 'deposit', 'outputs': [{'name': '', 'type': 'uint256'}], 'payable': true, 'stateMutability': 'payable', 'type': 'function'}, {'inputs': [], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'constructor'}]) + deployButton.onclick = async () => { + contractStatus.innerHTML = 'Deploying' + + const piggybank = await piggybankContract.new( + { + from: accounts[0], + data: '0x608060405234801561001057600080fd5b5033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506000808190555061023b806100686000396000f300608060405260043610610057576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e1a7d4d1461005c5780638da5cb5b1461009d578063d0e30db0146100f4575b600080fd5b34801561006857600080fd5b5061008760048036038101908080359060200190929190505050610112565b6040518082815260200191505060405180910390f35b3480156100a957600080fd5b506100b26101d0565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100fc6101f6565b6040518082815260200191505060405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614151561017057600080fd5b8160008082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc839081150290604051600060405180830381858888f193505050501580156101c5573d6000803e3d6000fd5b506000549050919050565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60003460008082825401925050819055506000549050905600a165627a7a72305820f237db3ec816a52589d82512117bc85bc08d3537683ffeff9059108caf3e5d400029', + gas: '4700000', + }, (error, contract) => { + if (error) { + contractStatus.innerHTML = 'Deployment Failed' + throw error + } else if (contract.address === undefined) { + return + } + + console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash) + contractStatus.innerHTML = 'Deployed' + depositButton.disabled = false + withdrawButton.disabled = false + + depositButton.onclick = () => { + contractStatus.innerHTML = 'Deposit initiated' + contract.deposit( + { + from: accounts[0], + value: '0x3782dace9d900000', + }, + (result) => { + console.log(result) + contractStatus.innerHTML = 'Deposit completed' + } + ) + } + withdrawButton.onclick = () => { + contract.withdraw( + '0xde0b6b3a7640000', + { from: accounts[0] }, + (result) => { + console.log(result) + contractStatus.innerHTML = 'Withdrawn' + } + ) + } } + ) + console.log(piggybank) + } + + sendButton.onclick = () => { + web3.eth.sendTransaction({ + from: accounts[0], + to: '0x2f318C334780961FB129D2a6c30D0763d9a5C970', + value: '0x29a2241af62c0000', + gas: 21000, + gasPrice: 20000000000, + }, (result) => { + console.log(result) }) + } - console.log(piggybank) - }) - - sendButton.addEventListener('click', function () { - web3.eth.sendTransaction({ - from: web3.eth.accounts[0], - to: '0x2f318C334780961FB129D2a6c30D0763d9a5C970', - value: '0x29a2241af62c0000', - gas: 21000, - gasPrice: 20000000000, - }, (result) => { - console.log(result) - }) - }) - - - createToken.addEventListener('click', async function () { - var _initialAmount = 100 - var _tokenName = 'TST' - var _decimalUnits = 0 - var _tokenSymbol = 'TST' - var humanstandardtokenContract = web3.eth.contract([{'constant': true, 'inputs': [], 'name': 'name', 'outputs': [{'name': '', 'type': 'string'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': false, 'inputs': [{'name': '_spender', 'type': 'address'}, {'name': '_value', 'type': 'uint256'}], 'name': 'approve', 'outputs': [{'name': 'success', 'type': 'bool'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': true, 'inputs': [], 'name': 'totalSupply', 'outputs': [{'name': '', 'type': 'uint256'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': false, 'inputs': [{'name': '_from', 'type': 'address'}, {'name': '_to', 'type': 'address'}, {'name': '_value', 'type': 'uint256'}], 'name': 'transferFrom', 'outputs': [{'name': 'success', 'type': 'bool'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': true, 'inputs': [], 'name': 'decimals', 'outputs': [{'name': '', 'type': 'uint8'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': true, 'inputs': [], 'name': 'version', 'outputs': [{'name': '', 'type': 'string'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': true, 'inputs': [{'name': '_owner', 'type': 'address'}], 'name': 'balanceOf', 'outputs': [{'name': 'balance', 'type': 'uint256'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': true, 'inputs': [], 'name': 'symbol', 'outputs': [{'name': '', 'type': 'string'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': false, 'inputs': [{'name': '_to', 'type': 'address'}, {'name': '_value', 'type': 'uint256'}], 'name': 'transfer', 'outputs': [{'name': 'success', 'type': 'bool'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': false, 'inputs': [{'name': '_spender', 'type': 'address'}, {'name': '_value', 'type': 'uint256'}, {'name': '_extraData', 'type': 'bytes'}], 'name': 'approveAndCall', 'outputs': [{'name': 'success', 'type': 'bool'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': true, 'inputs': [{'name': '_owner', 'type': 'address'}, {'name': '_spender', 'type': 'address'}], 'name': 'allowance', 'outputs': [{'name': 'remaining', 'type': 'uint256'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'inputs': [{'name': '_initialAmount', 'type': 'uint256'}, {'name': '_tokenName', 'type': 'string'}, {'name': '_decimalUnits', 'type': 'uint8'}, {'name': '_tokenSymbol', 'type': 'string'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'constructor'}, {'payable': false, 'stateMutability': 'nonpayable', 'type': 'fallback'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': '_from', 'type': 'address'}, {'indexed': true, 'name': '_to', 'type': 'address'}, {'indexed': false, 'name': '_value', 'type': 'uint256'}], 'name': 'Transfer', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': '_owner', 'type': 'address'}, {'indexed': true, 'name': '_spender', 'type': 'address'}, {'indexed': false, 'name': '_value', 'type': 'uint256'}], 'name': 'Approval', 'type': 'event'}]) - return humanstandardtokenContract.new( - _initialAmount, - _tokenName, - _decimalUnits, - _tokenSymbol, - { - from: web3.eth.accounts[0], - data: '0x60806040523480156200001157600080fd5b506040516200156638038062001566833981018060405260808110156200003757600080fd5b8101908080516401000000008111156200005057600080fd5b828101905060208101848111156200006757600080fd5b81518560018202830111640100000000821117156200008557600080fd5b50509291906020018051640100000000811115620000a257600080fd5b82810190506020810184811115620000b957600080fd5b8151856001820283011164010000000082111715620000d757600080fd5b5050929190602001805190602001909291908051906020019092919050505083838382600390805190602001906200011192919062000305565b5081600490805190602001906200012a92919062000305565b5080600560006101000a81548160ff021916908360ff1602179055505050506200016433826200016e640100000000026401000000009004565b50505050620003b4565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1614151515620001ab57600080fd5b620001d081600254620002e36401000000000262001155179091906401000000009004565b60028190555062000237816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054620002e36401000000000262001155179091906401000000009004565b6000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508173ffffffffffffffffffffffffffffffffffffffff16600073ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040518082815260200191505060405180910390a35050565b6000808284019050838110151515620002fb57600080fd5b8091505092915050565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106200034857805160ff191683800117855562000379565b8280016001018555821562000379579182015b82811115620003785782518255916020019190600101906200035b565b5b5090506200038891906200038c565b5090565b620003b191905b80821115620003ad57600081600090555060010162000393565b5090565b90565b6111a280620003c46000396000f3fe6080604052600436106100af576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806306fdde03146100b4578063095ea7b31461014457806318160ddd146101b757806323b872dd146101e2578063313ce5671461027557806339509351146102a657806370a082311461031957806395d89b411461037e578063a457c2d71461040e578063a9059cbb14610481578063dd62ed3e146104f4575b600080fd5b3480156100c057600080fd5b506100c9610579565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156101095780820151818401526020810190506100ee565b50505050905090810190601f1680156101365780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561015057600080fd5b5061019d6004803603604081101561016757600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291908035906020019092919050505061061b565b604051808215151515815260200191505060405180910390f35b3480156101c357600080fd5b506101cc610748565b6040518082815260200191505060405180910390f35b3480156101ee57600080fd5b5061025b6004803603606081101561020557600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610752565b604051808215151515815260200191505060405180910390f35b34801561028157600080fd5b5061028a61095a565b604051808260ff1660ff16815260200191505060405180910390f35b3480156102b257600080fd5b506102ff600480360360408110156102c957600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610971565b604051808215151515815260200191505060405180910390f35b34801561032557600080fd5b506103686004803603602081101561033c57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610ba8565b6040518082815260200191505060405180910390f35b34801561038a57600080fd5b50610393610bf0565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156103d35780820151818401526020810190506103b8565b50505050905090810190601f1680156104005780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561041a57600080fd5b506104676004803603604081101561043157600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610c92565b604051808215151515815260200191505060405180910390f35b34801561048d57600080fd5b506104da600480360360408110156104a457600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610ec9565b604051808215151515815260200191505060405180910390f35b34801561050057600080fd5b506105636004803603604081101561051757600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610ee0565b6040518082815260200191505060405180910390f35b606060038054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156106115780601f106105e657610100808354040283529160200191610611565b820191906000526020600020905b8154815290600101906020018083116105f457829003601f168201915b5050505050905090565b60008073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff161415151561065857600080fd5b81600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040518082815260200191505060405180910390a36001905092915050565b6000600254905090565b60006107e382600160008773ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054610f6790919063ffffffff16565b600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000208190555061086e848484610f89565b3373ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925600160008873ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020546040518082815260200191505060405180910390a3600190509392505050565b6000600560009054906101000a900460ff16905090565b60008073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16141515156109ae57600080fd5b610a3d82600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205461115590919063ffffffff16565b600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008773ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020546040518082815260200191505060405180910390a36001905092915050565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b606060048054600181600116156101000203166002900480601f016020809104026020016040519081016040528092919081815260200182805460018160011615610100020316600290048015610c885780601f10610c5d57610100808354040283529160200191610c88565b820191906000526020600020905b815481529060010190602001808311610c6b57829003601f168201915b5050505050905090565b60008073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1614151515610ccf57600080fd5b610d5e82600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054610f6790919063ffffffff16565b600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008773ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020546040518082815260200191505060405180910390a36001905092915050565b6000610ed6338484610f89565b6001905092915050565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905092915050565b6000828211151515610f7857600080fd5b600082840390508091505092915050565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1614151515610fc557600080fd5b611016816000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054610f6790919063ffffffff16565b6000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055506110a9816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205461115590919063ffffffff16565b6000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040518082815260200191505060405180910390a3505050565b600080828401905083811015151561116c57600080fd5b809150509291505056fea165627a7a723058205fcdfea06f4d97b442bc9f444b1e92524bc66398eb4f37ed5a99f2093a8842640029000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000186a00000000000000000000000000000000000000000000000000000000000000003545354000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035453540000000000000000000000000000000000000000000000000000000000', - gas: '4700000', - gasPrice: '20000000000', - }, function (e, contract) { - console.log(e, contract) - if (typeof contract.address !== 'undefined') { - console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash) + createToken.onclick = async () => { + const _initialAmount = 100 + const _tokenName = 'TST' + const _decimalUnits = 0 + const _tokenSymbol = 'TST' + const humanstandardtokenContract = web3.eth.contract([{'constant': true, 'inputs': [], 'name': 'name', 'outputs': [{'name': '', 'type': 'string'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': false, 'inputs': [{'name': '_spender', 'type': 'address'}, {'name': '_value', 'type': 'uint256'}], 'name': 'approve', 'outputs': [{'name': 'success', 'type': 'bool'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': true, 'inputs': [], 'name': 'totalSupply', 'outputs': [{'name': '', 'type': 'uint256'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': false, 'inputs': [{'name': '_from', 'type': 'address'}, {'name': '_to', 'type': 'address'}, {'name': '_value', 'type': 'uint256'}], 'name': 'transferFrom', 'outputs': [{'name': 'success', 'type': 'bool'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': true, 'inputs': [], 'name': 'decimals', 'outputs': [{'name': '', 'type': 'uint8'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': true, 'inputs': [], 'name': 'version', 'outputs': [{'name': '', 'type': 'string'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': true, 'inputs': [{'name': '_owner', 'type': 'address'}], 'name': 'balanceOf', 'outputs': [{'name': 'balance', 'type': 'uint256'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': true, 'inputs': [], 'name': 'symbol', 'outputs': [{'name': '', 'type': 'string'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'constant': false, 'inputs': [{'name': '_to', 'type': 'address'}, {'name': '_value', 'type': 'uint256'}], 'name': 'transfer', 'outputs': [{'name': 'success', 'type': 'bool'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': false, 'inputs': [{'name': '_spender', 'type': 'address'}, {'name': '_value', 'type': 'uint256'}, {'name': '_extraData', 'type': 'bytes'}], 'name': 'approveAndCall', 'outputs': [{'name': 'success', 'type': 'bool'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'function'}, {'constant': true, 'inputs': [{'name': '_owner', 'type': 'address'}, {'name': '_spender', 'type': 'address'}], 'name': 'allowance', 'outputs': [{'name': 'remaining', 'type': 'uint256'}], 'payable': false, 'stateMutability': 'view', 'type': 'function'}, {'inputs': [{'name': '_initialAmount', 'type': 'uint256'}, {'name': '_tokenName', 'type': 'string'}, {'name': '_decimalUnits', 'type': 'uint8'}, {'name': '_tokenSymbol', 'type': 'string'}], 'payable': false, 'stateMutability': 'nonpayable', 'type': 'constructor'}, {'payable': false, 'stateMutability': 'nonpayable', 'type': 'fallback'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': '_from', 'type': 'address'}, {'indexed': true, 'name': '_to', 'type': 'address'}, {'indexed': false, 'name': '_value', 'type': 'uint256'}], 'name': 'Transfer', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': '_owner', 'type': 'address'}, {'indexed': true, 'name': '_spender', 'type': 'address'}, {'indexed': false, 'name': '_value', 'type': 'uint256'}], 'name': 'Approval', 'type': 'event'}]) - document.getElementById('tokenAddress').innerHTML = contract.address + return humanstandardtokenContract.new( + _initialAmount, + _tokenName, + _decimalUnits, + _tokenSymbol, + { + from: accounts[0], + data: '0x60806040523480156200001157600080fd5b506040516200156638038062001566833981018060405260808110156200003757600080fd5b8101908080516401000000008111156200005057600080fd5b828101905060208101848111156200006757600080fd5b81518560018202830111640100000000821117156200008557600080fd5b50509291906020018051640100000000811115620000a257600080fd5b82810190506020810184811115620000b957600080fd5b8151856001820283011164010000000082111715620000d757600080fd5b5050929190602001805190602001909291908051906020019092919050505083838382600390805190602001906200011192919062000305565b5081600490805190602001906200012a92919062000305565b5080600560006101000a81548160ff021916908360ff1602179055505050506200016433826200016e640100000000026401000000009004565b50505050620003b4565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1614151515620001ab57600080fd5b620001d081600254620002e36401000000000262001155179091906401000000009004565b60028190555062000237816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054620002e36401000000000262001155179091906401000000009004565b6000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508173ffffffffffffffffffffffffffffffffffffffff16600073ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040518082815260200191505060405180910390a35050565b6000808284019050838110151515620002fb57600080fd5b8091505092915050565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106200034857805160ff191683800117855562000379565b8280016001018555821562000379579182015b82811115620003785782518255916020019190600101906200035b565b5b5090506200038891906200038c565b5090565b620003b191905b80821115620003ad57600081600090555060010162000393565b5090565b90565b6111a280620003c46000396000f3fe6080604052600436106100af576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806306fdde03146100b4578063095ea7b31461014457806318160ddd146101b757806323b872dd146101e2578063313ce5671461027557806339509351146102a657806370a082311461031957806395d89b411461037e578063a457c2d71461040e578063a9059cbb14610481578063dd62ed3e146104f4575b600080fd5b3480156100c057600080fd5b506100c9610579565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156101095780820151818401526020810190506100ee565b50505050905090810190601f1680156101365780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561015057600080fd5b5061019d6004803603604081101561016757600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291908035906020019092919050505061061b565b604051808215151515815260200191505060405180910390f35b3480156101c357600080fd5b506101cc610748565b6040518082815260200191505060405180910390f35b3480156101ee57600080fd5b5061025b6004803603606081101561020557600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610752565b604051808215151515815260200191505060405180910390f35b34801561028157600080fd5b5061028a61095a565b604051808260ff1660ff16815260200191505060405180910390f35b3480156102b257600080fd5b506102ff600480360360408110156102c957600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610971565b604051808215151515815260200191505060405180910390f35b34801561032557600080fd5b506103686004803603602081101561033c57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610ba8565b6040518082815260200191505060405180910390f35b34801561038a57600080fd5b50610393610bf0565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156103d35780820151818401526020810190506103b8565b50505050905090810190601f1680156104005780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561041a57600080fd5b506104676004803603604081101561043157600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610c92565b604051808215151515815260200191505060405180910390f35b34801561048d57600080fd5b506104da600480360360408110156104a457600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610ec9565b604051808215151515815260200191505060405180910390f35b34801561050057600080fd5b506105636004803603604081101561051757600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610ee0565b6040518082815260200191505060405180910390f35b606060038054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156106115780601f106105e657610100808354040283529160200191610611565b820191906000526020600020905b8154815290600101906020018083116105f457829003601f168201915b5050505050905090565b60008073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff161415151561065857600080fd5b81600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040518082815260200191505060405180910390a36001905092915050565b6000600254905090565b60006107e382600160008773ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054610f6790919063ffffffff16565b600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000208190555061086e848484610f89565b3373ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925600160008873ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020546040518082815260200191505060405180910390a3600190509392505050565b6000600560009054906101000a900460ff16905090565b60008073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16141515156109ae57600080fd5b610a3d82600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205461115590919063ffffffff16565b600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008773ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020546040518082815260200191505060405180910390a36001905092915050565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b606060048054600181600116156101000203166002900480601f016020809104026020016040519081016040528092919081815260200182805460018160011615610100020316600290048015610c885780601f10610c5d57610100808354040283529160200191610c88565b820191906000526020600020905b815481529060010190602001808311610c6b57829003601f168201915b5050505050905090565b60008073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1614151515610ccf57600080fd5b610d5e82600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054610f6790919063ffffffff16565b600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008773ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020546040518082815260200191505060405180910390a36001905092915050565b6000610ed6338484610f89565b6001905092915050565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905092915050565b6000828211151515610f7857600080fd5b600082840390508091505092915050565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1614151515610fc557600080fd5b611016816000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054610f6790919063ffffffff16565b6000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055506110a9816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205461115590919063ffffffff16565b6000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040518082815260200191505060405180910390a3505050565b600080828401905083811015151561116c57600080fd5b809150509291505056fea165627a7a723058205fcdfea06f4d97b442bc9f444b1e92524bc66398eb4f37ed5a99f2093a8842640029000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000186a00000000000000000000000000000000000000000000000000000000000000003545354000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035453540000000000000000000000000000000000000000000000000000000000', + gas: '4700000', + gasPrice: '20000000000', + }, (error, contract) => { + if (error) { + tokenAddress.innerHTML = 'Creation Failed' + throw error + } else if (contract.address === undefined) { + return + } - transferTokens.addEventListener('click', function (event) { + console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash) + tokenAddress.innerHTML = contract.address + transferTokens.disabled = false + approveTokens.disabled = false + transferTokensWithoutGas.disabled = false + approveTokensWithoutGas.disabled = false + + transferTokens.onclick = (event) => { console.log(`event`, event) contract.transfer('0x2f318C334780961FB129D2a6c30D0763d9a5C970', '15000', { - from: web3.eth.accounts[0], + from: accounts[0], to: contract.address, data: '0xa9059cbb0000000000000000000000002f318c334780961fb129d2a6c30d0763d9a5c9700000000000000000000000000000000000000000000000000000000000003a98', gas: 60000, gasPrice: '20000000000', - }, function (result) { + }, (result) => { console.log('result', result) }) - }) + } - approveTokens.addEventListener('click', function () { + approveTokens.onclick = () => { contract.approve('0x9bc5baF874d2DA8D216aE9f137804184EE5AfEF4', '70000', { - from: web3.eth.accounts[0], + from: accounts[0], to: contract.address, data: '0x095ea7b30000000000000000000000009bc5baF874d2DA8D216aE9f137804184EE5AfEF40000000000000000000000000000000000000000000000000000000000000005', gas: 60000, gasPrice: '20000000000', - }, function (result) { + }, (result) => { console.log(result) }) - }) + } - transferTokensWithoutGas.addEventListener('click', function (event) { + transferTokensWithoutGas.onclick = (event) => { console.log(`event`, event) contract.transfer('0x2f318C334780961FB129D2a6c30D0763d9a5C970', '15000', { - from: web3.eth.accounts[0], + from: accounts[0], to: contract.address, data: '0xa9059cbb0000000000000000000000002f318c334780961fb129d2a6c30d0763d9a5c9700000000000000000000000000000000000000000000000000000000000003a98', gasPrice: '20000000000', - }, function (result) { + }, (result) => { console.log('result', result) }) - }) + } - approveTokensWithoutGas.addEventListener('click', function () { + approveTokensWithoutGas.onclick = () => { contract.approve('0x2f318C334780961FB129D2a6c30D0763d9a5C970', '70000', { - from: web3.eth.accounts[0], + from: accounts[0], to: contract.address, data: '0x095ea7b30000000000000000000000002f318C334780961FB129D2a6c30D0763d9a5C9700000000000000000000000000000000000000000000000000000000000000005', gasPrice: '20000000000', - }, function (result) { + }, (result) => { console.log(result) }) - }) + } } - }) - - }) + ) + } - ethereum.autoRefreshOnNetworkChange = false - - const networkDiv = document.getElementById('network') - const chainIdDiv = document.getElementById('chainId') - const accountsDiv = document.getElementById('accounts') - - ethereum.on('networkChanged', (networkId) => { - networkDiv.innerHTML = networkId - }) + signTypedData.addEventListener('click', () => { + const typedData = { + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + Person: [ + { name: 'name', type: 'string' }, + { name: 'wallet', type: 'address' }, + ], + Mail: [ + { name: 'from', type: 'Person' }, + { name: 'to', type: 'Person' }, + { name: 'contents', type: 'string' }, + ], + }, + primaryType: 'Mail', + domain: { + name: 'Ether Mail', + version: '1', + chainId: 3, + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + }, + message: { + sender: { + name: 'Cow', + wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', + }, + recipient: { + name: 'Bob', + wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + }, + contents: 'Hello, Bob!', + }, + } + web3.currentProvider.sendAsync({ + method: 'eth_signTypedData_v3', + params: [ethereum.selectedAddress, JSON.stringify(typedData)], + from: ethereum.selectedAddress, + }, (err, result) => { + if (err) { + console.log(err) + } else { + signTypedDataResults.innerHTML = result + } + }) + }) - ethereum.on('chainIdChanged', (chainId) => { - chainIdDiv.innerHTML = chainId - }) + } - ethereum.on('accountsChanged', (accounts) => { - accountsDiv.innerHTML = accounts - }) -}) + updateButtons() + if (isMetaMaskInstalled()) { + ethereum.autoRefreshOnNetworkChange = false + ethereum.on('networkChanged', (networkId) => { + networkDiv.innerHTML = networkId + }) + ethereum.on('chainIdChanged', (chainId) => { + chainIdDiv.innerHTML = chainId + }) + ethereum.on('accountsChanged', (newAccounts) => { + const connecting = Boolean((!accounts || !accounts.length) && newAccounts && newAccounts.length) + accounts = newAccounts + accountsDiv.innerHTML = accounts + if (connecting) { + initializeAccountButtons() + } + updateButtons() + }) + } +} +window.addEventListener('DOMContentLoaded', initialize) diff --git a/test/e2e/contract-test/index.html b/test/e2e/contract-test/index.html index 03792de76..9454a67dd 100644 --- a/test/e2e/contract-test/index.html +++ b/test/e2e/contract-test/index.html @@ -1,44 +1,64 @@ + E2E Test Dapp + + -
-
Contract
-
- - - -
-
- Not clicked -
-
-
-
Send eth
-
+
+

E2E Test Dapp

+
+
+
+

Connect

+ +
+
+

Contract

+
+ + + +
+
+ Contract Status: Not clicked +
+
+
+

Send Eth

-
-
-
-
Send tokens
-
-
- - - - - -
-
-
-
Network:
-
ChainId:
-
Accounts:
-
-
- - + +
+

Send Tokens

+
+ Token: +
+
+ + + + + +
+
+
+

Status

+
+ Network: +
+
+ ChainId: +
+
+ Accounts: +
+
+
+

Sign Typed Data

+ +
Sign Typed Data Result:
+
+ - - \ No newline at end of file + diff --git a/test/e2e/ethereum-on.spec.js b/test/e2e/ethereum-on.spec.js index 144da97a4..ca062ca26 100644 --- a/test/e2e/ethereum-on.spec.js +++ b/test/e2e/ethereum-on.spec.js @@ -7,7 +7,6 @@ const { const { checkBrowserForConsoleErrors, findElement, - findElements, openNewPage, verboseReportOnFailure, waitUntilXWindowHandles, @@ -15,6 +14,7 @@ const { setupFetchMocking, prepareExtensionForTesting, } = require('./helpers') +const enLocaleMessages = require('../../app/_locales/en/messages.json') describe('MetaMask', function () { let driver @@ -54,7 +54,7 @@ describe('MetaMask', function () { describe('Going through the first time flow, but skipping the seed phrase challenge', () => { it('clicks the continue button on the welcome screen', async () => { await findElement(driver, By.css('.welcome-page__header')) - const welcomeScreenBtn = await findElement(driver, By.css('.first-time-flow__button')) + const welcomeScreenBtn = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`)) welcomeScreenBtn.click() await delay(largeDelayMs) }) @@ -87,8 +87,8 @@ describe('MetaMask', function () { }) it('skips the seed phrase challenge', async () => { - const buttons = await findElements(driver, By.css('.first-time-flow__button')) - await buttons[0].click() + const button = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.remindMeLater.message}')]`)) + await button.click() await delay(regularDelayMs) const detailsButton = await findElement(driver, By.css('.account-details__details-button')) @@ -117,6 +117,11 @@ describe('MetaMask', function () { await openNewPage(driver, 'http://127.0.0.1:8080/') await delay(regularDelayMs) + const connectButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Connect')]`)) + await connectButton.click() + + await delay(regularDelayMs) + await waitUntilXWindowHandles(driver, 3) const windowHandles = await driver.getAllWindowHandles() @@ -132,9 +137,9 @@ describe('MetaMask', function () { await delay(regularDelayMs) }) - it('has not set the network within the dapp', async () => { + it('has the ganache network id within the dapp', async () => { const networkDiv = await findElement(driver, By.css('#network')) - assert.equal(await networkDiv.getText(), '') + assert.equal(await networkDiv.getText(), '5777') }) it('changes the network', async () => { diff --git a/test/e2e/from-import-ui.spec.js b/test/e2e/from-import-ui.spec.js index 3bc31734a..ce224e781 100644 --- a/test/e2e/from-import-ui.spec.js +++ b/test/e2e/from-import-ui.spec.js @@ -12,6 +12,7 @@ const { setupFetchMocking, prepareExtensionForTesting, } = require('./helpers') +const enLocaleMessages = require('../../app/_locales/en/messages.json') describe('Using MetaMask with an existing account', function () { let driver @@ -54,7 +55,7 @@ describe('Using MetaMask with an existing account', function () { describe('First time flow starting from an existing seed phrase', () => { it('clicks the continue button on the welcome screen', async () => { await findElement(driver, By.css('.welcome-page__header')) - const welcomeScreenBtn = await findElement(driver, By.css('.first-time-flow__button')) + const welcomeScreenBtn = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`)) welcomeScreenBtn.click() await delay(largeDelayMs) }) @@ -91,7 +92,7 @@ describe('Using MetaMask with an existing account', function () { it('clicks through the success screen', async () => { await findElement(driver, By.xpath(`//div[contains(text(), 'Congratulations')]`)) - const doneButton = await findElement(driver, By.css('button.first-time-flow__button')) + const doneButton = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`)) await doneButton.click() await delay(regularDelayMs) }) @@ -225,8 +226,10 @@ describe('Using MetaMask with an existing account', function () { }) it('finds the transaction in the transactions list', async function () { - const transactions = await findElements(driver, By.css('.transaction-list-item')) - assert.equal(transactions.length, 1) + await driver.wait(async () => { + const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) + return confirmedTxes.length === 1 + }, 10000) const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary')) assert.equal(txValues.length, 1) diff --git a/test/e2e/func.js b/test/e2e/func.js index dfad8466c..ab94f6231 100644 --- a/test/e2e/func.js +++ b/test/e2e/func.js @@ -91,7 +91,7 @@ async function getExtensionIdChrome (driver) { async function getExtensionIdFirefox (driver) { await driver.get('about:debugging#addons') - const extensionId = await driver.findElement(By.css('dd.addon-target-info-content:nth-child(6) > span:nth-child(1)')).getText() + const extensionId = await driver.wait(webdriver.until.elementLocated(By.xpath('//dl/div[contains(., \'Internal UUID\')]/dd')), 1000).getText() return extensionId } diff --git a/test/e2e/incremental-security.spec.js b/test/e2e/incremental-security.spec.js index f8d82abc3..d7ead799f 100644 --- a/test/e2e/incremental-security.spec.js +++ b/test/e2e/incremental-security.spec.js @@ -9,15 +9,14 @@ const { checkBrowserForConsoleErrors, findElement, findElements, - loadExtension, openNewPage, verboseReportOnFailure, setupFetchMocking, prepareExtensionForTesting, } = require('./helpers') +const enLocaleMessages = require('../../app/_locales/en/messages.json') describe('MetaMask', function () { - let extensionId let driver let publicAddress @@ -31,7 +30,6 @@ describe('MetaMask', function () { before(async function () { const result = await prepareExtensionForTesting() driver = result.driver - extensionId = result.extensionId await setupFetchMocking(driver) }) @@ -56,7 +54,7 @@ describe('MetaMask', function () { describe('Going through the first time flow, but skipping the seed phrase challenge', () => { it('clicks the continue button on the welcome screen', async () => { await findElement(driver, By.css('.welcome-page__header')) - const welcomeScreenBtn = await findElement(driver, By.css('.first-time-flow__button')) + const welcomeScreenBtn = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`)) welcomeScreenBtn.click() await delay(largeDelayMs) }) @@ -89,8 +87,8 @@ describe('MetaMask', function () { }) it('skips the seed phrase challenge', async () => { - const buttons = await findElements(driver, By.css('.first-time-flow__button')) - await buttons[0].click() + const button = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.remindMeLater.message}')]`)) + await button.click() await delay(regularDelayMs) const detailsButton = await findElement(driver, By.css('.account-details__details-button')) @@ -173,7 +171,7 @@ describe('MetaMask', function () { assert.equal(seedPhrase.split(' ').length, 12) await delay(regularDelayMs) - const nextScreen = (await findElements(driver, By.css('button.first-time-flow__button')))[1] + const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.next.message}')]`)) await nextScreen.click() await delay(regularDelayMs) }) @@ -186,37 +184,12 @@ describe('MetaMask', function () { await delay(tinyDelayMs) } - async function retypeSeedPhrase (words, wasReloaded, count = 0) { - try { - if (wasReloaded) { - const byRevealButton = By.css('.reveal-seed-phrase__secret-blocker .reveal-seed-phrase__reveal-button') - await driver.wait(until.elementLocated(byRevealButton, 10000)) - const revealSeedPhraseButton = await findElement(driver, byRevealButton, 10000) - await revealSeedPhraseButton.click() - await delay(regularDelayMs) - - const nextScreen = await findElement(driver, By.css('button.first-time-flow__button')) - await nextScreen.click() - await delay(regularDelayMs) - } - - for (let i = 0; i < 12; i++) { - await clickWordAndWait(words[i]) - } - } catch (e) { - if (count > 2) { - throw e - } else { - await loadExtension(driver, extensionId) - await retypeSeedPhrase(words, true, count + 1) - } - } - } - it('can retype the seed phrase', async () => { const words = seedPhrase.split(' ') - await retypeSeedPhrase(words) + for (const word of words) { + await clickWordAndWait(word) + } const confirm = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) await confirm.click() diff --git a/test/e2e/metamask-responsive-ui.spec.js b/test/e2e/metamask-responsive-ui.spec.js index a413aae5c..90cf35710 100644 --- a/test/e2e/metamask-responsive-ui.spec.js +++ b/test/e2e/metamask-responsive-ui.spec.js @@ -8,14 +8,13 @@ const { checkBrowserForConsoleErrors, findElement, findElements, - loadExtension, verboseReportOnFailure, setupFetchMocking, prepareExtensionForTesting, } = require('./helpers') +const enLocaleMessages = require('../../app/_locales/en/messages.json') describe('MetaMask', function () { - let extensionId let driver const testSeedPhrase = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent' @@ -29,7 +28,6 @@ describe('MetaMask', function () { before(async function () { const result = await prepareExtensionForTesting({ responsive: true }) driver = result.driver - extensionId = result.extensionId await setupFetchMocking(driver) }) @@ -54,7 +52,7 @@ describe('MetaMask', function () { describe('Going through the first time flow', () => { it('clicks the continue button on the welcome screen', async () => { await findElement(driver, By.css('.welcome-page__header')) - const welcomeScreenBtn = await findElement(driver, By.css('.first-time-flow__button')) + const welcomeScreenBtn = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`)) welcomeScreenBtn.click() await delay(largeDelayMs) }) @@ -99,7 +97,7 @@ describe('MetaMask', function () { assert.equal(seedPhrase.split(' ').length, 12) await delay(regularDelayMs) - const nextScreen = (await findElements(driver, By.css('button.first-time-flow__button')))[1] + const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.next.message}')]`)) await nextScreen.click() await delay(regularDelayMs) }) @@ -112,37 +110,12 @@ describe('MetaMask', function () { await delay(tinyDelayMs) } - async function retypeSeedPhrase (words, wasReloaded, count = 0) { - try { - if (wasReloaded) { - const byRevealButton = By.css('.reveal-seed-phrase__secret-blocker .reveal-seed-phrase__reveal-button') - await driver.wait(until.elementLocated(byRevealButton, 10000)) - const revealSeedPhraseButton = await findElement(driver, byRevealButton, 10000) - await revealSeedPhraseButton.click() - await delay(regularDelayMs) - - const nextScreen = await findElement(driver, By.css('button.first-time-flow__button')) - await nextScreen.click() - await delay(regularDelayMs) - } - - for (let i = 0; i < 12; i++) { - await clickWordAndWait(words[i]) - } - } catch (e) { - if (count > 2) { - throw e - } else { - await loadExtension(driver, extensionId) - await retypeSeedPhrase(words, true, count + 1) - } - } - } - it('can retype the seed phrase', async () => { const words = seedPhrase.split(' ') - await retypeSeedPhrase(words) + for (const word of words) { + await clickWordAndWait(word) + } const confirm = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) await confirm.click() @@ -151,7 +124,7 @@ describe('MetaMask', function () { it('clicks through the success screen', async () => { await findElement(driver, By.xpath(`//div[contains(text(), 'Congratulations')]`)) - const doneButton = await findElement(driver, By.css('button.first-time-flow__button')) + const doneButton = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`)) await doneButton.click() await delay(regularDelayMs) }) @@ -192,7 +165,7 @@ describe('MetaMask', function () { await passwordInputs[0].sendKeys('correct horse battery staple') await passwordInputs[1].sendKeys('correct horse battery staple') - await driver.findElement(By.css('.first-time-flow__button')).click() + await driver.findElement(By.xpath(`//button[contains(text(), '${enLocaleMessages.restore.message}')]`)).click() await delay(regularDelayMs) }) @@ -258,13 +231,13 @@ describe('MetaMask', function () { }) it('finds the transaction in the transactions list', async function () { - const transactions = await findElements(driver, By.css('.transaction-list-item')) - assert.equal(transactions.length, 1) + await driver.wait(async () => { + const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) + return confirmedTxes.length === 1 + }, 10000) - if (process.env.SELENIUM_BROWSER !== 'firefox') { - const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary')) - await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000) - } + const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary')) + await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000) }) }) }) diff --git a/test/e2e/metamask-ui.spec.js b/test/e2e/metamask-ui.spec.js index 664cc4b6b..b5a8220b0 100644 --- a/test/e2e/metamask-ui.spec.js +++ b/test/e2e/metamask-ui.spec.js @@ -10,7 +10,6 @@ const { closeAllWindowHandlesExcept, findElement, findElements, - loadExtension, openNewPage, switchToWindowWithTitle, verboseReportOnFailure, @@ -18,9 +17,9 @@ const { setupFetchMocking, prepareExtensionForTesting, } = require('./helpers') +const enLocaleMessages = require('../../app/_locales/en/messages.json') describe('MetaMask', function () { - let extensionId let driver let tokenAddress @@ -35,7 +34,6 @@ describe('MetaMask', function () { before(async function () { const result = await prepareExtensionForTesting() driver = result.driver - extensionId = result.extensionId await setupFetchMocking(driver) }) @@ -60,7 +58,7 @@ describe('MetaMask', function () { describe('Going through the first time flow', () => { it('clicks the continue button on the welcome screen', async () => { await findElement(driver, By.css('.welcome-page__header')) - const welcomeScreenBtn = await findElement(driver, By.css('.first-time-flow__button')) + const welcomeScreenBtn = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`)) welcomeScreenBtn.click() await delay(largeDelayMs) }) @@ -105,7 +103,7 @@ describe('MetaMask', function () { assert.equal(seedPhrase.split(' ').length, 12) await delay(regularDelayMs) - const nextScreen = (await findElements(driver, By.css('button.first-time-flow__button')))[1] + const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.next.message}')]`)) await nextScreen.click() await delay(regularDelayMs) }) @@ -118,37 +116,12 @@ describe('MetaMask', function () { await delay(tinyDelayMs) } - async function retypeSeedPhrase (words, wasReloaded, count = 0) { - try { - if (wasReloaded) { - const byRevealButton = By.css('.reveal-seed-phrase__secret-blocker .reveal-seed-phrase__reveal-button') - await driver.wait(until.elementLocated(byRevealButton, 10000)) - const revealSeedPhraseButton = await findElement(driver, byRevealButton, 10000) - await revealSeedPhraseButton.click() - await delay(regularDelayMs) - - const nextScreen = await findElement(driver, By.css('button.first-time-flow__button')) - await nextScreen.click() - await delay(regularDelayMs) - } - - for (let i = 0; i < 12; i++) { - await clickWordAndWait(words[i]) - } - } catch (e) { - if (count > 2) { - throw e - } else { - await loadExtension(driver, extensionId) - await retypeSeedPhrase(words, true, count + 1) - } - } - } - it('can retype the seed phrase', async () => { const words = seedPhrase.split(' ') - await retypeSeedPhrase(words) + for (const word of words) { + await clickWordAndWait(word) + } const confirm = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) await confirm.click() @@ -157,7 +130,7 @@ describe('MetaMask', function () { it('clicks through the success screen', async () => { await findElement(driver, By.xpath(`//div[contains(text(), 'Congratulations')]`)) - const doneButton = await findElement(driver, By.css('button.first-time-flow__button')) + const doneButton = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`)) await doneButton.click() await delay(regularDelayMs) }) @@ -249,7 +222,7 @@ describe('MetaMask', function () { await passwordInputs[0].sendKeys('correct horse battery staple') await passwordInputs[1].sendKeys('correct horse battery staple') - await driver.findElement(By.css('.first-time-flow__button')).click() + await driver.findElement(By.xpath(`//button[contains(text(), '${enLocaleMessages.restore.message}')]`)).click() await delay(regularDelayMs) }) @@ -316,13 +289,13 @@ describe('MetaMask', function () { }) it('finds the transaction in the transactions list', async function () { - const transactions = await findElements(driver, By.css('.transaction-list-item')) - assert.equal(transactions.length, 1) + await driver.wait(async () => { + const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) + return confirmedTxes.length === 1 + }, 10000) - if (process.env.SELENIUM_BROWSER !== 'firefox') { - const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary')) - await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000) - } + const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary')) + await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000) }) }) @@ -359,13 +332,13 @@ describe('MetaMask', function () { }) it('finds the transaction in the transactions list', async function () { - const transactions = await findElements(driver, By.css('.transaction-list-item')) - assert.equal(transactions.length, 2) + await driver.wait(async () => { + const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) + return confirmedTxes.length === 2 + }, 10000) - if (process.env.SELENIUM_BROWSER !== 'firefox') { - const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary')) - await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000) - } + const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary')) + await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000) }) }) @@ -412,13 +385,13 @@ describe('MetaMask', function () { }) it('finds the transaction in the transactions list', async function () { - const transactions = await findElements(driver, By.css('.transaction-list-item')) - assert.equal(transactions.length, 3) + await driver.wait(async () => { + const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) + return confirmedTxes.length === 3 + }, 10000) - if (process.env.SELENIUM_BROWSER !== 'firefox') { - const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary')) - await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000) - } + const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary')) + await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000) }) }) @@ -463,6 +436,11 @@ describe('MetaMask', function () { await openNewPage(driver, 'http://127.0.0.1:8080/') await delay(regularDelayMs) + const connectButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Connect')]`)) + await connectButton.click() + + await delay(regularDelayMs) + await waitUntilXWindowHandles(driver, 3) windowHandles = await driver.getAllWindowHandles() @@ -475,13 +453,13 @@ describe('MetaMask', function () { await approveButton.click() await driver.switchTo().window(dapp) - await delay(regularDelayMs) + await delay(2000) }) it('initiates a send from the dapp', async () => { const send3eth = await findElement(driver, By.xpath(`//button[contains(text(), 'Send')]`), 10000) await send3eth.click() - await delay(5000) + await delay(2000) windowHandles = await driver.getAllWindowHandles() await switchToWindowWithTitle(driver, 'MetaMask Notification', windowHandles) @@ -494,8 +472,6 @@ describe('MetaMask', function () { 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') @@ -504,9 +480,9 @@ describe('MetaMask', function () { await delay(50) await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'a')) await delay(50) - await gasLimitInput.sendKeys('25000') - await delay(largeDelayMs * 2) + + await delay(1000) const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`), 10000) await confirmButton.click() @@ -563,19 +539,20 @@ describe('MetaMask', function () { const dapp = windowHandles[1] await driver.switchTo().window(dapp) - await delay(regularDelayMs) + await delay(largeDelayMs) const send3eth = await findElement(driver, By.xpath(`//button[contains(text(), 'Send')]`), 10000) await send3eth.click() - await delay(regularDelayMs) + await delay(largeDelayMs) const contractDeployment = await findElement(driver, By.xpath(`//button[contains(text(), 'Deploy Contract')]`), 10000) await contractDeployment.click() - await delay(regularDelayMs) + await delay(largeDelayMs) await send3eth.click() + await delay(largeDelayMs) await contractDeployment.click() - await delay(regularDelayMs) + await delay(largeDelayMs) await driver.switchTo().window(extension) await delay(regularDelayMs) @@ -791,14 +768,12 @@ describe('MetaMask', function () { await modalTabs[1].click() await delay(regularDelayMs) - const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-tab__gas-edit-row__input')) + const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-gas-inputs__gas-edit-row__input')) const gasLimitValue = await gasLimitInput.getAttribute('value') assert(Number(gasLimitValue) < 100000, 'Gas Limit too high') 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') @@ -807,18 +782,9 @@ describe('MetaMask', function () { 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) + + await delay(1000) const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`)) await save.click() @@ -877,12 +843,10 @@ describe('MetaMask', function () { it('renders the correct ETH balance', async () => { const balance = await findElement(driver, By.css('.transaction-view-balance__primary-balance')) await delay(regularDelayMs) - if (process.env.SELENIUM_BROWSER !== 'firefox') { - await driver.wait(until.elementTextMatches(balance, /^87.*\s*ETH.*$/), 10000) - const tokenAmount = await balance.getText() - assert.ok(/^87.*\s*ETH.*$/.test(tokenAmount)) - await delay(regularDelayMs) - } + await driver.wait(until.elementTextMatches(balance, /^87.*\s*ETH.*$/), 10000) + const tokenAmount = await balance.getText() + assert.ok(/^87.*\s*ETH.*$/.test(tokenAmount)) + await delay(regularDelayMs) }) }) @@ -913,7 +877,7 @@ describe('MetaMask', function () { await advancedTabButton.click() await delay(tinyDelayMs) - const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-tab__gas-edit-row__input')) + const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-gas-inputs__gas-edit-row__input')) assert(gasPriceInput.getAttribute('value'), 20) assert(gasLimitInput.getAttribute('value'), 4700000) @@ -1041,22 +1005,15 @@ describe('MetaMask', function () { }) it('finds the transaction in the transactions list', async function () { - const transactions = await findElements(driver, By.css('.transaction-list-item')) - assert.equal(transactions.length, 1) - - const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary')) - assert.equal(txValues.length, 1) - - // test cancelled on firefox until https://github.com/mozilla/geckodriver/issues/906 is resolved, - // or possibly until we use latest version of firefox in the tests - if (process.env.SELENIUM_BROWSER !== 'firefox') { - await driver.wait(until.elementTextMatches(txValues[0], /-1\s*TST/), 10000) - } - await driver.wait(async () => { const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) return confirmedTxes.length === 1 }, 10000) + + const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary')) + assert.equal(txValues.length, 1) + await driver.wait(until.elementTextMatches(txValues[0], /-1\s*TST/), 10000) + const txStatuses = await findElements(driver, By.css('.transaction-list-item__action')) await driver.wait(until.elementTextMatches(txStatuses[0], /Sent\sToken/i), 10000) }) @@ -1102,12 +1059,10 @@ describe('MetaMask', function () { await modalTabs[1].click() await delay(regularDelayMs) - const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-tab__gas-edit-row__input')) + const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-gas-inputs__gas-edit-row__input')) 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') @@ -1116,18 +1071,9 @@ describe('MetaMask', function () { 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('60000') - await delay(50) - await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'e')) - await delay(50) + + await delay(1000) const save = await findElement(driver, By.css('.page-container__footer-button')) await save.click() @@ -1154,7 +1100,6 @@ describe('MetaMask', function () { return confirmedTxes.length === 2 }, 10000) - await delay(regularDelayMs) const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary')) await driver.wait(until.elementTextMatches(txValues[0], /-1.5\s*TST/)) const txStatuses = await findElements(driver, By.css('.transaction-list-item__action')) @@ -1165,14 +1110,10 @@ describe('MetaMask', function () { const tokenListItems = await findElements(driver, By.css('.token-list-item')) await tokenListItems[0].click() - await delay(regularDelayMs) + await delay(1000) - // test cancelled on firefox until https://github.com/mozilla/geckodriver/issues/906 is resolved, - // or possibly until we use latest version of firefox in the tests - if (process.env.SELENIUM_BROWSER !== 'firefox') { - const tokenBalanceAmount = await findElements(driver, By.css('.transaction-view-balance__primary-balance')) - await driver.wait(until.elementTextMatches(tokenBalanceAmount[0], /7.500\s*TST/), 10000) - } + const tokenBalanceAmount = await findElements(driver, By.css('.transaction-view-balance__primary-balance')) + await driver.wait(until.elementTextMatches(tokenBalanceAmount[0], /7.500\s*TST/), 10000) }) }) @@ -1188,12 +1129,9 @@ describe('MetaMask', function () { await driver.switchTo().window(dapp) await delay(tinyDelayMs) - const transferTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Approve Tokens')]`)) - await transferTokens.click() + const approveTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Approve Tokens')]`)) + await approveTokens.click() - if (process.env.SELENIUM_BROWSER !== 'firefox') { - await closeAllWindowHandlesExcept(driver, [extension, dapp]) - } await driver.switchTo().window(extension) await delay(regularDelayMs) @@ -1210,31 +1148,22 @@ describe('MetaMask', function () { }) it('displays the token approval data', async () => { - const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`)) - dataTab.click() + const fullTxDataButton = await findElement(driver, By.css('.confirm-approve-content__view-full-tx-button')) + await fullTxDataButton.click() await delay(regularDelayMs) - const functionType = await findElement(driver, By.css('.confirm-page-container-content__function-type')) + const functionType = await findElement(driver, By.css('.confirm-approve-content__data .confirm-approve-content__small-text')) const functionTypeText = await functionType.getText() - assert.equal(functionTypeText, 'Approve') + assert.equal(functionTypeText, 'Function: Approve') - const confirmDataDiv = await findElement(driver, By.css('.confirm-page-container-content__data-box')) + const confirmDataDiv = await findElement(driver, By.css('.confirm-approve-content__data__data-block')) const confirmDataText = await confirmDataDiv.getText() assert(confirmDataText.match(/0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef4/)) - - const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`)) - detailsTab.click() - await delay(regularDelayMs) - - const approvalWarning = await findElement(driver, By.css('.confirm-page-container-warning__warning')) - const approvalWarningText = await approvalWarning.getText() - assert(approvalWarningText.match(/By approving this/)) - await delay(regularDelayMs) }) it('opens the gas edit modal', async () => { - const configureGas = await driver.wait(until.elementLocated(By.css('.confirm-detail-row__header-text--edit'))) - await configureGas.click() + const editButtons = await findElements(driver, By.css('.confirm-approve-content__small-blue-text.cursor-pointer')) + await editButtons[0].click() await delay(regularDelayMs) gasModal = await driver.findElement(By.css('span .modal')) @@ -1245,12 +1174,10 @@ describe('MetaMask', function () { await modalTabs[1].click() await delay(regularDelayMs) - const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-tab__gas-edit-row__input')) + const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-gas-inputs__gas-edit-row__input')) 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') @@ -1259,31 +1186,42 @@ describe('MetaMask', function () { 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) + + await delay(1000) const save = await findElement(driver, By.css('.page-container__footer-button')) await save.click() await driver.wait(until.stalenessOf(gasModal)) - const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__primary')) - assert.equal(await gasFeeInputs[0].getText(), '0.0006') + const gasFeeInEth = await findElement(driver, By.css('.confirm-approve-content__transaction-details-content__secondary-fee')) + assert.equal(await gasFeeInEth.getText(), '0.0006 ETH') }) - it('shows the correct recipient', async function () { - const senderToRecipientDivs = await findElements(driver, By.css('.sender-to-recipient__name')) - const recipientDiv = senderToRecipientDivs[1] - assert.equal(await recipientDiv.getText(), '0x9bc5...fEF4') + it('edits the permission', async () => { + const editButtons = await findElements(driver, By.css('.confirm-approve-content__small-blue-text.cursor-pointer')) + await editButtons[1].click() + await delay(regularDelayMs) + + const permissionModal = await driver.findElement(By.css('span .modal')) + + const radioButtons = await findElements(driver, By.css('.edit-approval-permission__edit-section__radio-button')) + await radioButtons[1].click() + + const customInput = await findElement(driver, By.css('input')) + await delay(50) + await customInput.sendKeys('5') + await delay(regularDelayMs) + + const saveButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`)) + await saveButton.click() + await delay(regularDelayMs) + + await driver.wait(until.stalenessOf(permissionModal)) + + const permissionInfo = await findElements(driver, By.css('.confirm-approve-content__medium-text')) + const amountDiv = permissionInfo[0] + assert.equal(await amountDiv.getText(), '5 TST') }) it('submits the transaction', async function () { @@ -1293,29 +1231,19 @@ describe('MetaMask', function () { }) it('finds the transaction in the transactions list', async function () { - if (process.env.SELENIUM_BROWSER === 'firefox') { - this.skip() - } - await driver.wait(async () => { const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) return confirmedTxes.length === 3 }, 10000) const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary')) - await driver.wait(until.elementTextMatches(txValues[0], /-7\s*TST/)) + await driver.wait(until.elementTextMatches(txValues[0], /-5\s*TST/)) const txStatuses = await findElements(driver, By.css('.transaction-list-item__action')) await driver.wait(until.elementTextMatches(txStatuses[0], /Approve/)) }) }) describe('Tranfers a custom token from dapp when no gas value is specified', () => { - before(function () { - if (process.env.SELENIUM_BROWSER === 'firefox') { - this.skip() - } - }) - it('transfers an already created token, without specifying gas', async () => { const windowHandles = await driver.getAllWindowHandles() const extension = windowHandles[0] @@ -1328,7 +1256,6 @@ describe('MetaMask', function () { 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) @@ -1365,12 +1292,6 @@ describe('MetaMask', function () { }) describe('Approves a custom token from dapp when no gas value is specified', () => { - before(function () { - if (process.env.SELENIUM_BROWSER === 'firefox') { - this.skip() - } - }) - it('approves an already created token', async () => { const windowHandles = await driver.getAllWindowHandles() const extension = windowHandles[0] @@ -1384,7 +1305,6 @@ describe('MetaMask', function () { 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) @@ -1401,13 +1321,17 @@ describe('MetaMask', function () { }) it('shows the correct recipient', async function () { - const senderToRecipientDivs = await findElements(driver, By.css('.sender-to-recipient__name')) - const recipientDiv = senderToRecipientDivs[1] - assert.equal(await recipientDiv.getText(), 'Account 2') + const fullTxDataButton = await findElement(driver, By.css('.confirm-approve-content__view-full-tx-button')) + await fullTxDataButton.click() + await delay(regularDelayMs) + + const permissionInfo = await findElements(driver, By.css('.confirm-approve-content__medium-text')) + const recipientDiv = permissionInfo[1] + assert.equal(await recipientDiv.getText(), '0x2f318C33...C970') }) it('submits the transaction', async function () { - await delay(regularDelayMs) + await delay(1000) const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`)) await confirmButton.click() await delay(regularDelayMs) diff --git a/test/e2e/run-all.sh b/test/e2e/run-all.sh index 08c090da4..33c2428da 100755 --- a/test/e2e/run-all.sh +++ b/test/e2e/run-all.sh @@ -29,6 +29,14 @@ concurrently --kill-others \ 'yarn dapp' \ 'sleep 5 && mocha test/e2e/metamask-responsive-ui.spec' +concurrently --kill-others \ + --names 'ganache,dapp,e2e' \ + --prefix '[{time}][{name}]' \ + --success first \ + 'yarn ganache:start' \ + 'yarn dapp' \ + 'sleep 5 && mocha test/e2e/signature-request.spec' + export GANACHE_ARGS="${BASE_GANACHE_ARGS} --deterministic --account=0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9,25000000000000000000" concurrently --kill-others \ --names 'ganache,e2e' \ @@ -37,7 +45,6 @@ concurrently --kill-others \ 'yarn ganache:start' \ 'sleep 5 && mocha test/e2e/from-import-ui.spec' -export GANACHE_ARGS="${BASE_GANACHE_ARGS} --deterministic --account=0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9,25000000000000000000" concurrently --kill-others \ --names 'ganache,e2e' \ --prefix '[{time}][{name}]' \ @@ -45,14 +52,13 @@ concurrently --kill-others \ 'npm run ganache:start' \ 'sleep 5 && mocha test/e2e/send-edit.spec' - - concurrently --kill-others \ - --names 'ganache,dapp,e2e' \ - --prefix '[{time}][{name}]' \ - --success first \ - 'yarn ganache:start' \ - 'yarn dapp' \ - 'sleep 5 && mocha test/e2e/ethereum-on.spec' +concurrently --kill-others \ + --names 'ganache,dapp,e2e' \ + --prefix '[{time}][{name}]' \ + --success first \ + 'yarn ganache:start' \ + 'yarn dapp' \ + 'sleep 5 && mocha test/e2e/ethereum-on.spec' export GANACHE_ARGS="${BASE_GANACHE_ARGS} --deterministic --account=0x250F458997A364988956409A164BA4E16F0F99F916ACDD73ADCD3A1DE30CF8D1,0 --account=0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9,25000000000000000000" concurrently --kill-others \ @@ -73,12 +79,11 @@ concurrently --kill-others \ 'sleep 5 && mocha test/e2e/address-book.spec' export GANACHE_ARGS="${BASE_GANACHE_ARGS} --deterministic --account=0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9,25000000000000000000" - concurrently --kill-others \ - --names 'ganache,dapp,e2e' \ - --prefix '[{time}][{name}]' \ - --success first \ - 'node test/e2e/mock-3box/server.js' \ - 'yarn ganache:start' \ - 'yarn dapp' \ - 'sleep 5 && mocha test/e2e/threebox.spec' - \ No newline at end of file +concurrently --kill-others \ + --names 'ganache,dapp,e2e' \ + --prefix '[{time}][{name}]' \ + --success first \ + 'node test/e2e/mock-3box/server.js' \ + 'yarn ganache:start' \ + 'yarn dapp' \ + 'sleep 5 && mocha test/e2e/threebox.spec' diff --git a/test/e2e/run-web3.sh b/test/e2e/run-web3.sh index 174370683..729333b84 100755 --- a/test/e2e/run-web3.sh +++ b/test/e2e/run-web3.sh @@ -9,5 +9,5 @@ export PATH="$PATH:./node_modules/.bin" concurrently --kill-others \ --names 'dapp,e2e' \ --prefix '[{time}][{name}]' \ - 'static-server test/web3 --port 8080' \ + 'node development/static-server.js test/web3 --port 8080' \ 'sleep 5 && mocha test/e2e/web3.spec' diff --git a/test/e2e/send-edit.spec.js b/test/e2e/send-edit.spec.js index 3f12aaaf2..d29245e3e 100644 --- a/test/e2e/send-edit.spec.js +++ b/test/e2e/send-edit.spec.js @@ -12,6 +12,7 @@ const { setupFetchMocking, prepareExtensionForTesting, } = require('./helpers') +const enLocaleMessages = require('../../app/_locales/en/messages.json') describe('Using MetaMask with an existing account', function () { let driver @@ -51,7 +52,7 @@ describe('Using MetaMask with an existing account', function () { describe('First time flow starting from an existing seed phrase', () => { it('clicks the continue button on the welcome screen', async () => { await findElement(driver, By.css('.welcome-page__header')) - const welcomeScreenBtn = await findElement(driver, By.css('.first-time-flow__button')) + const welcomeScreenBtn = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`)) welcomeScreenBtn.click() await delay(largeDelayMs) }) @@ -88,7 +89,7 @@ describe('Using MetaMask with an existing account', function () { it('clicks through the success screen', async () => { await findElement(driver, By.xpath(`//div[contains(text(), 'Congratulations')]`)) - const doneButton = await findElement(driver, By.css('button.first-time-flow__button')) + const doneButton = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`)) await doneButton.click() await delay(regularDelayMs) }) @@ -113,7 +114,7 @@ describe('Using MetaMask with an existing account', function () { const gasModal = await driver.findElement(By.css('span .modal')) - const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-tab__gas-edit-row__input')) + const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-gas-inputs__gas-edit-row__input')) await gasPriceInput.sendKeys(Key.chord(Key.CONTROL, 'a')) await delay(50) @@ -131,6 +132,8 @@ describe('Using MetaMask with an existing account', function () { await gasLimitInput.sendKeys('25000') + await delay(1000) + const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`)) await save.click() await driver.wait(until.stalenessOf(gasModal)) @@ -170,7 +173,7 @@ describe('Using MetaMask with an existing account', function () { const gasModal = await driver.findElement(By.css('span .modal')) - const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-tab__gas-edit-row__input')) + const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-gas-inputs__gas-edit-row__input')) await gasPriceInput.sendKeys(Key.chord(Key.CONTROL, 'a')) await delay(50) @@ -187,6 +190,8 @@ describe('Using MetaMask with an existing account', function () { await gasLimitInput.sendKeys('100000') + await delay(1000) + const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`)) await save.click() await driver.wait(until.stalenessOf(gasModal)) @@ -213,8 +218,10 @@ describe('Using MetaMask with an existing account', function () { }) it('finds the transaction in the transactions list', async function () { - const transactions = await findElements(driver, By.css('.transaction-list-item')) - assert.equal(transactions.length, 1) + await driver.wait(async () => { + const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item')) + return confirmedTxes.length === 1 + }, 10000) const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary')) assert.equal(txValues.length, 1) diff --git a/test/e2e/signature-request.spec.js b/test/e2e/signature-request.spec.js new file mode 100644 index 000000000..a5be61baf --- /dev/null +++ b/test/e2e/signature-request.spec.js @@ -0,0 +1,196 @@ +const assert = require('assert') +const webdriver = require('selenium-webdriver') +const { By, until } = webdriver +const { + delay, +} = require('./func') +const { + checkBrowserForConsoleErrors, + findElement, + findElements, + openNewPage, + verboseReportOnFailure, + waitUntilXWindowHandles, + switchToWindowWithTitle, + setupFetchMocking, + prepareExtensionForTesting, +} = require('./helpers') +const enLocaleMessages = require('../../app/_locales/en/messages.json') + +describe('MetaMask', function () { + let driver + let publicAddress + + const tinyDelayMs = 200 + const regularDelayMs = tinyDelayMs * 2 + const largeDelayMs = regularDelayMs * 2 + + this.timeout(0) + this.bail(true) + + before(async function () { + const result = await prepareExtensionForTesting() + driver = result.driver + await setupFetchMocking(driver) + }) + + afterEach(async function () { + if (process.env.SELENIUM_BROWSER === 'chrome') { + const errors = await checkBrowserForConsoleErrors(driver) + if (errors.length) { + const errorReports = errors.map(err => err.message) + const errorMessage = `Errors found in browser console:\n${errorReports.join('\n')}` + console.error(new Error(errorMessage)) + } + } + if (this.currentTest.state === 'failed') { + await verboseReportOnFailure(driver, this.currentTest) + } + }) + + after(async function () { + await driver.quit() + }) + + describe('Going through the first time flow, but skipping the seed phrase challenge', () => { + it('clicks the continue button on the welcome screen', async () => { + await findElement(driver, By.css('.welcome-page__header')) + const welcomeScreenBtn = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`)) + welcomeScreenBtn.click() + await delay(largeDelayMs) + }) + + it('clicks the "Create New Wallet" option', async () => { + const customRpcButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Create a Wallet')]`)) + customRpcButton.click() + await delay(largeDelayMs) + }) + + it('clicks the "No thanks" option on the metametrics opt-in screen', async () => { + const optOutButton = await findElement(driver, By.css('.btn-default')) + optOutButton.click() + await delay(largeDelayMs) + }) + + it('accepts a secure password', async () => { + const passwordBox = await findElement(driver, By.css('.first-time-flow__form #create-password')) + const passwordBoxConfirm = await findElement(driver, By.css('.first-time-flow__form #confirm-password')) + const button = await findElement(driver, By.css('.first-time-flow__form button')) + + await passwordBox.sendKeys('correct horse battery staple') + await passwordBoxConfirm.sendKeys('correct horse battery staple') + + const tosCheckBox = await findElement(driver, By.css('.first-time-flow__checkbox')) + await tosCheckBox.click() + + await button.click() + await delay(largeDelayMs) + }) + + it('skips the seed phrase challenge', async () => { + const button = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.remindMeLater.message}')]`)) + await button.click() + await delay(regularDelayMs) + + const detailsButton = await findElement(driver, By.css('.account-details__details-button')) + await detailsButton.click() + await delay(regularDelayMs) + }) + + it('gets the current accounts address', async () => { + const addressInput = await findElement(driver, By.css('.qr-ellip-address')) + publicAddress = (await addressInput.getAttribute('value')).toLowerCase() + const accountModal = await driver.findElement(By.css('span .modal')) + + await driver.executeScript("document.querySelector('.account-modal-close').click()") + + await driver.wait(until.stalenessOf(accountModal)) + await delay(regularDelayMs) + }) + + it('changes the network', async () => { + const networkDropdown = await findElement(driver, By.css('.network-name')) + await networkDropdown.click() + await delay(regularDelayMs) + + const ropstenButton = await findElement(driver, By.xpath(`//span[contains(text(), 'Ropsten')]`)) + await ropstenButton.click() + await delay(largeDelayMs) + }) + }) + + describe('provider listening for events', () => { + let extension + let popup + let dapp + let windowHandles + it('switches to a dapp', async () => { + await openNewPage(driver, 'http://127.0.0.1:8080/') + await delay(regularDelayMs) + + const connectButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Connect')]`)) + await connectButton.click() + + await delay(regularDelayMs) + + await waitUntilXWindowHandles(driver, 3) + windowHandles = await driver.getAllWindowHandles() + + extension = windowHandles[0] + popup = await switchToWindowWithTitle(driver, 'MetaMask Notification', windowHandles) + dapp = windowHandles.find(handle => handle !== extension && handle !== popup) + + await delay(regularDelayMs) + const approveButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Connect')]`)) + await approveButton.click() + + await driver.switchTo().window(dapp) + await delay(regularDelayMs) + }) + + it('creates a sign typed data signature request', async () => { + const signTypedMessage = await findElement(driver, By.xpath(`//button[contains(text(), 'Sign')]`), 10000) + await signTypedMessage.click() + await delay(largeDelayMs) + + windowHandles = await driver.getAllWindowHandles() + await switchToWindowWithTitle(driver, 'MetaMask Notification', windowHandles) + await delay(regularDelayMs) + + const title = await findElement(driver, By.css('.signature-request-content__title')) + const name = await findElement(driver, By.css('.signature-request-content__info--bolded')) + const content = await findElements(driver, By.css('.signature-request-content__info')) + const origin = content[0] + const address = content[1] + assert.equal(await title.getText(), 'Signature Request') + assert.equal(await name.getText(), 'Ether Mail') + assert.equal(await origin.getText(), '127.0.0.1') + assert.equal(await address.getText(), publicAddress.slice(0, 8) + '...' + publicAddress.slice(publicAddress.length - 8)) + }) + + it('signs the transaction', async () => { + const signButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Sign')]`), 10000) + await signButton.click() + await delay(regularDelayMs) + + extension = windowHandles[0] + await driver.switchTo().window(extension) + }) + + it('gets the current accounts address', async () => { + const detailsButton = await findElement(driver, By.css('.account-details__details-button')) + await detailsButton.click() + await delay(regularDelayMs) + + const addressInput = await findElement(driver, By.css('.qr-ellip-address')) + const newPublicAddress = await addressInput.getAttribute('value') + const accountModal = await driver.findElement(By.css('span .modal')) + + await driver.executeScript("document.querySelector('.account-modal-close').click()") + + await driver.wait(until.stalenessOf(accountModal)) + await delay(regularDelayMs) + assert.equal(newPublicAddress.toLowerCase(), publicAddress) + }) + }) +}) diff --git a/test/e2e/threebox.spec.js b/test/e2e/threebox.spec.js index ddadff686..e272e6798 100644 --- a/test/e2e/threebox.spec.js +++ b/test/e2e/threebox.spec.js @@ -12,6 +12,7 @@ const { setupFetchMocking, prepareExtensionForTesting, } = require('./helpers') +const enLocaleMessages = require('../../app/_locales/en/messages.json') describe('MetaMask', function () { let driver @@ -51,15 +52,9 @@ describe('MetaMask', function () { describe('set up data to be restored by 3box', () => { describe('First time flow starting from an existing seed phrase', () => { - it('turns on the threebox feature flag', async () => { - await delay(largeDelayMs) - await driver.executeScript('window.metamask.setFeatureFlag("threeBox", true)') - await delay(largeDelayMs) - }) - it('clicks the continue button on the welcome screen', async () => { await findElement(driver, By.css('.welcome-page__header')) - const welcomeScreenBtn = await findElement(driver, By.css('.first-time-flow__button')) + const welcomeScreenBtn = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`)) welcomeScreenBtn.click() await delay(largeDelayMs) }) @@ -96,7 +91,7 @@ describe('MetaMask', function () { it('clicks through the success screen', async () => { await findElement(driver, By.xpath(`//div[contains(text(), 'Congratulations')]`)) - const doneButton = await findElement(driver, By.css('button.first-time-flow__button')) + const doneButton = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`)) await doneButton.click() await delay(regularDelayMs) }) @@ -108,7 +103,7 @@ describe('MetaMask', function () { }) }) - describe('updates settings and address book', () => { + describe('turns on threebox syncing', () => { it('goes to the settings screen', async () => { await driver.findElement(By.css('.account-menu__icon')).click() await delay(regularDelayMs) @@ -117,6 +112,23 @@ describe('MetaMask', function () { settingsButton.click() }) + it('turns on threebox syncing', async () => { + const advancedButton = await findElement(driver, By.xpath(`//div[contains(text(), 'Advanced')]`)) + await advancedButton.click() + + const threeBoxToggle = await findElements(driver, By.css('.toggle-button')) + const threeBoxToggleButton = await threeBoxToggle[4].findElement(By.css('div')) + await threeBoxToggleButton.click() + }) + + }) + + describe('updates settings and address book', () => { + it('adds an address to the contact list', async () => { + const generalButton = await findElement(driver, By.xpath(`//div[contains(text(), 'General')]`)) + await generalButton.click() + }) + it('turns on use of blockies', async () => { const toggleButton = await findElement(driver, By.css('.toggle-button > div')) await toggleButton.click() @@ -163,15 +175,9 @@ describe('MetaMask', function () { }) describe('First time flow starting from an existing seed phrase', () => { - it('turns on the threebox feature flag', async () => { - await delay(largeDelayMs) - await driver2.executeScript('window.metamask.setFeatureFlag("threeBox", true)') - await delay(largeDelayMs) - }) - it('clicks the continue button on the welcome screen', async () => { await findElement(driver2, By.css('.welcome-page__header')) - const welcomeScreenBtn = await findElement(driver2, By.css('.first-time-flow__button')) + const welcomeScreenBtn = await findElement(driver2, By.xpath(`//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`)) welcomeScreenBtn.click() await delay(largeDelayMs) }) @@ -208,7 +214,7 @@ describe('MetaMask', function () { it('clicks through the success screen', async () => { await findElement(driver2, By.xpath(`//div[contains(text(), 'Congratulations')]`)) - const doneButton = await findElement(driver2, By.css('button.first-time-flow__button')) + const doneButton = await findElement(driver2, By.xpath(`//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`)) await doneButton.click() await delay(regularDelayMs) }) diff --git a/test/e2e/web3.spec.js b/test/e2e/web3.spec.js index a0df97b4e..f576b397f 100644 --- a/test/e2e/web3.spec.js +++ b/test/e2e/web3.spec.js @@ -15,6 +15,7 @@ const { setupFetchMocking, prepareExtensionForTesting, } = require('./helpers') +const enLocaleMessages = require('../../app/_locales/en/messages.json') describe('Using MetaMask with an existing account', function () { let driver @@ -65,7 +66,7 @@ describe('Using MetaMask with an existing account', function () { describe('First time flow starting from an existing seed phrase', () => { it('clicks the continue button on the welcome screen', async () => { await findElement(driver, By.css('.welcome-page__header')) - const welcomeScreenBtn = await findElement(driver, By.css('.first-time-flow__button')) + const welcomeScreenBtn = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`)) welcomeScreenBtn.click() await delay(largeDelayMs) }) @@ -102,7 +103,7 @@ describe('Using MetaMask with an existing account', function () { it('clicks through the success screen', async () => { await findElement(driver, By.xpath(`//div[contains(text(), 'Congratulations')]`)) - const doneButton = await findElement(driver, By.css('button.first-time-flow__button')) + const doneButton = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`)) await doneButton.click() await delay(regularDelayMs) }) @@ -125,6 +126,11 @@ describe('Using MetaMask with an existing account', function () { await openNewPage(driver, 'http://127.0.0.1:8080/') await delay(regularDelayMs) + const connectButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Connect')]`)) + await connectButton.click() + + await delay(regularDelayMs) + await waitUntilXWindowHandles(driver, 3) const windowHandles = await driver.getAllWindowHandles() diff --git a/test/helper.js b/test/helper.js index ddc2aba40..a49249062 100644 --- a/test/helper.js +++ b/test/helper.js @@ -36,8 +36,12 @@ require('jsdom-global')() window.localStorage = {} // crypto.getRandomValues -if (!window.crypto) window.crypto = {} -if (!window.crypto.getRandomValues) window.crypto.getRandomValues = require('polyfill-crypto.getrandomvalues') +if (!window.crypto) { + window.crypto = {} +} +if (!window.crypto.getRandomValues) { + window.crypto.getRandomValues = require('polyfill-crypto.getrandomvalues') +} function enableFailureOnUnhandledPromiseRejection () { // overwrite node's promise with the stricter Bluebird promise @@ -60,7 +64,9 @@ function enableFailureOnUnhandledPromiseRejection () { } else { var oldOHR = window.onunhandledrejection window.onunhandledrejection = function (evt) { - if (typeof oldOHR === 'function') oldOHR.apply(this, arguments) + if (typeof oldOHR === 'function') { + oldOHR.apply(this, arguments) + } throw evt.detail.reason } } diff --git a/test/integration/index.js b/test/integration/index.js index b266ddf37..1c4523e1e 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -19,7 +19,9 @@ pump( b.bundle(), writeStream, (err) => { - if (err) throw err + if (err) { + throw err + } console.log(`Integration test build completed: "${bundlePath}"`) process.exit(0) } diff --git a/test/lib/mock-simple-keychain.js b/test/lib/mock-simple-keychain.js index d3addc3e8..daf6001c4 100644 --- a/test/lib/mock-simple-keychain.js +++ b/test/lib/mock-simple-keychain.js @@ -6,7 +6,9 @@ const type = 'Simple Key Pair' module.exports = class MockSimpleKeychain { - static type () { return type } + static type () { + return type + } constructor (opts) { this.type = type diff --git a/test/lib/util.js b/test/lib/util.js index 4c5d789d1..be7e240a4 100644 --- a/test/lib/util.js +++ b/test/lib/util.js @@ -15,7 +15,9 @@ async function findAsync (container, selector, opts) { try { return await pollUntilTruthy(() => { const result = container.find(selector) - if (result.length > 0) return result + if (result.length > 0) { + return result + } }, opts) } catch (err) { throw new Error(`Failed to find element within interval: "${selector}"`) @@ -26,7 +28,9 @@ async function queryAsync (jQuery, selector, opts) { try { return await pollUntilTruthy(() => { const result = jQuery(selector) - if (result.length > 0) return result + if (result.length > 0) { + return result + } }, opts) } catch (err) { throw new Error(`Failed to find element within interval: "${selector}"`) diff --git a/test/setup.js b/test/setup.js index bccb4d58e..5aa6e59dd 100644 --- a/test/setup.js +++ b/test/setup.js @@ -5,6 +5,10 @@ require('@babel/register')({ require('./helper') window.SVGPathElement = window.SVGPathElement || { prototype: {} } -window.fetch = window.fetch || function fetch () { return Promise.resolve() } +window.fetch = window.fetch || function fetch () { + return Promise.resolve() +} global.indexedDB = {} -global.fetch = global.fetch || function fetch () { return Promise.resolve() } +global.fetch = global.fetch || function fetch () { + return Promise.resolve() +} diff --git a/test/unit-global/frozenPromise.js b/test/unit-global/frozenPromise.js new file mode 100644 index 000000000..bc1c96dfd --- /dev/null +++ b/test/unit-global/frozenPromise.js @@ -0,0 +1,55 @@ + +/* eslint-disable no-native-reassign */ + +// this is what we're testing +require('../../app/scripts/lib/freezeGlobals') + +const assert = require('assert') + +describe('Promise global is immutable', () => { + + it('throws when reassinging promise (syntax 1)', () => { + try { + Promise = {} + assert.fail('did not throw error') + } catch (err) { + assert.ok(err, 'did throw error') + } + }) + + it('throws when reassinging promise (syntax 2)', () => { + try { + global.Promise = {} + assert.fail('did not throw error') + } catch (err) { + assert.ok(err, 'did throw error') + } + }) + + it('throws when mutating existing Promise property', () => { + try { + Promise.all = () => {} + assert.fail('did not throw error') + } catch (err) { + assert.ok(err, 'did throw error') + } + }) + + it('throws when adding new Promise property', () => { + try { + Promise.foo = 'bar' + assert.fail('did not throw error') + } catch (err) { + assert.ok(err, 'did throw error') + } + }) + + it('throws when deleting Promise from global', () => { + try { + delete global.Promise + assert.fail('did not throw error') + } catch (err) { + assert.ok(err, 'did throw error') + } + }) +}) diff --git a/test/unit/actions/tx_test.js b/test/unit/actions/tx_test.js index 66378b594..ca1225113 100644 --- a/test/unit/actions/tx_test.js +++ b/test/unit/actions/tx_test.js @@ -33,9 +33,15 @@ describe('tx confirmation screen', function () { describe('cancelTx', function () { before(function (done) { actions._setBackgroundConnection({ - approveTransaction (_, cb) { cb('An error!') }, - cancelTransaction (_, cb) { cb() }, - getState (cb) { cb() }, + approveTransaction (_, cb) { + cb('An error!') + }, + cancelTransaction (_, cb) { + cb() + }, + getState (cb) { + cb() + }, }) done() }) diff --git a/test/unit/app/buy-eth-url.spec.js b/test/unit/app/buy-eth-url.spec.js index 7db992244..96814c59d 100644 --- a/test/unit/app/buy-eth-url.spec.js +++ b/test/unit/app/buy-eth-url.spec.js @@ -17,10 +17,10 @@ describe('buy-eth-url', function () { network: '42', } - it('returns coinbase url with amount and address for network 1', function () { + it('returns wyre url with address for network 1', function () { const wyreUrl = getBuyEthUrl(mainnet) - assert.equal(wyreUrl, 'https://dash.sendwyre.com/sign-up') + assert.equal(wyreUrl, 'https://pay.sendwyre.com/?dest=ethereum:0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc&destCurrency=ETH&accountId=AC-7AG3W4XH4N2') }) diff --git a/test/unit/app/controllers/assets-test.js b/test/unit/app/controllers/assets-test.js index 18f6ee5af..840958486 100644 --- a/test/unit/app/controllers/assets-test.js +++ b/test/unit/app/controllers/assets-test.js @@ -21,7 +21,9 @@ describe('AssetsController', () => { assets.addAsset(domain, sampleAsset) const result = assets.assets[assetCount] Object.keys(result).forEach((key) => { - if (key === 'fromDomain') return + if (key === 'fromDomain') { + return + } assert.equal(result[key], sampleAsset[key], `${key} should be same`) }) }) @@ -35,7 +37,9 @@ describe('AssetsController', () => { assets.updateAsset(domain, result) Object.keys(result).forEach((key) => { - if (key === 'fromDomain') return + if (key === 'fromDomain') { + return + } if (key === 'balance') { assert.notEqual(result[key], sampleAsset[key], `${key} should be updated`) } else { diff --git a/test/unit/app/controllers/balance-controller.spec.js b/test/unit/app/controllers/balance-controller.spec.js new file mode 100644 index 000000000..9ef25b95f --- /dev/null +++ b/test/unit/app/controllers/balance-controller.spec.js @@ -0,0 +1,55 @@ +const assert = require('assert') +const ObservableStore = require('obs-store') +const PollingBlockTracker = require('eth-block-tracker') + +const BalanceController = require('../../../../app/scripts/controllers/balance') +const AccountTracker = require('../../../../app/scripts/lib/account-tracker') +const TransactionController = require('../../../../app/scripts/controllers/transactions') +const { createTestProviderTools } = require('../../../stub/provider') +const provider = createTestProviderTools({ scaffold: {}}).provider + +const TEST_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc' + +const accounts = { + '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': { + balance: '0x5e942b06dc24c4d50', + address: TEST_ADDRESS, + }, +} + +describe('Balance Controller', () => { + + let balanceController + + it('errors when address, accountTracker, txController, or blockTracker', function () { + try { + balanceController = new BalanceController() + } catch (error) { + assert.equal(error.message, 'Cannot construct a balance checker without address, accountTracker, txController, and blockTracker.') + } + }) + + beforeEach(() => { + balanceController = new BalanceController({ + address: TEST_ADDRESS, + accountTracker: new AccountTracker({ + provider, + blockTracker: new PollingBlockTracker({ provider }), + }), + txController: new TransactionController({ + provider, + networkStore: new ObservableStore(), + blockTracker: new PollingBlockTracker({ provider }), + }), + blockTracker: new PollingBlockTracker({ provider }), + }) + + balanceController.accountTracker.store.updateState({ accounts }) + }) + + it('updates balance controller ethBalance from account tracker', async function () { + await balanceController.updateBalance() + const balanceControllerState = balanceController.store.getState() + assert.equal(balanceControllerState.ethBalance, '0x5e942b06dc24c4d50') + }) +}) diff --git a/test/unit/app/controllers/ens-controller-test.js b/test/unit/app/controllers/ens-controller-test.js new file mode 100644 index 000000000..1eb52a17c --- /dev/null +++ b/test/unit/app/controllers/ens-controller-test.js @@ -0,0 +1,135 @@ +const assert = require('assert') +const sinon = require('sinon') +const ObservableStore = require('obs-store') +const HttpProvider = require('ethjs-provider-http') +const EnsController = require('../../../../app/scripts/controllers/ens') + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' +const ZERO_X_ERROR_ADDRESS = '0x' + +describe('EnsController', function () { + describe('#constructor', function () { + it('should construct the controller given a provider and a network', async () => { + const provider = new HttpProvider('https://ropsten.infura.io') + const currentNetworkId = '3' + const networkStore = new ObservableStore(currentNetworkId) + const ens = new EnsController({ + provider, + networkStore, + }) + + assert.ok(ens._ens) + }) + + it('should construct the controller given an existing ENS instance', async () => { + const networkStore = { + subscribe: sinon.spy(), + } + const ens = new EnsController({ + ens: {}, + networkStore, + }) + + assert.ok(ens._ens) + }) + }) + + describe('#reverseResolveName', function () { + it('should resolve to an ENS name', async () => { + const address = '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5' + const networkStore = { + subscribe: sinon.spy(), + } + const ens = new EnsController({ + ens: { + reverse: sinon.stub().withArgs(address).returns('peaksignal.eth'), + lookup: sinon.stub().withArgs('peaksignal.eth').returns(address), + }, + networkStore, + }) + + const name = await ens.reverseResolveAddress(address) + assert.equal(name, 'peaksignal.eth') + }) + + it('should only resolve an ENS name once', async () => { + const address = '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5' + const reverse = sinon.stub().withArgs(address).returns('peaksignal.eth') + const lookup = sinon.stub().withArgs('peaksignal.eth').returns(address) + const networkStore = { + subscribe: sinon.spy(), + } + const ens = new EnsController({ + ens: { + reverse, + lookup, + }, + networkStore, + }) + + assert.equal(await ens.reverseResolveAddress(address), 'peaksignal.eth') + assert.equal(await ens.reverseResolveAddress(address), 'peaksignal.eth') + assert.ok(lookup.calledOnce) + assert.ok(reverse.calledOnce) + }) + + it('should fail if the name is registered to a different address than the reverse-resolved', async () => { + const address = '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5' + const networkStore = { + subscribe: sinon.spy(), + } + const ens = new EnsController({ + ens: { + reverse: sinon.stub().withArgs(address).returns('peaksignal.eth'), + lookup: sinon.stub().withArgs('peaksignal.eth').returns('0xfoo'), + }, + networkStore, + }) + + const name = await ens.reverseResolveAddress(address) + assert.strictEqual(name, undefined) + }) + + it('should throw an error when the lookup resolves to the zero address', async () => { + const address = '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5' + const networkStore = { + subscribe: sinon.spy(), + } + const ens = new EnsController({ + ens: { + reverse: sinon.stub().withArgs(address).returns('peaksignal.eth'), + lookup: sinon.stub().withArgs('peaksignal.eth').returns(ZERO_ADDRESS), + }, + networkStore, + }) + + try { + await ens.reverseResolveAddress(address) + assert.fail('#reverseResolveAddress did not throw') + } catch (e) { + assert.ok(e) + } + }) + + it('should throw an error the lookup resolves to the zero x address', async () => { + const address = '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5' + const networkStore = { + subscribe: sinon.spy(), + } + const ens = new EnsController({ + ens: { + reverse: sinon.stub().withArgs(address).returns('peaksignal.eth'), + lookup: sinon.stub().withArgs('peaksignal.eth').returns(ZERO_X_ERROR_ADDRESS), + }, + networkStore, + }) + + try { + await ens.reverseResolveAddress(address) + assert.fail('#reverseResolveAddress did not throw') + } catch (e) { + assert.ok(e) + } + }) + }) +}) diff --git a/test/unit/app/controllers/metamask-controller-test.js b/test/unit/app/controllers/metamask-controller-test.js index 447c3c892..a904dec08 100644 --- a/test/unit/app/controllers/metamask-controller-test.js +++ b/test/unit/app/controllers/metamask-controller-test.js @@ -2,6 +2,7 @@ const assert = require('assert') const sinon = require('sinon') const clone = require('clone') const nock = require('nock') +const ethUtil = require('ethereumjs-util') const createThoughStream = require('through2').obj const blacklistJSON = require('eth-phishing-detect/src/config') const firstTimeState = require('../../../unit/localhostState') @@ -9,7 +10,7 @@ const createTxMeta = require('../../../lib/createTxMeta') const EthQuery = require('eth-query') const threeBoxSpies = { - new3Box: sinon.spy(), + init: sinon.spy(), getThreeBoxAddress: sinon.spy(), getThreeBoxSyncingState: sinon.stub().returns(true), turnThreeBoxSyncingOn: sinon.spy(), @@ -23,7 +24,7 @@ class ThreeBoxControllerMock { subscribe: () => {}, getState: () => ({}), } - this.new3Box = threeBoxSpies.new3Box + this.init = threeBoxSpies.init this.getThreeBoxAddress = threeBoxSpies.getThreeBoxAddress this.getThreeBoxSyncingState = threeBoxSpies.getThreeBoxSyncingState this.turnThreeBoxSyncingOn = threeBoxSpies.turnThreeBoxSyncingOn @@ -103,12 +104,57 @@ describe('MetaMaskController', function () { sandbox.restore() }) + describe('#getAccounts', function () { + + beforeEach(async function () { + const password = 'a-fake-password' + + await metamaskController.createNewVaultAndRestore(password, TEST_SEED) + }) + + it('returns first address when dapp calls web3.eth.getAccounts', function () { + metamaskController.networkController._baseProviderParams.getAccounts((err, res) => { + assert.ifError(err) + assert.equal(res.length, 1) + assert.equal(res[0], '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc') + }) + }) + }) + + describe('#importAccountWithStrategy', function () { + + const importPrivkey = '4cfd3e90fc78b0f86bf7524722150bb8da9c60cd532564d7ff43f5716514f553' + + beforeEach(async function () { + const password = 'a-fake-password' + + await metamaskController.createNewVaultAndRestore(password, TEST_SEED) + await metamaskController.importAccountWithStrategy('Private Key', [ importPrivkey ]) + }) + + it('adds private key to keyrings in KeyringController', async function () { + const simpleKeyrings = metamaskController.keyringController.getKeyringsByType('Simple Key Pair') + const privKeyBuffer = simpleKeyrings[0].wallets[0]._privKey + const pubKeyBuffer = simpleKeyrings[0].wallets[0]._pubKey + const addressBuffer = ethUtil.pubToAddress(pubKeyBuffer) + const privKey = ethUtil.bufferToHex(privKeyBuffer) + const pubKey = ethUtil.bufferToHex(addressBuffer) + assert.equal(privKey, ethUtil.addHexPrefix(importPrivkey)) + assert.equal(pubKey, '0xe18035bf8712672935fdb4e5e431b1a0183d2dfc') + }) + + it('adds private key to keyrings in KeyringController', async function () { + const keyringAccounts = await metamaskController.keyringController.getAccounts() + assert.equal(keyringAccounts[keyringAccounts.length - 1], '0xe18035bf8712672935fdb4e5e431b1a0183d2dfc') + }) + }) + describe('submitPassword', function () { const password = 'password' beforeEach(async function () { await metamaskController.createNewVaultAndKeychain(password) - threeBoxSpies.new3Box.reset() + threeBoxSpies.init.reset() threeBoxSpies.turnThreeBoxSyncingOn.reset() }) @@ -129,16 +175,9 @@ describe('MetaMaskController', function () { }) }) - it('gets does not instantiate 3box if the feature flag is false', async () => { + it('gets the address from threebox and creates a new 3box instance', async () => { await metamaskController.submitPassword(password) - assert(threeBoxSpies.new3Box.notCalled) - assert(threeBoxSpies.turnThreeBoxSyncingOn.notCalled) - }) - - it('gets the address from threebox and creates a new 3box instance if the feature flag is true', async () => { - metamaskController.preferencesController.setFeatureFlag('threeBox', true) - await metamaskController.submitPassword(password) - assert(threeBoxSpies.new3Box.calledOnce) + assert(threeBoxSpies.init.calledOnce) assert(threeBoxSpies.turnThreeBoxSyncingOn.calledOnce) }) }) @@ -188,7 +227,9 @@ describe('MetaMaskController', function () { it('should be able to call newVaultAndRestore despite a mistake.', async function () { const password = 'what-what-what' sandbox.stub(metamaskController, 'getBalance') - metamaskController.getBalance.callsFake(() => { return Promise.resolve('0x0') }) + metamaskController.getBalance.callsFake(() => { + return Promise.resolve('0x0') + }) await metamaskController.createNewVaultAndRestore(password, TEST_SEED.slice(0, -1)).catch(() => null) await metamaskController.createNewVaultAndRestore(password, TEST_SEED) @@ -198,7 +239,9 @@ describe('MetaMaskController', function () { it('should clear previous identities after vault restoration', async () => { sandbox.stub(metamaskController, 'getBalance') - metamaskController.getBalance.callsFake(() => { return Promise.resolve('0x0') }) + metamaskController.getBalance.callsFake(() => { + return Promise.resolve('0x0') + }) await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED) assert.deepEqual(metamaskController.getState().identities, { @@ -637,7 +680,9 @@ describe('MetaMaskController', function () { beforeEach(async () => { sandbox.stub(metamaskController, 'getBalance') - metamaskController.getBalance.callsFake(() => { return Promise.resolve('0x0') }) + metamaskController.getBalance.callsFake(() => { + return Promise.resolve('0x0') + }) await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED_ALT) @@ -695,7 +740,9 @@ describe('MetaMaskController', function () { beforeEach(async function () { sandbox.stub(metamaskController, 'getBalance') - metamaskController.getBalance.callsFake(() => { return Promise.resolve('0x0') }) + metamaskController.getBalance.callsFake(() => { + return Promise.resolve('0x0') + }) await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED_ALT) @@ -758,7 +805,7 @@ describe('MetaMaskController', function () { describe('#setupUntrustedCommunication', function () { let streamTest - const phishingUrl = 'myethereumwalletntw.com' + const phishingUrl = new URL('http://myethereumwalletntw.com') afterEach(function () { streamTest.end() @@ -770,8 +817,10 @@ describe('MetaMaskController', function () { const { promise, resolve } = deferredPromise() streamTest = createThoughStream((chunk, _, cb) => { - if (chunk.name !== 'phishing') return cb() - assert.equal(chunk.data.hostname, phishingUrl) + if (chunk.name !== 'phishing') { + return cb() + } + assert.equal(chunk.data.hostname, phishingUrl.hostname) resolve() cb() }) @@ -890,6 +939,8 @@ describe('MetaMaskController', function () { function deferredPromise () { let resolve - const promise = new Promise(_resolve => { resolve = _resolve }) + const promise = new Promise(_resolve => { + resolve = _resolve + }) return { promise, resolve } } diff --git a/test/unit/app/controllers/network-contoller-test.js b/test/unit/app/controllers/network/network-controller-test.js similarity index 95% rename from test/unit/app/controllers/network-contoller-test.js rename to test/unit/app/controllers/network/network-controller-test.js index 32f7b337d..b63a23a4f 100644 --- a/test/unit/app/controllers/network-contoller-test.js +++ b/test/unit/app/controllers/network/network-controller-test.js @@ -1,9 +1,9 @@ const assert = require('assert') const nock = require('nock') -const NetworkController = require('../../../../app/scripts/controllers/network') +const NetworkController = require('../../../../../app/scripts/controllers/network') const { getNetworkDisplayName, -} = require('../../../../app/scripts/controllers/network/util') +} = require('../../../../../app/scripts/controllers/network/util') describe('# Network Controller', function () { let networkController diff --git a/test/unit/app/controllers/network/pending-middleware-test.js b/test/unit/app/controllers/network/pending-middleware-test.js new file mode 100644 index 000000000..ac6f8ad9a --- /dev/null +++ b/test/unit/app/controllers/network/pending-middleware-test.js @@ -0,0 +1,85 @@ +const assert = require('assert') +const { createPendingNonceMiddleware, createPendingTxMiddleware } = require('../../../../../app/scripts/controllers/network/middleware/pending') +const txMetaStub = require('./stubs').txMetaStub +describe('#createPendingNonceMiddleware', function () { + const getPendingNonce = async () => '0x2' + const address = '0xF231D46dD78806E1DD93442cf33C7671f8538748' + const pendingNonceMiddleware = createPendingNonceMiddleware({ getPendingNonce }) + + it('should call next if not a eth_getTransactionCount request', (done) => { + const req = {method: 'eth_getBlockByNumber'} + const res = {} + pendingNonceMiddleware(req, res, () => done()) + }) + it('should call next if not a "pending" block request', (done) => { + const req = { method: 'eth_getTransactionCount', params: [address] } + const res = {} + pendingNonceMiddleware(req, res, () => done()) + }) + it('should fill the result with a the "pending" nonce', (done) => { + const req = { method: 'eth_getTransactionCount', params: [address, 'pending'] } + const res = {} + pendingNonceMiddleware(req, res, () => { + done(new Error('should not have called next')) + }, () => { + assert(res.result === '0x2') + done() + }) + }) +}) + +describe('#createPendingTxMiddleware', function () { + let returnUndefined = true + const getPendingTransactionByHash = () => returnUndefined ? undefined : txMetaStub + const address = '0xF231D46dD78806E1DD93442cf33C7671f8538748' + const pendingTxMiddleware = createPendingTxMiddleware({ getPendingTransactionByHash }) + const spec = { + 'blockHash': null, + 'blockNumber': null, + 'from': '0xf231d46dd78806e1dd93442cf33c7671f8538748', + 'gas': '0x5208', + 'gasPrice': '0x1e8480', + 'hash': '0x2cc5a25744486f7383edebbf32003e5a66e18135799593d6b5cdd2bb43674f09', + 'input': '0x', + 'nonce': '0x4', + 'to': '0xf231d46dd78806e1dd93442cf33c7671f8538748', + 'transactionIndex': null, + 'value': '0x0', + 'v': '0x2c', + 'r': '0x5f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57', + 's': '0x0259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a', + } + it('should call next if not a eth_getTransactionByHash request', (done) => { + const req = {method: 'eth_getBlockByNumber'} + const res = {} + pendingTxMiddleware(req, res, () => done()) + }) + + it('should call next if no pending txMeta is in history', (done) => { + const req = { method: 'eth_getTransactionByHash', params: [address] } + const res = {} + pendingTxMiddleware(req, res, () => done()) + }) + + it('should fill the result with a the "pending" tx the result should match the rpc spec', (done) => { + returnUndefined = false + const req = { method: 'eth_getTransactionByHash', params: [address, 'pending'] } + const res = {} + pendingTxMiddleware(req, res, () => { + done(new Error('should not have called next')) + }, () => { + /* + // uncomment this section for debugging help with non matching keys + const coppy = {...res.result} + Object.keys(spec).forEach((key) => { + console.log(coppy[key], '===', spec[key], coppy[key] === spec[key], key) + delete coppy[key] + }) + console.log(coppy) + */ + assert.deepStrictEqual(res.result, spec, new Error('result does not match the spec object')) + done() + }) + }) + +}) diff --git a/test/unit/app/controllers/network/stubs.js b/test/unit/app/controllers/network/stubs.js new file mode 100644 index 000000000..1551cd581 --- /dev/null +++ b/test/unit/app/controllers/network/stubs.js @@ -0,0 +1,225 @@ +/* + this file is for all my big stubs because i don't want to + to mingle with my tests +*/ + +module.exports = {} + +// for pending middlewares test +module.exports.txMetaStub = { + 'estimatedGas': '0x5208', + 'firstRetryBlockNumber': '0x51a402', + 'gasLimitSpecified': true, + 'gasPriceSpecified': true, + 'hash': '0x2cc5a25744486f7383edebbf32003e5a66e18135799593d6b5cdd2bb43674f09', + 'history': [ + { + 'id': 405984854664302, + 'loadingDefaults': true, + 'metamaskNetworkId': '4', + 'status': 'unapproved', + 'time': 1572395156620, + 'transactionCategory': 'sentEther', + 'txParams': { + 'from': '0xf231d46dd78806e1dd93442cf33c7671f8538748', + 'gas': '0x5208', + 'gasPrice': '0x1e8480', + 'to': '0xf231d46dd78806e1dd93442cf33c7671f8538748', + 'value': '0x0', + }, + 'type': 'standard', + }, + [ + { + 'op': 'replace', + 'path': '/loadingDefaults', + 'timestamp': 1572395156645, + 'value': false, + }, + { + 'op': 'add', + 'path': '/gasPriceSpecified', + 'value': true, + }, + { + 'op': 'add', + 'path': '/gasLimitSpecified', + 'value': true, + }, + { + 'op': 'add', + 'path': '/estimatedGas', + 'value': '0x5208', + }, + ], + [ + { + 'note': '#newUnapprovedTransaction - adding the origin', + 'op': 'add', + 'path': '/origin', + 'timestamp': 1572395156645, + 'value': 'MetaMask', + }, + ], + [], + [ + { + 'note': 'txStateManager: setting status to approved', + 'op': 'replace', + 'path': '/status', + 'timestamp': 1572395158240, + 'value': 'approved', + }, + ], + [ + { + 'note': 'transactions#approveTransaction', + 'op': 'add', + 'path': '/txParams/nonce', + 'timestamp': 1572395158261, + 'value': '0x4', + }, + { + 'op': 'add', + 'path': '/nonceDetails', + 'value': { + 'local': { + 'details': { + 'highest': 4, + 'startPoint': 4, + }, + 'name': 'local', + 'nonce': 4, + }, + 'network': { + 'details': { + 'baseCount': 4, + 'blockNumber': '0x51a401', + }, + 'name': 'network', + 'nonce': 4, + }, + 'params': { + 'highestLocallyConfirmed': 0, + 'highestSuggested': 4, + 'nextNetworkNonce': 4, + }, + }, + }, + ], + [ + { + 'note': 'transactions#signTransaction: add r, s, v values', + 'op': 'add', + 'path': '/r', + 'timestamp': 1572395158280, + 'value': '0x5f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57', + }, + { + 'op': 'add', + 'path': '/s', + 'value': '0x0259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a', + }, + { + 'op': 'add', + 'path': '/v', + 'value': '0x2c', + }, + ], + [ + { + 'note': 'transactions#publishTransaction', + 'op': 'replace', + 'path': '/status', + 'timestamp': 1572395158281, + 'value': 'signed', + }, + { + 'op': 'add', + 'path': '/rawTx', + 'value': '0xf86204831e848082520894f231d46dd78806e1dd93442cf33c7671f853874880802ca05f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57a00259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a', + }, + ], + [], + [ + { + 'note': 'transactions#setTxHash', + 'op': 'add', + 'path': '/hash', + 'timestamp': 1572395158570, + 'value': '0x2cc5a25744486f7383edebbf32003e5a66e18135799593d6b5cdd2bb43674f09', + }, + ], + [ + { + 'note': 'txStateManager - add submitted time stamp', + 'op': 'add', + 'path': '/submittedTime', + 'timestamp': 1572395158571, + 'value': 1572395158570, + }, + ], + [ + { + 'note': 'txStateManager: setting status to submitted', + 'op': 'replace', + 'path': '/status', + 'timestamp': 1572395158576, + 'value': 'submitted', + }, + ], + [ + { + 'note': 'transactions/pending-tx-tracker#event: tx:block-update', + 'op': 'add', + 'path': '/firstRetryBlockNumber', + 'timestamp': 1572395168972, + 'value': '0x51a402', + }, + ], + ], + 'id': 405984854664302, + 'loadingDefaults': false, + 'metamaskNetworkId': '4', + 'nonceDetails': { + 'local': { + 'details': { + 'highest': 4, + 'startPoint': 4, + }, + 'name': 'local', + 'nonce': 4, + }, + 'network': { + 'details': { + 'baseCount': 4, + 'blockNumber': '0x51a401', + }, + 'name': 'network', + 'nonce': 4, + }, + 'params': { + 'highestLocallyConfirmed': 0, + 'highestSuggested': 4, + 'nextNetworkNonce': 4, + }, + }, + 'origin': 'MetaMask', + 'r': '0x5f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57', + 'rawTx': '0xf86204831e848082520894f231d46dd78806e1dd93442cf33c7671f853874880802ca05f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57a00259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a', + 's': '0x0259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a', + 'status': 'submitted', + 'submittedTime': 1572395158570, + 'time': 1572395156620, + 'transactionCategory': 'sentEther', + 'txParams': { + 'from': '0xf231d46dd78806e1dd93442cf33c7671f8538748', + 'gas': '0x5208', + 'gasPrice': '0x1e8480', + 'nonce': '0x4', + 'to': '0xf231d46dd78806e1dd93442cf33c7671f8538748', + 'value': '0x0', + }, + 'type': 'standard', + 'v': '0x2c', +} diff --git a/test/unit/app/controllers/preferences-controller-test.js b/test/unit/app/controllers/preferences-controller-test.js index 5378f5e6f..fee4dad68 100644 --- a/test/unit/app/controllers/preferences-controller-test.js +++ b/test/unit/app/controllers/preferences-controller-test.js @@ -456,28 +456,44 @@ describe('preferences controller', function () { }) it('should validate ERC20 asset correctly', async function () { const validateSpy = sandbox.spy(preferencesController._validateERC20AssetParams) - try { validateSpy({rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', symbol: 'ABC', decimals: 0}) } catch (e) {} + try { + validateSpy({rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', symbol: 'ABC', decimals: 0}) + } catch (e) {} assert.equal(validateSpy.threw(), false, 'correct options object') const validateSpyAddress = sandbox.spy(preferencesController._validateERC20AssetParams) - try { validateSpyAddress({symbol: 'ABC', decimals: 0}) } catch (e) {} + try { + validateSpyAddress({symbol: 'ABC', decimals: 0}) + } catch (e) {} assert.equal(validateSpyAddress.threw(), true, 'options object with no address') const validateSpySymbol = sandbox.spy(preferencesController._validateERC20AssetParams) - try { validateSpySymbol({rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', decimals: 0}) } catch (e) {} + try { + validateSpySymbol({rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', decimals: 0}) + } catch (e) {} assert.equal(validateSpySymbol.threw(), true, 'options object with no symbol') const validateSpyDecimals = sandbox.spy(preferencesController._validateERC20AssetParams) - try { validateSpyDecimals({rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', symbol: 'ABC'}) } catch (e) {} + try { + validateSpyDecimals({rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', symbol: 'ABC'}) + } catch (e) {} assert.equal(validateSpyDecimals.threw(), true, 'options object with no decimals') const validateSpyInvalidSymbol = sandbox.spy(preferencesController._validateERC20AssetParams) - try { validateSpyInvalidSymbol({rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', symbol: 'ABCDEFGHI', decimals: 0}) } catch (e) {} + try { + validateSpyInvalidSymbol({rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', symbol: 'ABCDEFGHI', decimals: 0}) + } catch (e) {} assert.equal(validateSpyInvalidSymbol.threw(), true, 'options object with invalid symbol') const validateSpyInvalidDecimals1 = sandbox.spy(preferencesController._validateERC20AssetParams) - try { validateSpyInvalidDecimals1({rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', symbol: 'ABCDEFGHI', decimals: -1}) } catch (e) {} + try { + validateSpyInvalidDecimals1({rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', symbol: 'ABCDEFGHI', decimals: -1}) + } catch (e) {} assert.equal(validateSpyInvalidDecimals1.threw(), true, 'options object with decimals less than zero') const validateSpyInvalidDecimals2 = sandbox.spy(preferencesController._validateERC20AssetParams) - try { validateSpyInvalidDecimals2({rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', symbol: 'ABCDEFGHI', decimals: 38}) } catch (e) {} + try { + validateSpyInvalidDecimals2({rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', symbol: 'ABCDEFGHI', decimals: 38}) + } catch (e) {} assert.equal(validateSpyInvalidDecimals2.threw(), true, 'options object with decimals more than 36') const validateSpyInvalidAddress = sandbox.spy(preferencesController._validateERC20AssetParams) - try { validateSpyInvalidAddress({rawAddress: '0x123', symbol: 'ABC', decimals: 0}) } catch (e) {} + try { + validateSpyInvalidAddress({rawAddress: '0x123', symbol: 'ABC', decimals: 0}) + } catch (e) {} assert.equal(validateSpyInvalidAddress.threw(), true, 'options object with address invalid') }) }) diff --git a/test/unit/app/controllers/transactions/pending-tx-test.js b/test/unit/app/controllers/transactions/pending-tx-test.js index e1de5731b..622a53b18 100644 --- a/test/unit/app/controllers/transactions/pending-tx-test.js +++ b/test/unit/app/controllers/transactions/pending-tx-test.js @@ -40,13 +40,19 @@ describe('PendingTransactionTracker', function () { return { releaseLock: () => {} } }, }, - getPendingTransactions: () => { return [] }, - getCompletedTransactions: () => { return [] }, + getPendingTransactions: () => { + return [] + }, + getCompletedTransactions: () => { + return [] + }, publishTransaction: () => {}, confirmTransaction: () => {}, }) - pendingTxTracker._getBlock = (blockNumber) => { return {number: blockNumber, transactions: []} } + pendingTxTracker._getBlock = (blockNumber) => { + return {number: blockNumber, transactions: []} + } }) describe('_checkPendingTx state management', function () { @@ -150,14 +156,18 @@ describe('PendingTransactionTracker', function () { txMeta2.id = 2 txMeta3.id = 3 txList = [txMeta, txMeta2, txMeta3].map((tx) => { - tx.processed = new Promise((resolve) => { tx.resolve = resolve }) + tx.processed = new Promise((resolve) => { + tx.resolve = resolve + }) return tx }) }) it('should warp all txMeta\'s in #updatePendingTxs', function (done) { pendingTxTracker.getPendingTransactions = () => txList - pendingTxTracker._checkPendingTx = (tx) => { tx.resolve(tx) } + pendingTxTracker._checkPendingTx = (tx) => { + tx.resolve(tx) + } Promise.all(txList.map((tx) => tx.processed)) .then(() => done()) .catch(done) @@ -171,7 +181,9 @@ describe('PendingTransactionTracker', function () { beforeEach(function () { const txMeta2 = txMeta3 = txMeta txList = [txMeta, txMeta2, txMeta3].map((tx) => { - tx.processed = new Promise((resolve) => { tx.resolve = resolve }) + tx.processed = new Promise((resolve) => { + tx.resolve = resolve + }) return tx }) }) @@ -181,7 +193,9 @@ describe('PendingTransactionTracker', function () { }) it('should call #_resubmitTx for all pending tx\'s', function (done) { pendingTxTracker.getPendingTransactions = () => txList - pendingTxTracker._resubmitTx = async (tx) => { tx.resolve(tx) } + pendingTxTracker._resubmitTx = async (tx) => { + tx.resolve(tx) + } Promise.all(txList.map((tx) => tx.processed)) .then(() => done()) .catch(done) @@ -225,7 +239,9 @@ describe('PendingTransactionTracker', function () { }) pendingTxTracker.getPendingTransactions = () => txList - pendingTxTracker._resubmitTx = async () => { 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(() => done()) .catch(done) @@ -369,7 +385,9 @@ describe('PendingTransactionTracker', function () { rawTx: '0xf86c808504a817c800827b0d940c62bb85faa3311a998d3aba8098c1235c564966880de0b6b3a7640000802aa08ff665feb887a25d4099e40e11f0fef93ee9608f404bd3f853dd9e84ed3317a6a02ec9d3d1d6e176d4d2593dd760e74ccac753e6a0ea0d00cc9789d0d7ff1f471d', }] pendingTxTracker.getCompletedTransactions = (address) => { - if (!address) throw new Error('unless behavior has changed #_checkIfNonceIsTaken needs a filtered list of transactions to see if the nonce is taken') + if (!address) { + throw new Error('unless behavior has changed #_checkIfNonceIsTaken needs a filtered list of transactions to see if the nonce is taken') + } return confirmedTxList } }) diff --git a/test/unit/app/controllers/transactions/tx-controller-test.js b/test/unit/app/controllers/transactions/tx-controller-test.js index 9072dc684..d398c7e04 100644 --- a/test/unit/app/controllers/transactions/tx-controller-test.js +++ b/test/unit/app/controllers/transactions/tx-controller-test.js @@ -38,7 +38,9 @@ describe('Transaction Controller', function () { blockTrackerStub.getLatestBlock = noop txController = new TransactionController({ provider, - getGasPrice: function () { return '0xee6b2800' }, + getGasPrice: function () { + return '0xee6b2800' + }, networkStore: netStore, txHistoryLimit: 10, blockTracker: blockTrackerStub, @@ -46,6 +48,7 @@ describe('Transaction Controller', function () { ethTx.sign(fromAccount.key) resolve() }), + getPermittedAccounts: () => {}, }) txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop }) }) @@ -162,8 +165,11 @@ describe('Transaction Controller', function () { txController.newUnapprovedTransaction(txParams) .catch((err) => { - if (err.message === 'MetaMask Tx Signature: User denied transaction signature.') done() - else done(err) + if (err.message === 'MetaMask Tx Signature: User denied transaction signature.') { + done() + } else { + done(err) + } }) }) }) @@ -171,13 +177,15 @@ describe('Transaction Controller', function () { describe('#addUnapprovedTransaction', function () { const selectedAddress = '0x1678a085c290ebd122dc42cba69373b5953b831d' - let getSelectedAddress + let getSelectedAddress, getPermittedAccounts beforeEach(function () { getSelectedAddress = sinon.stub(txController, 'getSelectedAddress').returns(selectedAddress) + getPermittedAccounts = sinon.stub(txController, 'getPermittedAccounts').returns([selectedAddress]) }) afterEach(function () { getSelectedAddress.restore() + getPermittedAccounts.restore() }) it('should add an unapproved transaction and return a valid txMeta', function (done) { @@ -209,8 +217,11 @@ describe('Transaction Controller', function () { txController.networkStore = new ObservableStore(1) txController.addUnapprovedTransaction({ from: selectedAddress, to: '0x0d1d4e623D10F9FBA5Db95830F7d3839406C6AF2' }) .catch((err) => { - if (err.message === 'Recipient is a public account') done() - else done(err) + if (err.message === 'Recipient is a public account') { + done() + } else { + done(err) + } }) }) @@ -239,8 +250,11 @@ describe('Transaction Controller', function () { txController.networkStore = new ObservableStore('loading') txController.addUnapprovedTransaction({ from: selectedAddress, to: '0x0d1d4e623D10F9FBA5Db95830F7d3839406C6AF2' }) .catch((err) => { - if (err.message === 'MetaMask is having trouble connecting to the network') done() - else done(err) + if (err.message === 'MetaMask is having trouble connecting to the network') { + done() + } else { + done(err) + } }) }) }) @@ -403,7 +417,9 @@ describe('Transaction Controller', function () { assert.equal(status, 'rejected', 'status should e rejected') assert.equal(txId, 0, 'id should e 0') done() - } catch (e) { done(e) } + } catch (e) { + done(e) + } }) txController.cancelTransaction(0) @@ -496,6 +512,18 @@ describe('Transaction Controller', function () { assert.equal(publishedTx.hash, hash) assert.equal(publishedTx.status, 'submitted') }) + + it('should ignore the error "Transaction Failed: known transaction" and be as usual', async function () { + providerResultStub['eth_sendRawTransaction'] = async (_, __, ___, end) => { + end('Transaction Failed: known transaction') + } + const rawTx = '0xf86204831e848082520894f231d46dd78806e1dd93442cf33c7671f853874880802ca05f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57a00259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a' + txController.txStateManager.addTx(txMeta) + await txController.publishTransaction(txMeta.id, rawTx) + const publishedTx = txController.txStateManager.getTx(1) + assert.equal(publishedTx.hash, '0x2cc5a25744486f7383edebbf32003e5a66e18135799593d6b5cdd2bb43674f09') + assert.equal(publishedTx.status, 'submitted') + }) }) describe('#retryTransaction', function () { @@ -607,7 +635,9 @@ describe('Transaction Controller', function () { _blockTrackerStub.getLatestBlock = noop const _txController = new TransactionController({ provider: _provider, - getGasPrice: function () { return '0xee6b2800' }, + getGasPrice: function () { + return '0xee6b2800' + }, networkStore: new ObservableStore(currentNetworkId), txHistoryLimit: 10, blockTracker: _blockTrackerStub, @@ -622,6 +652,38 @@ describe('Transaction Controller', function () { }) assert.deepEqual(result, { transactionCategory: CONTRACT_INTERACTION_KEY, getCodeResponse: '0x0a' }) }) + + it('should return a contract interaction transactionCategory with the correct getCodeResponse when to is a contract address and data is falsey', 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: '', + }) + assert.deepEqual(result, { transactionCategory: CONTRACT_INTERACTION_KEY, getCodeResponse: '0x0a' }) + }) }) describe('#getPendingTransactions', function () { 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 328c2ac6f..6136de66e 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 @@ -106,7 +106,9 @@ describe('Transaction state history helper', function () { assert.equal(result[0].path, expectedEntry1.path) assert.equal(result[0].value, expectedEntry1.value) assert.equal(result[0].value, expectedEntry1.value) - if (note) { assert.equal(result[0].note, note) } + if (note) { + assert.equal(result[0].note, note) + } assert.ok(result[0].timestamp >= before && result[0].timestamp <= after) diff --git a/test/unit/app/controllers/transactions/tx-utils-test.js b/test/unit/app/controllers/transactions/tx-utils-test.js index 65c8d35b0..1c5d20b09 100644 --- a/test/unit/app/controllers/transactions/tx-utils-test.js +++ b/test/unit/app/controllers/transactions/tx-utils-test.js @@ -66,7 +66,9 @@ describe('txUtils', function () { from: '0x1678a085c290ebd122dc42cba69373b5953b831d', to: '0x', } - assert.throws(() => { txUtils.validateRecipient(zeroRecipientTxParams) }, Error, 'Invalid recipient address') + assert.throws(() => { + txUtils.validateRecipient(zeroRecipientTxParams) + }, Error, 'Invalid recipient address') }) }) @@ -76,19 +78,27 @@ describe('txUtils', function () { // where from is undefined const txParams = {} - assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`) + assert.throws(() => { + txUtils.validateFrom(txParams) + }, Error, `Invalid from address ${txParams.from} not a string`) // where from is array txParams.from = [] - assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`) + assert.throws(() => { + txUtils.validateFrom(txParams) + }, Error, `Invalid from address ${txParams.from} not a string`) // where from is a object txParams.from = {} - assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`) + assert.throws(() => { + txUtils.validateFrom(txParams) + }, Error, `Invalid from address ${txParams.from} not a string`) // where from is a invalid address txParams.from = 'im going to fail' - assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address`) + assert.throws(() => { + txUtils.validateFrom(txParams) + }, Error, `Invalid from address`) // should run txParams.from = '0x1678a085c290ebd122dc42cba69373b5953b831d' diff --git a/test/unit/app/nodeify-test.js b/test/unit/app/nodeify-test.js index fa5e49fb2..fc06803db 100644 --- a/test/unit/app/nodeify-test.js +++ b/test/unit/app/nodeify-test.js @@ -33,7 +33,9 @@ describe('nodeify', function () { }) it('no callback - should asyncly throw an error if underlying function does', function (done) { - const nodified = nodeify(async () => { throw new Error('boom!') }, obj) + const nodified = nodeify(async () => { + throw new Error('boom!') + }, obj) process.prependOnceListener('uncaughtException', function (err) { assert.ok(err, 'got expected error') assert.ok(err.message.includes('boom!'), 'got expected error message') @@ -50,7 +52,9 @@ describe('nodeify', function () { const nodified = nodeify(() => 42) try { nodified((err, result) => { - if (err) return done(new Error(`should not have thrown any error: ${err.message}`)) + if (err) { + return done(new Error(`should not have thrown any error: ${err.message}`)) + } assert.equal(42, result, 'got expected result') }) done() @@ -60,10 +64,14 @@ describe('nodeify', function () { }) it('sync functions - handles errors', function (done) { - const nodified = nodeify(() => { throw new Error('boom!') }) + const nodified = nodeify(() => { + throw new Error('boom!') + }) try { nodified((err, result) => { - if (result) return done(new Error('should not have returned any result')) + if (result) { + return done(new Error('should not have returned any result')) + } assert.ok(err, 'got expected error') assert.ok(err.message.includes('boom!'), 'got expected error message') }) diff --git a/test/unit/app/typed-message-manager.spec.js b/test/unit/app/typed-message-manager.spec.js new file mode 100644 index 000000000..3db92d4a3 --- /dev/null +++ b/test/unit/app/typed-message-manager.spec.js @@ -0,0 +1,116 @@ +import assert from 'assert' +import sinon from 'sinon' +import NetworkController from '../../../app/scripts/controllers/network/index' +import TypedMessageManager from '../../../app/scripts/lib/typed-message-manager' + +describe('Typed Message Manager', () => { + let typedMessageManager, msgParamsV1, msgParamsV3, typedMsgs, messages, msgId, numberMsgId + + const address = '0xc42edfcc21ed14dda456aa0756c153f7985d8813' + const networkController = new NetworkController() + sinon.stub(networkController, 'getNetworkState').returns('1') + + beforeEach(() => { + typedMessageManager = new TypedMessageManager({ + networkController, + }) + + msgParamsV1 = { + from: address, + data: [ + { type: 'string', name: 'unit test', value: 'hello there' }, + { type: 'uint32', name: 'A number, but not really a number', value: '$$$' }, + ], + } + + msgParamsV3 = { + from: address, + data: JSON.stringify({ + 'types': { + 'EIP712Domain': [ + {'name': 'name', 'type': 'string' }, + {'name': 'version', 'type': 'string' }, + {'name': 'chainId', ' type': 'uint256' }, + {'name': 'verifyingContract', ' type': 'address' }, + ], + 'Person': [ + {'name': 'name', 'type': 'string' }, + {'name': 'wallet', ' type': 'address' }, + ], + 'Mail': [ + {'name': 'from', 'type': 'Person' }, + {'name': 'to', 'type': 'Person' }, + {'name': 'contents', 'type': 'string' }, + ], + }, + 'primaryType': 'Mail', + 'domain': { + 'name': 'Ether Mainl', + 'version': '1', + 'chainId': 1, + 'verifyingContract': '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + }, + 'message': { + 'from': { + 'name': 'Cow', + 'wallet': '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', + }, + 'to': { + 'name': 'Bob', + 'wallet': '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + }, + 'contents': 'Hello, Bob!', + }, + }), + } + + typedMessageManager.addUnapprovedMessage(msgParamsV3, 'V3') + typedMsgs = typedMessageManager.getUnapprovedMsgs() + messages = typedMessageManager.messages + msgId = Object.keys(typedMsgs)[0] + messages[0].msgParams.metamaskId = parseInt(msgId) + numberMsgId = parseInt(msgId) + }) + + it('supports version 1 of signedTypedData', () => { + typedMessageManager.addUnapprovedMessage(msgParamsV1, 'V1') + assert.equal(messages[messages.length - 1].msgParams.data, msgParamsV1.data) + }) + + it('has params address', function () { + assert.equal(typedMsgs[msgId].msgParams.from, address) + }) + + it('adds to unapproved messages and sets status to unapproved', function () { + assert.equal(typedMsgs[msgId].status, 'unapproved') + }) + + it('validates params', function () { + assert.doesNotThrow(() => { + typedMessageManager.validateParams(messages[0]) + }, 'Does not throw with valid parameters') + }) + + it('gets unapproved by id', function () { + const getMsg = typedMessageManager.getMsg(numberMsgId) + assert.equal(getMsg.id, numberMsgId) + }) + + it('approves messages', async function () { + const messageMetaMaskId = messages[0].msgParams + typedMessageManager.approveMessage(messageMetaMaskId) + assert.equal(messages[0].status, 'approved') + }) + + it('sets msg status to signed and adds a raw sig to message details', function () { + typedMessageManager.setMsgStatusSigned(numberMsgId, 'raw sig') + assert.equal(messages[0].status, 'signed') + assert.equal(messages[0].rawSig, 'raw sig') + }) + + it('rejects message', function () { + typedMessageManager.rejectMsg(numberMsgId) + assert.equal(messages[0].status, 'rejected') + }) + +}) diff --git a/test/unit/app/util-test.js b/test/unit/app/util-test.js index 259bd708b..a6a4a458c 100644 --- a/test/unit/app/util-test.js +++ b/test/unit/app/util-test.js @@ -18,11 +18,16 @@ describe('getEnvironmentType', function () { assert.equal(environmentType, ENVIRONMENT_TYPE_NOTIFICATION) }) - it('should return fullscreen type', function () { + it('should return fullscreen type for home.html', function () { const environmentType = getEnvironmentType('http://extension-id/home.html') assert.equal(environmentType, ENVIRONMENT_TYPE_FULLSCREEN) }) + it('should return fullscreen type for phishing.html', function () { + const environmentType = getEnvironmentType('http://extension-id/phishing.html') + assert.equal(environmentType, ENVIRONMENT_TYPE_FULLSCREEN) + }) + it('should return background type', function () { const environmentType = getEnvironmentType('http://extension-id/_generated_background_page.html') assert.equal(environmentType, ENVIRONMENT_TYPE_BACKGROUND) diff --git a/test/unit/migrations/023-test.js b/test/unit/migrations/023-test.js index 1b47dea92..7c93feefe 100644 --- a/test/unit/migrations/023-test.js +++ b/test/unit/migrations/023-test.js @@ -37,7 +37,9 @@ let nonDeletableCount = 0 let status while (transactions.length <= 100) { status = txStates[Math.floor(Math.random() * Math.floor(txStates.length - 1))] - if (!deletableTxStates.find((s) => s === status)) nonDeletableCount++ + if (!deletableTxStates.find((s) => s === status)) { + nonDeletableCount++ + } transactions.push({status}) } diff --git a/test/unit/migrations/024-test.js b/test/unit/migrations/024-test.js index 671c7f832..9a6a6661a 100644 --- a/test/unit/migrations/024-test.js +++ b/test/unit/migrations/024-test.js @@ -31,8 +31,11 @@ describe('storage is migrated successfully and the txParams.from are lowercase', .then((migratedData) => { const migratedTransactions = migratedData.data.TransactionController.transactions migratedTransactions.forEach((tx) => { - if (tx.status === 'unapproved') assert.equal(tx.txParams.from, '0x8acce2391c0d510a6c5e5d8f819a678f79b7e675') - else assert.equal(tx.txParams.from, '0x8aCce2391c0d510a6c5E5d8f819a678f79b7e675') + if (tx.status === 'unapproved') { + assert.equal(tx.txParams.from, '0x8acce2391c0d510a6c5e5d8f819a678f79b7e675') + } else { + assert.equal(tx.txParams.from, '0x8aCce2391c0d510a6c5E5d8f819a678f79b7e675') + } }) done() }).catch(done) diff --git a/test/unit/migrations/025-test.js b/test/unit/migrations/025-test.js index fa89bc54f..cabaecd41 100644 --- a/test/unit/migrations/025-test.js +++ b/test/unit/migrations/025-test.js @@ -32,8 +32,12 @@ describe('storage is migrated successfully and the txParams.from are lowercase', .then((migratedData) => { const migratedTransactions = migratedData.data.TransactionController.transactions migratedTransactions.forEach((tx) => { - if (tx.status === 'unapproved') assert(!tx.txParams.random) - if (tx.status === 'unapproved') assert(!tx.txParams.chainId) + if (tx.status === 'unapproved') { + assert(!tx.txParams.random) + } + if (tx.status === 'unapproved') { + assert(!tx.txParams.chainId) + } }) done() }).catch(done) diff --git a/test/unit/migrations/027-test.js b/test/unit/migrations/027-test.js index 3ec9f0c0e..767243a25 100644 --- a/test/unit/migrations/027-test.js +++ b/test/unit/migrations/027-test.js @@ -30,7 +30,9 @@ describe('migration #27', () => { const newTransactions = newStorage.data.TransactionController.transactions assert.equal(newTransactions.length, 6, 'transactions is expected to have the length of 6') newTransactions.forEach((txMeta) => { - if (txMeta.status === 'rejected') done(new Error('transaction was found with a status of rejected')) + if (txMeta.status === 'rejected') { + done(new Error('transaction was found with a status of rejected')) + } }) done() }) diff --git a/test/unit/migrations/029-test.js b/test/unit/migrations/029-test.js index 7f9b8a005..ca0996389 100644 --- a/test/unit/migrations/029-test.js +++ b/test/unit/migrations/029-test.js @@ -28,7 +28,9 @@ describe('storage is migrated successfully where transactions that are submitted assert(txMeta1.err.message.includes('too long'), 'error message assigned') txs.forEach((tx) => { - if (tx.id === 1) return + if (tx.id === 1) { + return + } assert.notEqual(tx.status, 'failed', 'other tx is not auto failed') }) diff --git a/test/unit/migrations/037-test.js b/test/unit/migrations/037-test.js new file mode 100644 index 000000000..5d82de308 --- /dev/null +++ b/test/unit/migrations/037-test.js @@ -0,0 +1,121 @@ +const assert = require('assert') +const migration37 = require('../../../app/scripts/migrations/037') + +describe('migration #37', () => { + it('should update the version metadata', (done) => { + const oldStorage = { + 'meta': { + 'version': 36, + }, + 'data': {}, + } + + migration37.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.meta, { + 'version': 37, + }) + done() + }) + .catch(done) + }) + + it('should transform old state to new format', (done) => { + const oldStorage = { + 'meta': {}, + 'data': { + 'AddressBookController': { + 'addressBook': { + '0x1De7e54679bfF0c23856FbF547b2394e723FCA91': { + address: '0x1De7e54679bfF0c23856FbF547b2394e723FCA91', + chainId: '4', + memo: '', + name: 'account 3', + }, + '0x32Be343B94f860124dC4fEe278FDCBD38C102D88': { + address: '0x32Be343B94f860124dC4fEe278FDCBD38C102D88', + chainId: '4', + memo: '', + name: 'account 2', + }, + // there are no repeated addresses by the current implementation + '0x1De7e54679bfF0c23856FbF547b2394e723FCA93': { + address: '0x1De7e54679bfF0c23856FbF547b2394e723FCA93', + chainId: '2', + memo: '', + name: 'account 2', + }, + }, + }, + }, + } + + migration37.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.data.AddressBookController.addressBook, { + '4': { + '0x1De7e54679bfF0c23856FbF547b2394e723FCA91': { + address: '0x1De7e54679bfF0c23856FbF547b2394e723FCA91', + chainId: '4', + isEns: false, + memo: '', + name: 'account 3', + }, + '0x32Be343B94f860124dC4fEe278FDCBD38C102D88': { + address: '0x32Be343B94f860124dC4fEe278FDCBD38C102D88', + chainId: '4', + isEns: false, + memo: '', + name: 'account 2', + }, + }, + '2': { + '0x1De7e54679bfF0c23856FbF547b2394e723FCA93': { + address: '0x1De7e54679bfF0c23856FbF547b2394e723FCA93', + chainId: '2', + isEns: false, + memo: '', + name: 'account 2', + }, + }, + }) + done() + }) + .catch(done) + }) + + it('ens validation test', (done) => { + const oldStorage = { + 'meta': {}, + 'data': { + 'AddressBookController': { + 'addressBook': { + '0x1De7e54679bfF0c23856FbF547b2394e723FCA91': { + address: '0x1De7e54679bfF0c23856FbF547b2394e723FCA91', + chainId: '4', + memo: '', + name: 'metamask.eth', + }, + }, + }, + }, + } + + migration37.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.data.AddressBookController.addressBook, { + '4': { + '0x1De7e54679bfF0c23856FbF547b2394e723FCA91': { + address: '0x1De7e54679bfF0c23856FbF547b2394e723FCA91', + chainId: '4', + isEns: true, + memo: '', + name: 'metamask.eth', + }, + }, + }) + done() + }) + .catch(done) + }) +}) diff --git a/test/unit/migrations/038-test.js b/test/unit/migrations/038-test.js new file mode 100644 index 000000000..15d14b7d3 --- /dev/null +++ b/test/unit/migrations/038-test.js @@ -0,0 +1,60 @@ +const assert = require('assert') +const migration38 = require('../../../app/scripts/migrations/038') + +describe('migration #38', () => { + it('should update the version metadata', (done) => { + const oldStorage = { + 'meta': { + 'version': 37, + }, + 'data': {}, + } + + migration38.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.meta, { + 'version': 38, + }) + done() + }) + .catch(done) + }) + + it('should add a fullScreenVsPopup property set to either "control" or "fullScreen"', (done) => { + const oldStorage = { + 'meta': {}, + 'data': {}, + } + + migration38.migrate(oldStorage) + .then((newStorage) => { + assert(newStorage.data.ABTestController.abTests.fullScreenVsPopup.match(/control|fullScreen/)) + done() + }) + .catch(done) + }) + + it('should leave the fullScreenVsPopup property unchanged if it exists', (done) => { + const oldStorage = { + 'meta': {}, + 'data': { + 'ABTestController': { + abTests: { + 'fullScreenVsPopup': 'fullScreen', + }, + }, + }, + } + + migration38.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.data.ABTestController, { + abTests: { + 'fullScreenVsPopup': 'fullScreen', + }, + }) + done() + }) + .catch(done) + }) +}) diff --git a/test/unit/migrations/039-test.js b/test/unit/migrations/039-test.js new file mode 100644 index 000000000..231d8fdee --- /dev/null +++ b/test/unit/migrations/039-test.js @@ -0,0 +1,419 @@ +const assert = require('assert') +const migration39 = require('../../../app/scripts/migrations/039') + +describe('migration #39', () => { + it('should update the version metadata', (done) => { + const oldStorage = { + 'meta': { + 'version': 38, + }, + 'data': {}, + } + + migration39.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.meta, { + 'version': 39, + }) + done() + }) + .catch(done) + }) + + it('should update old DAI token symbol to SAI in tokens', (done) => { + const oldStorage = { + 'meta': {}, + 'data': { + 'PreferencesController': { + 'tokens': [{ + 'address': '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', + 'decimals': 18, + 'symbol': 'DAI', + }, { + 'address': '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + 'symbol': 'BAT', + 'decimals': 18, + }, { + 'address': '0x617b3f8050a0bd94b6b1da02b4384ee5b4df13f4', + 'symbol': 'META', + 'decimals': 18, + }], + }, + }, + } + + migration39.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.data.PreferencesController, { + 'tokens': [{ + 'address': '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', + 'decimals': 18, + 'symbol': 'SAI', + }, { + 'address': '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + 'symbol': 'BAT', + 'decimals': 18, + }, { + 'address': '0x617b3f8050a0bd94b6b1da02b4384ee5b4df13f4', + 'symbol': 'META', + 'decimals': 18, + }], + }) + done() + }) + .catch(done) + }) + + it('should update old DAI token symbol to SAI in accountTokens', (done) => { + const oldStorage = { + 'meta': {}, + 'data': { + 'PreferencesController': { + 'accountTokens': { + '0x7250739de134d33ec7ab1ee592711e15098c9d2d': { + 'mainnet': [ + { + 'address': '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', + 'decimals': 18, + 'symbol': 'DAI', + }, + ], + }, + '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5': { + 'mainnet': [], + 'rinkeby': [], + }, + '0x8e5d75d60224ea0c33d1041e75de68b1c3cb6dd5': {}, + '0xb3958fb96c8201486ae20be1d5c9f58083df343a': { + 'mainnet': [ + { + 'address': '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', + 'decimals': 18, + 'symbol': 'DAI', + }, + { + 'address': '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + 'decimals': 18, + 'symbol': 'BAT', + }, + { + 'address': '0x617b3f8050a0bd94b6b1da02b4384ee5b4df13f4', + 'decimals': 18, + 'symbol': 'META', + }, + ], + }, + }, + }, + }, + } + + migration39.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.data.PreferencesController, { + 'accountTokens': { + '0x7250739de134d33ec7ab1ee592711e15098c9d2d': { + 'mainnet': [ + { + 'address': '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', + 'decimals': 18, + 'symbol': 'SAI', + }, + ], + }, + '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5': { + 'mainnet': [], + 'rinkeby': [], + }, + '0x8e5d75d60224ea0c33d1041e75de68b1c3cb6dd5': {}, + '0xb3958fb96c8201486ae20be1d5c9f58083df343a': { + 'mainnet': [ + { + 'address': '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', + 'decimals': 18, + 'symbol': 'SAI', + }, + { + 'address': '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + 'decimals': 18, + 'symbol': 'BAT', + }, + { + 'address': '0x617b3f8050a0bd94b6b1da02b4384ee5b4df13f4', + 'decimals': 18, + 'symbol': 'META', + }, + ], + }, + }, + }) + done() + }) + .catch(done) + }) + + it('should NOT change any state if accountTokens is not an object', (done) => { + const oldStorage = { + 'meta': {}, + 'data': { + 'PreferencesController': { + 'accountTokens': [], + }, + }, + } + + migration39.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.data, oldStorage.data) + done() + }) + .catch(done) + }) + + it('should NOT change any state if accountTokens is an object with invalid values', (done) => { + const oldStorage = { + 'meta': {}, + 'data': { + 'PreferencesController': { + 'accountTokens': { + '0x7250739de134d33ec7ab1ee592711e15098c9d2d': [ + { + 'address': '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', + 'decimals': 18, + 'symbol': 'DAI', + }, + ], + '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359': null, + '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5': { + 'mainnet': [ + null, + undefined, + [], + 42, + ], + 'rinkeby': null, + }, + }, + }, + }, + } + + migration39.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.data, oldStorage.data) + done() + }) + .catch(done) + }) + + it('should NOT change any state if accountTokens includes the new DAI token', (done) => { + const oldStorage = { + 'meta': {}, + 'data': { + 'PreferencesController': { + 'accountTokens': { + '0x7250739de134d33ec7ab1ee592711e15098c9d2d': { + 'mainnet': [ + { + 'address': '0x6B175474E89094C44Da98b954EedeAC495271d0F', + 'decimals': 18, + 'symbol': 'DAI', + }, + ], + }, + '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5': { + 'mainnet': [], + 'rinkeby': [], + }, + '0x8e5d75d60224ea0c33d1041e75de68b1c3cb6dd5': {}, + '0xb3958fb96c8201486ae20be1d5c9f58083df343a': { + 'mainnet': [ + { + 'address': '0x6B175474E89094C44Da98b954EedeAC495271d0F', + 'decimals': 18, + 'symbol': 'DAI', + }, + { + 'address': '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + 'decimals': 18, + 'symbol': 'BAT', + }, + { + 'address': '0x617b3f8050a0bd94b6b1da02b4384ee5b4df13f4', + 'decimals': 18, + 'symbol': 'META', + }, + ], + }, + }, + }, + }, + } + + migration39.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.data, oldStorage.data) + done() + }) + .catch(done) + }) + + it('should NOT change any state if tokens includes the new DAI token', (done) => { + const oldStorage = { + 'meta': {}, + 'data': { + 'PreferencesController': { + 'tokens': [{ + 'address': '0x6B175474E89094C44Da98b954EedeAC495271d0F', + 'symbol': 'DAI', + 'decimals': 18, + }, { + 'address': '0x617b3f8050a0bd94b6b1da02b4384ee5b4df13f4', + 'symbol': 'META', + 'decimals': 18, + }], + }, + }, + } + + migration39.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.data, oldStorage.data) + done() + }) + .catch(done) + }) + + it('should NOT change any state if tokens does not include DAI', (done) => { + const oldStorage = { + 'meta': {}, + 'data': { + 'PreferencesController': { + 'tokens': [{ + 'address': '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + 'symbol': 'BAT', + 'decimals': 18, + }, { + 'address': '0x617b3f8050a0bd94b6b1da02b4384ee5b4df13f4', + 'symbol': 'META', + 'decimals': 18, + }], + }, + }, + } + + migration39.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.data, oldStorage.data) + done() + }) + .catch(done) + }) + + it('should NOT change any state if a tokens property has invalid entries', (done) => { + const oldStorage = { + 'meta': {}, + 'data': { + 'PreferencesController': { + 'tokens': [ + null, + [], + undefined, + 42, + ], + }, + }, + } + + migration39.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.data, oldStorage.data) + done() + }) + .catch(done) + }) + + it('should NOT change any state if a tokens property is not an array', (done) => { + const oldStorage = { + 'meta': {}, + 'data': { + 'PreferencesController': { + 'tokens': {}, + }, + }, + } + + migration39.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.data, oldStorage.data) + done() + }) + .catch(done) + }) + + it('should NOT change any state if a tokens property is null', (done) => { + const oldStorage = { + 'meta': {}, + 'data': { + 'PreferencesController': { + 'tokens': null, + }, + }, + } + + migration39.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.data, oldStorage.data) + done() + }) + .catch(done) + }) + + it('should NOT change any state if a tokens property is missing', (done) => { + const oldStorage = { + 'meta': {}, + 'data': { + 'PreferencesController': { + }, + }, + } + + migration39.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.data, oldStorage.data) + done() + }) + .catch(done) + }) + + it('should NOT change any state if a accountTokens property is missing', (done) => { + const oldStorage = { + 'meta': {}, + 'data': { + 'PreferencesController': { + }, + }, + } + + migration39.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.data, oldStorage.data) + done() + }) + .catch(done) + }) + + it('should NOT change any state if PreferencesController is missing', (done) => { + const oldStorage = { + 'meta': {}, + 'data': {}, + } + + migration39.migrate(oldStorage) + .then((newStorage) => { + assert.deepEqual(newStorage.data, oldStorage.data) + done() + }) + .catch(done) + }) +}) diff --git a/test/unit/migrations/migrator-test.js b/test/unit/migrations/migrator-test.js index 3dcc5aff7..9a949def4 100644 --- a/test/unit/migrations/migrator-test.js +++ b/test/unit/migrations/migrator-test.js @@ -58,7 +58,9 @@ describe('Migrator', () => { it('should emit an error', function (done) { this.timeout(15000) - const migrator = new Migrator({ migrations: [{ version: 1, migrate: async () => { throw new Error('test') } } ] }) + const migrator = new Migrator({ migrations: [{ version: 1, migrate: async () => { + throw new Error('test') + } } ] }) migrator.on('error', () => done()) migrator.migrateData({ meta: {version: 0} }) .then(() => { diff --git a/test/unit/test-utils.js b/test/unit/test-utils.js index 7d0ae4d91..de508c504 100644 --- a/test/unit/test-utils.js +++ b/test/unit/test-utils.js @@ -10,7 +10,9 @@ async function assertRejects (asyncFn, regExp) { try { await asyncFn() } catch (error) { - f = () => { throw error } + f = () => { + throw error + } } finally { assert.throws(f, regExp) } diff --git a/test/unit/ui/app/actions.spec.js b/test/unit/ui/app/actions.spec.js index be966b3ec..9ed21e729 100644 --- a/test/unit/ui/app/actions.spec.js +++ b/test/unit/ui/app/actions.spec.js @@ -1,3 +1,4 @@ +/* eslint-disable */ // Used to inspect long objects // util.inspect({JSON}, false, null)) // const util = require('util') @@ -29,6 +30,8 @@ describe('Actions', () => { const noop = () => {} + const currentNetworkId = 42 + let background, metamaskController const TEST_SEED = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium' @@ -87,11 +90,9 @@ describe('Actions', () => { submitPasswordSpy = sinon.spy(background, 'submitPassword') verifySeedPhraseSpy = sinon.spy(background, 'verifySeedPhrase') - return store.dispatch(actions.tryUnlockMetamask()) - .then(() => { - assert(submitPasswordSpy.calledOnce) - assert(verifySeedPhraseSpy.calledOnce) - }) + await store.dispatch(actions.tryUnlockMetamask()) + assert(submitPasswordSpy.calledOnce) + assert(verifySeedPhraseSpy.calledOnce) }) it('errors on submitPassword will fail', async () => { @@ -192,15 +193,17 @@ describe('Actions', () => { describe('#requestRevealSeedWords', () => { let submitPasswordSpy - it('calls submitPassword in background', () => { + afterEach(() => { + submitPasswordSpy.restore() + }) + + it('calls submitPassword in background', async () => { const store = mockStore() submitPasswordSpy = sinon.spy(background, 'verifySeedPhrase') - return store.dispatch(actions.requestRevealSeedWords()) - .then(() => { - assert(submitPasswordSpy.calledOnce) - }) + await store.dispatch(actions.requestRevealSeedWords()) + assert(submitPasswordSpy.calledOnce) }) it('displays warning error message then callback in background errors', async () => { @@ -234,8 +237,9 @@ describe('Actions', () => { removeAccountSpy.restore() }) - it('calls removeAccount in background and expect actions to show account', () => { + it('calls removeAccount in background and expect actions to show account', async () => { const store = mockStore(devState) + const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'HIDE_LOADING_INDICATION' }, @@ -244,20 +248,20 @@ describe('Actions', () => { removeAccountSpy = sinon.spy(background, 'removeAccount') - return store.dispatch(actions.removeAccount('0xe18035bf8712672935fdb4e5e431b1a0183d2dfc')) - .then(() => { - assert(removeAccountSpy.calledOnce) - assert.deepEqual(store.getActions(), expectedActions) - }) + await store.dispatch(actions.removeAccount('0xe18035bf8712672935fdb4e5e431b1a0183d2dfc')) + assert(removeAccountSpy.calledOnce) + assert.deepEqual(store.getActions(), expectedActions) }) it('displays warning error message when removeAccount callback errors', async () => { const store = mockStore() + const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'HIDE_LOADING_INDICATION' }, { type: 'DISPLAY_WARNING', value: 'error' }, ] + removeAccountSpy = sinon.stub(background, 'removeAccount') removeAccountSpy.callsFake((_, callback) => { callback(new Error('error')) @@ -330,11 +334,9 @@ describe('Actions', () => { resetAccountSpy = sinon.spy(background, 'resetAccount') - return store.dispatch(actions.resetAccount()) - .then(() => { - assert(resetAccountSpy.calledOnce) - assert.deepEqual(store.getActions(), expectedActions) - }) + await store.dispatch(actions.resetAccount()) + assert(resetAccountSpy.calledOnce) + assert.deepEqual(store.getActions(), expectedActions) }) it('throws if resetAccount throws', async () => { @@ -375,10 +377,8 @@ describe('Actions', () => { const importPrivkey = 'c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3' - return store.dispatch(actions.importNewAccount('Private Key', [ importPrivkey ])) - .then(() => { - assert(importAccountWithStrategySpy.calledOnce) - }) + store.dispatch(actions.importNewAccount('Private Key', [ importPrivkey ])) + assert(importAccountWithStrategySpy.calledOnce) }) it('displays warning error message when importAccount in background callback errors', async () => { @@ -406,21 +406,181 @@ describe('Actions', () => { describe('#addNewAccount', () => { - let addNewAccountSpy + it('Adds a new account', () => { + const store = mockStore({ metamask: devState }) + + const addNewAccountSpy = sinon.spy(background, 'addNewAccount') + + store.dispatch(actions.addNewAccount()) + assert(addNewAccountSpy.calledOnce) + }) + + }) + + describe('#checkHardwareStatus', () => { + + let checkHardwareStatusSpy + + beforeEach(() => { + checkHardwareStatusSpy = sinon.stub(background, 'checkHardwareStatus') + }) afterEach(() => { - addNewAccountSpy.restore() + checkHardwareStatusSpy.restore() }) - it('Adds a new account', () => { - const store = mockStore({ metamask: devState }) + it('calls checkHardwareStatus in background', async () => { + + const store = mockStore() + + store.dispatch(await actions.checkHardwareStatus('ledger', `m/44'/60'/0'/0`)) + assert.equal(checkHardwareStatusSpy.calledOnce, true) + }) + + it('shows loading indicator and displays error', async () => { + const store = mockStore() + + const expectedActions = [ + { type: 'SHOW_LOADING_INDICATION', value: undefined }, + { type: 'DISPLAY_WARNING', value: 'error' }, + ] + + checkHardwareStatusSpy.callsFake((deviceName, hdPath, callback) => { + callback(new Error('error')) + }) + + try { + await store.dispatch(actions.checkHardwareStatus()) + assert.fail('Should have thrown error') + } catch (_) { + assert.deepEqual(store.getActions(), expectedActions) + } + }) + }) + + describe('#forgetDevice', () => { + + let forgetDeviceSpy + + beforeEach(() => { + forgetDeviceSpy = sinon.stub(background, 'forgetDevice') + }) + + afterEach(() => { + forgetDeviceSpy.restore() + }) + + it('calls forgetDevice in background', () => { + + const store = mockStore() + + store.dispatch(actions.forgetDevice('ledger')) + assert.equal(forgetDeviceSpy.calledOnce, true) + + }) + + it('shows loading indicator and displays error', async () => { + const store = mockStore() + + const expectedActions = [ + { type: 'SHOW_LOADING_INDICATION', value: undefined }, + { type: 'DISPLAY_WARNING', value: 'error' }, + ] + + forgetDeviceSpy.callsFake((deviceName, callback) => { + callback(new Error('error')) + }) + + try { + await store.dispatch(actions.forgetDevice()) + assert.fail('Should have thrown error') + } catch (_) { + assert.deepEqual(store.getActions(), expectedActions) + } + }) + }) + + describe('#connectHardware', () => { + + let connectHardwareSpy + + beforeEach(() => { + connectHardwareSpy = sinon.stub(background, 'connectHardware') + }) + + afterEach(() => { + connectHardwareSpy.restore() + }) + + it('calls connectHardware in background', () => { + + const store = mockStore() + + store.dispatch(actions.connectHardware('ledger', 0, `m/44'/60'/0'/0`)) + assert.equal(connectHardwareSpy.calledOnce, true) + + }) + + it('shows loading indicator and displays error', async () => { + const store = mockStore() + + const expectedActions = [ + { type: 'SHOW_LOADING_INDICATION', value: undefined }, + { type: 'DISPLAY_WARNING', value: 'error' }, + ] + + connectHardwareSpy.callsFake((deviceName, page, hdPath, callback) => { + callback(new Error('error')) + }) + + try { + await store.dispatch(actions.connectHardware()) + assert.fail('Should have thrown error') + } catch (_) { + assert.deepEqual(store.getActions(), expectedActions) + } + }) + }) + + describe('#unlockHardwareWalletAccount', () => { + + let unlockHardwareWalletAccountSpy + + beforeEach(() => { + unlockHardwareWalletAccountSpy = sinon.stub(background, 'unlockHardwareWalletAccount') + }) + + afterEach(() => { + unlockHardwareWalletAccountSpy.restore() + }) + + it('calls unlockHardwareWalletAccount in background', () => { + + const store = mockStore() + + store.dispatch(actions.unlockHardwareWalletAccount('ledger', 0, `m/44'/60'/0'/0`)) + assert.equal(unlockHardwareWalletAccountSpy.calledOnce, true) + + }) + + it('shows loading indicator and displays error', async() => { + const store = mockStore() + + const expectedActions = [ + { type: 'SHOW_LOADING_INDICATION', value: undefined }, + { type: 'DISPLAY_WARNING', value: 'error' }, + ] - addNewAccountSpy = sinon.spy(background, 'addNewAccount') + unlockHardwareWalletAccountSpy.callsFake((deviceName, page, hdPath, callback) => { + callback(new Error('error')) + }) - return store.dispatch(actions.addNewAccount()) - .then(() => { - assert(addNewAccountSpy.calledOnce) - }) + try { + await store.dispatch(actions.unlockHardwareWalletAccount()) + assert.fail('Should have thrown error') + } catch (error) { + assert.deepEqual(store.getActions(), expectedActions) + } }) }) @@ -484,11 +644,8 @@ describe('Actions', () => { const store = mockStore() signMessageSpy = sinon.spy(background, 'signMessage') - - return store.dispatch(actions.signMsg(msgParams)) - .then(() => { - assert(signMessageSpy.calledOnce) - }) + store.dispatch(actions.signMsg(msgParams)) + assert(signMessageSpy.calledOnce) }) @@ -542,10 +699,8 @@ describe('Actions', () => { signPersonalMessageSpy = sinon.spy(background, 'signPersonalMessage') - return store.dispatch(actions.signPersonalMsg(msgParams)) - .then(() => { - assert(signPersonalMessageSpy.calledOnce) - }) + store.dispatch(actions.signPersonalMsg(msgParams)) + assert(signPersonalMessageSpy.calledOnce) }) @@ -573,12 +728,100 @@ describe('Actions', () => { }) + describe('#signTypedMsg', () => { + let signTypedMsgSpy, messages, typedMessages, msgId + + const msgParamsV3 = { + from: '0x0DCD5D886577d5081B0c52e242Ef29E70Be3E7bc', + data: JSON.stringify({ + 'types': { + 'EIP712Domain': [ + {'name': 'name', 'type': 'string'}, + {'name': 'version', 'type': 'string'}, + {'name': 'chainId', 'type': 'uint256'}, + {'name': 'verifyingContract', 'type': 'address'}, + ], + 'Person': [ + {'name': 'name', 'type': 'string'}, + {'name': 'wallet', 'type': 'address'}, + ], + 'Mail': [ + {'name': 'from', 'type': 'Person'}, + {'name': 'to', 'type': 'Person'}, + {'name': 'contents', 'type': 'string'}, + ], + }, + 'primaryType': 'Mail', + 'domain': { + 'name': 'Ether Mainl', + 'version': '1', + 'chainId': 1, + 'verifyingContract': '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + }, + 'message': { + 'from': { + 'name': 'Cow', + 'wallet': '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', + }, + 'to': { + 'name': 'Bob', + 'wallet': '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + }, + 'contents': 'Hello, Bob!', + }, + }), + } + + beforeEach(() => { + metamaskController.newUnsignedTypedMessage(msgParamsV3, 'V3') + messages = metamaskController.typedMessageManager.getUnapprovedMsgs() + typedMessages = metamaskController.typedMessageManager.messages + msgId = Object.keys(messages)[0] + typedMessages[0].msgParams.metamaskId = parseInt(msgId) + }) + + afterEach(() => { + signTypedMsgSpy.restore() + }) + + it('calls signTypedMsg in background with no error', () => { + const store = mockStore() + signTypedMsgSpy = sinon.stub(background, 'signTypedMessage') + + store.dispatch(actions.signTypedMsg(msgParamsV3)) + assert(signTypedMsgSpy.calledOnce) + }) + + it('returns expected actions with error', async () => { + const store = mockStore() + const expectedActions = [ + { type: 'SHOW_LOADING_INDICATION', value: undefined }, + { type: 'UPDATE_METAMASK_STATE', value: undefined }, + { type: 'HIDE_LOADING_INDICATION' }, + { type: 'DISPLAY_WARNING', value: 'error' }, + ] + + signTypedMsgSpy = sinon.stub(background, 'signTypedMessage') + + signTypedMsgSpy.callsFake((_, callback) => { + callback(new Error('error')) + }) + + try { + await store.dispatch(actions.signTypedMsg()) + assert.fail('Should have thrown error') + } catch (_) { + assert.deepEqual(store.getActions(), expectedActions) + } + }) + + }) + describe('#signTx', () => { let sendTransactionSpy beforeEach(() => { - global.ethQuery = new EthQuery(provider) sendTransactionSpy = sinon.stub(global.ethQuery, 'sendTransaction') }) @@ -588,6 +831,7 @@ describe('Actions', () => { it('calls sendTransaction in global ethQuery', () => { const store = mockStore() + store.dispatch(actions.signTx()) assert(sendTransactionSpy.calledOnce) }) @@ -607,6 +851,71 @@ describe('Actions', () => { }) }) + describe('#updatedGasData', () => { + it('errors when get code does not return', async () => { + const store = mockStore() + + const expectedActions = [ + { type: 'GAS_LOADING_STARTED' }, + { type: 'UPDATE_SEND_ERRORS', value: { gasLoadingError: 'gasLoadingError' } }, + { type: 'GAS_LOADING_FINISHED' }, + ] + + const mockData = { + gasPrice: '0x3b9aca00', // + blockGasLimit: '0x6ad79a', // 7002010 + selectedAddress: '0x0DCD5D886577d5081B0c52e242Ef29E70Be3E7bc', + to: '0xEC1Adf982415D2Ef5ec55899b9Bfb8BC0f29251B', + value: '0xde0b6b3a7640000', // 1000000000000000000 + } + + try { + await store.dispatch(actions.updateGasData(mockData)) + assert.fail('Should have thrown error') + } catch (error) { + assert.deepEqual(store.getActions(), expectedActions) + } + }) + }) + + describe('#updatedGasData', () => { + + const stub = sinon.stub().returns('0x') + + const mockData = { + gasPrice: '0x3b9aca00', // + blockGasLimit: '0x6ad79a', // 7002010 + selectedAddress: '0x0DCD5D886577d5081B0c52e242Ef29E70Be3E7bc', + to: '0xEC1Adf982415D2Ef5ec55899b9Bfb8BC0f29251B', + value: '0xde0b6b3a7640000', // 1000000000000000000 + } + + beforeEach(() => { + global.eth = { + getCode: stub, + } + }) + + afterEach(() => { + stub.reset() + }) + + it('returns default gas limit for basic eth transaction', async () => { + const store = mockStore() + + const expectedActions = [ + { type: 'GAS_LOADING_STARTED' }, + { type: 'UPDATE_GAS_LIMIT', value: '0x5208' }, + { type: 'metamask/gas/SET_CUSTOM_GAS_LIMIT', value: '0x5208' }, + { type: 'UPDATE_SEND_ERRORS', value: { gasLoadingError: null } }, + { type: 'GAS_LOADING_FINISHED' }, + ] + + await store.dispatch(actions.updateGasData(mockData)) + assert.deepEqual(store.getActions(), expectedActions) + }) + }) + describe('#signTokenTx', () => { let tokenSpy @@ -627,6 +936,61 @@ describe('Actions', () => { }) }) + describe('#updateTransaction', () => { + + let updateTransactionSpy, updateTransactionParamsSpy + + const txParams = { + 'from': '0x1', + 'gas': '0x5208', + 'gasPrice': '0x3b9aca00', + 'to': '0x2', + 'value': '0x0', + } + + const txData = { id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: txParams } + + beforeEach( async () => { + await metamaskController.txController.txStateManager.addTx(txData) + }) + + afterEach(() => { + updateTransactionSpy.restore() + updateTransactionParamsSpy.restore() + }) + + it('updates transaction', async () => { + const store = mockStore() + + updateTransactionSpy = sinon.spy(background, 'updateTransaction') + updateTransactionParamsSpy = sinon.spy(actions, 'updateTransactionParams') + + const result = [ txData.id, txParams ] + + await store.dispatch(actions.updateTransaction(txData)) + assert(updateTransactionSpy.calledOnce) + assert(updateTransactionParamsSpy.calledOnce) + + assert.deepEqual(updateTransactionParamsSpy.args[0], result) + }) + + it('rejects with error message', async () => { + const store = mockStore() + + updateTransactionSpy = sinon.stub(background, 'updateTransaction') + updateTransactionSpy.callsFake((res, callback) => { + callback(new Error('error')) + }) + + try { + await store.dispatch(actions.updateTransaction(txData)) + assert.fail('Should have thrown error') + } catch (error) { + assert.equal(error.message, 'error') + } + }) + }) + describe('#lockMetamask', () => { let backgroundSetLockedSpy @@ -634,18 +998,16 @@ describe('Actions', () => { backgroundSetLockedSpy.restore() }) - it('calls setLocked', () => { + it('calls setLocked', async () => { const store = mockStore() backgroundSetLockedSpy = sinon.spy(background, 'setLocked') - return store.dispatch(actions.lockMetamask()) - .then(() => { - assert(backgroundSetLockedSpy.calledOnce) - }) + await store.dispatch(actions.lockMetamask()) + assert(backgroundSetLockedSpy.calledOnce) }) - it('returns display warning error with value when setLocked in background callback errors', () => { + it('returns display warning error with value when setLocked in background callback errors', async () => { const store = mockStore() const expectedActions = [ @@ -659,10 +1021,13 @@ describe('Actions', () => { callback(new Error('error')) }) - return store.dispatch(actions.lockMetamask()) - .then(() => { - assert.deepEqual(store.getActions(), expectedActions) - }) + try { + await store.dispatch(actions.lockMetamask()) + assert.fail('Should have thrown error') + } catch (error) { + assert.deepEqual(store.getActions(), expectedActions) + } + }) }) @@ -747,13 +1112,11 @@ describe('Actions', () => { addTokenSpy.restore() }) - it('calls addToken in background', () => { + it('calls addToken in background', async () => { const store = mockStore() store.dispatch(actions.addToken()) - .then(() => { - assert(addTokenSpy.calledOnce) - }) + assert(addTokenSpy.calledOnce) }) it('errors when addToken in background throws', async () => { @@ -789,12 +1152,10 @@ describe('Actions', () => { removeTokenSpy.restore() }) - it('calls removeToken in background', () => { + it('calls removeToken in background', async () => { const store = mockStore() - store.dispatch(actions.removeToken()) - .then(() => { - assert(removeTokenSpy.calledOnce) - }) + store.dispatch(await actions.removeToken()) + assert(removeTokenSpy.calledOnce) }) it('errors when removeToken in background fails', async () => { @@ -909,7 +1270,7 @@ describe('Actions', () => { exportAccountSpy.restore() }) - it('returns expected actions for successful action', () => { + it('returns expected actions for successful action', async () => { const store = mockStore(devState) const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, @@ -920,12 +1281,10 @@ describe('Actions', () => { submitPasswordSpy = sinon.spy(background, 'submitPassword') exportAccountSpy = sinon.spy(background, 'exportAccount') - return store.dispatch(actions.exportAccount(password, '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc')) - .then(() => { - assert(submitPasswordSpy.calledOnce) - assert(exportAccountSpy.calledOnce) - assert.deepEqual(store.getActions(), expectedActions) - }) + await store.dispatch(actions.exportAccount(password, '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc')) + assert(submitPasswordSpy.calledOnce) + assert(exportAccountSpy.calledOnce) + assert.deepEqual(store.getActions(), expectedActions) }) it('returns action errors when first func callback errors', async () => { @@ -1081,9 +1440,7 @@ describe('Actions', () => { getTransactionCountSpy = sinon.spy(global.ethQuery, 'getTransactionCount') store.dispatch(actions.updateNetworkNonce()) - .then(() => { - assert(getTransactionCountSpy.calledOnce) - }) + assert(getTransactionCountSpy.calledOnce) }) it('errors when getTransactionCount throws', async () => { @@ -1154,7 +1511,7 @@ describe('Actions', () => { fetchMock.restore() }) - it('calls expected actions', () => { + it('calls expected actions', async () => { const store = mockStore() setCurrentLocaleSpy = sinon.spy(background, 'setCurrentLocale') @@ -1164,14 +1521,12 @@ describe('Actions', () => { { type: 'HIDE_LOADING_INDICATION' }, ] - return store.dispatch(actions.updateCurrentLocale('en')) - .then(() => { - assert(setCurrentLocaleSpy.calledOnce) - assert.deepEqual(store.getActions(), expectedActions) - }) + await store.dispatch(actions.updateCurrentLocale('en')) + assert(setCurrentLocaleSpy.calledOnce) + assert.deepEqual(store.getActions(), expectedActions) }) - it('calls expected actions', () => { + it('errors when setCurrentLocale throws', async () => { const store = mockStore() const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, @@ -1183,48 +1538,54 @@ describe('Actions', () => { callback(new Error('error')) }) - return store.dispatch(actions.updateCurrentLocale('en')) - .then(() => { - assert.deepEqual(store.getActions(), expectedActions) - }) + try { + await store.dispatch(actions.updateCurrentLocale('en')) + assert.fail('Should have thrown error') + } catch (_) { + assert.deepEqual(store.getActions(), expectedActions) + } + }) }) describe('#markPasswordForgotten', () => { - let markPasswordForgottenSpy + let markPasswordForgottenSpy, forgotPasswordSpy beforeEach(() => { - markPasswordForgottenSpy = sinon.stub(background, 'markPasswordForgotten') + markPasswordForgottenSpy = sinon.spy(background, 'markPasswordForgotten') + forgotPasswordSpy = sinon.spy(actions, 'forgotPassword') }) afterEach(() => { markPasswordForgottenSpy.restore() + forgotPasswordSpy.restore() }) it('calls markPasswordForgotten', () => { const store = mockStore() store.dispatch(actions.markPasswordForgotten()) + assert(forgotPasswordSpy.calledOnce) assert(markPasswordForgottenSpy.calledOnce) }) }) describe('#unMarkPasswordForgotten', () => { - let unMarkPasswordForgottenSpy + let unMarkPasswordForgottenSpy, forgotPasswordSpy beforeEach(() => { - unMarkPasswordForgottenSpy = sinon.stub(background, 'unMarkPasswordForgotten') + unMarkPasswordForgottenSpy = sinon.stub(background, 'unMarkPasswordForgotten').returns(forgotPasswordSpy) + forgotPasswordSpy = sinon.spy(actions, 'forgotPassword') }) afterEach(() => { unMarkPasswordForgottenSpy.restore() + forgotPasswordSpy.restore() }) - it('calls unMarkPasswordForgotten', () => { + it('calls unMarkPasswordForgotten', async () => { const store = mockStore() - store.dispatch(actions.unMarkPasswordForgotten()) + store.dispatch(await actions.unMarkPasswordForgotten()) assert(unMarkPasswordForgottenSpy.calledOnce) }) }) - - }) diff --git a/test/unit/ui/app/components/token-cell.spec.js b/test/unit/ui/app/components/token-cell.spec.js index d26201f32..acc48b529 100644 --- a/test/unit/ui/app/components/token-cell.spec.js +++ b/test/unit/ui/app/components/token-cell.spec.js @@ -44,39 +44,37 @@ describe('Token Cell', () => { const mockStore = configureMockStore(middlewares) const store = mockStore(state) - describe('normal tokens', () => { - beforeEach(() => { - wrapper = mount( - - - - ) - }) + beforeEach(() => { + wrapper = mount( + + + + ) + }) - it('renders Identicon with props from token cell', () => { - assert.equal(wrapper.find(Identicon).prop('address'), '0xAnotherToken') - assert.equal(wrapper.find(Identicon).prop('network'), 'test') - assert.equal(wrapper.find(Identicon).prop('image'), './test-image') - }) + it('renders Identicon with props from token cell', () => { + assert.equal(wrapper.find(Identicon).prop('address'), '0xAnotherToken') + assert.equal(wrapper.find(Identicon).prop('network'), 'test') + assert.equal(wrapper.find(Identicon).prop('image'), './test-image') + }) - it('renders token balance', () => { - assert.equal(wrapper.find('.token-list-item__token-balance').text(), '5.000') - }) + it('renders token balance', () => { + assert.equal(wrapper.find('.token-list-item__token-balance').text(), '5.000') + }) - it('renders token symbol', () => { - assert.equal(wrapper.find('.token-list-item__token-symbol').text(), 'TEST') - }) + it('renders token symbol', () => { + assert.equal(wrapper.find('.token-list-item__token-symbol').text(), 'TEST') + }) - it('renders converted fiat amount', () => { - assert.equal(wrapper.find('.token-list-item__fiat-amount').text(), '0.52 USD') - }) + it('renders converted fiat amount', () => { + assert.equal(wrapper.find('.token-list-item__fiat-amount').text(), '0.52 USD') }) describe('custom tokens as from a plugin', () => { @@ -84,11 +82,11 @@ describe('Token Cell', () => { wrapper = mount( , diff --git a/test/unit/ui/app/selectors.spec.js b/test/unit/ui/app/selectors.spec.js index a190462b0..f8d58f61c 100644 --- a/test/unit/ui/app/selectors.spec.js +++ b/test/unit/ui/app/selectors.spec.js @@ -109,12 +109,6 @@ describe('Selectors', function () { assert.equal(currentAccountwithSendEther.name, 'Test Account') }) - describe('#transactionSelector', function () { - it('returns transactions from state', function () { - selectors.transactionsSelector(mockState) - }) - }) - it('#getGasIsLoading', () => { const gasIsLoading = selectors.getGasIsLoading(mockState) assert.equal(gasIsLoading, false) diff --git a/test/unit/util_test.js b/test/unit/util_test.js index 87f57b218..768288ce7 100644 --- a/test/unit/util_test.js +++ b/test/unit/util_test.js @@ -7,7 +7,9 @@ var util = require(path.join(__dirname, '..', '..', 'ui', 'app', 'helpers', 'uti describe('util', function () { var ethInWei = '1' - for (var i = 0; i < 18; i++) { ethInWei += '0' } + for (var i = 0; i < 18; i++) { + ethInWei += '0' + } beforeEach(function () { this.sinon = sinon.createSandbox() diff --git a/ui/app/components/app/account-details/account-details.component.js b/ui/app/components/app/account-details/account-details.component.js index ecf2f9428..55078cee0 100644 --- a/ui/app/components/app/account-details/account-details.component.js +++ b/ui/app/components/app/account-details/account-details.component.js @@ -76,7 +76,7 @@ export default class AccountDetails extends Component {
diff --git a/ui/app/components/app/account-menu/index.scss b/ui/app/components/app/account-menu/index.scss index 435dd6b2a..614e19104 100644 --- a/ui/app/components/app/account-menu/index.scss +++ b/ui/app/components/app/account-menu/index.scss @@ -2,7 +2,7 @@ position: fixed; z-index: 100; top: 58px; - width: 310px; + width: 320px; @media screen and (max-width: 575px) { right: calc(((100vw - 100%) / 2) + 8px); @@ -58,6 +58,7 @@ max-height: 256px; position: relative; z-index: 200; + scrollbar-width: none; &::-webkit-scrollbar { display: none; diff --git a/ui/app/components/app/app-header/app-header.component.js b/ui/app/components/app/app-header/app-header.component.js index 7bf7a39bd..e1bc0cf24 100644 --- a/ui/app/components/app/app-header/app-header.component.js +++ b/ui/app/components/app/app-header/app-header.component.js @@ -71,7 +71,7 @@ export default class AppHeader extends PureComponent { ) @@ -92,7 +92,7 @@ export default class AppHeader extends PureComponent { className={classnames('app-header', { 'app-header--back-drop': isUnlocked })}>
history.push(DEFAULT_ROUTE)} />
diff --git a/ui/app/components/app/confirm-page-container/confirm-detail-row/index.scss b/ui/app/components/app/confirm-page-container/confirm-detail-row/index.scss index 1672ef8c6..80eb9cce6 100644 --- a/ui/app/components/app/confirm-page-container/confirm-detail-row/index.scss +++ b/ui/app/components/app/confirm-page-container/confirm-detail-row/index.scss @@ -47,4 +47,11 @@ .advanced-gas-inputs__gas-edit-rows { margin-bottom: 16px; } + + .custom-nonce-input { + input { + width: 90px; + font-size: 1rem; + } + } } diff --git a/ui/app/components/app/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js b/ui/app/components/app/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js index 95ca8144a..d057bd449 100644 --- a/ui/app/components/app/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js +++ b/ui/app/components/app/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js @@ -14,15 +14,15 @@ describe('Confirm Detail Row Component', function () { beforeEach(() => { wrapper = shallow( ) }) diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container-content/index.scss b/ui/app/components/app/confirm-page-container/confirm-page-container-content/index.scss index 602a46848..ebc252e73 100644 --- a/ui/app/components/app/confirm-page-container/confirm-page-container-content/index.scss +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-content/index.scss @@ -4,6 +4,7 @@ .confirm-page-container-content { overflow-y: auto; + height: 100%; flex: 1; &__error-container { diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js b/ui/app/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js index 84ca40da5..898d59068 100644 --- a/ui/app/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js @@ -5,20 +5,24 @@ import { ENVIRONMENT_TYPE_NOTIFICATION, } from '../../../../../../app/scripts/lib/enums' import NetworkDisplay from '../../network-display' +import Identicon from '../../../ui/identicon' +import { addressSlicer } from '../../../../helpers/utils/util' -export default class ConfirmPageContainer extends Component { +export default class ConfirmPageContainerHeader extends Component { static contextTypes = { t: PropTypes.func, } static propTypes = { + accountAddress: PropTypes.string, + showAccountInHeader: PropTypes.bool, showEdit: PropTypes.bool, onEdit: PropTypes.func, children: PropTypes.node, } renderTop () { - const { onEdit, showEdit } = this.props + const { onEdit, showEdit, accountAddress, showAccountInHeader } = this.props const windowType = window.METAMASK_UI_TYPE const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION && windowType !== ENVIRONMENT_TYPE_POPUP @@ -29,22 +33,39 @@ export default class ConfirmPageContainer extends Component { return (
-
- - onEdit()} + { !showAccountInHeader + ?
- { this.context.t('edit') } - -
+ + onEdit()} + > + { this.context.t('edit') } + +
+ : null + } + { showAccountInHeader + ?
+
+ +
+
+ { addressSlicer(accountAddress) } +
+
+ : null + } { !isFullScreen && }
) diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container-header/index.scss b/ui/app/components/app/confirm-page-container/confirm-page-container-header/index.scss index 44c721446..fb24feb58 100644 --- a/ui/app/components/app/confirm-page-container/confirm-page-container-header/index.scss +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-header/index.scss @@ -9,6 +9,7 @@ border-bottom: 1px solid $geyser; padding: 4px 13px 4px 13px; flex: 0 0 auto; + align-items: center; } &__back-button-container { @@ -28,4 +29,16 @@ font-weight: 400; padding-left: 5px; } + + &__address-container { + display: flex; + align-items: center; + margin-top: 2px; + margin-bottom: 2px; + } + + &__address { + margin-left: 6px; + font-size: 14px; + } } diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container.component.js b/ui/app/components/app/confirm-page-container/confirm-page-container.component.js index 99ba74949..0f8899634 100644 --- a/ui/app/components/app/confirm-page-container/confirm-page-container.component.js +++ b/ui/app/components/app/confirm-page-container/confirm-page-container.component.js @@ -19,11 +19,14 @@ export default class ConfirmPageContainer extends Component { subtitleComponent: PropTypes.node, title: PropTypes.string, titleComponent: PropTypes.node, + hideSenderToRecipient: PropTypes.bool, + showAccountInHeader: PropTypes.bool, // Sender to Recipient fromAddress: PropTypes.string, fromName: PropTypes.string, toAddress: PropTypes.string, toName: PropTypes.string, + toEns: PropTypes.string, toNickname: PropTypes.string, recipientAudit: PropTypes.object, // Content @@ -70,6 +73,7 @@ export default class ConfirmPageContainer extends Component { fromName, fromAddress, toName, + toEns, toNickname, toAddress, disabled, @@ -104,6 +108,8 @@ export default class ConfirmPageContainer extends Component { ofText, requestsWaitingText, recipientAudit, + hideSenderToRecipient, + showAccountInHeader, } = this.props const renderAssetImage = contentComponent || (!contentComponent && !identiconAddress) @@ -124,16 +130,22 @@ export default class ConfirmPageContainer extends Component { onEdit()} + showAccountInHeader={showAccountInHeader} + accountAddress={fromAddress} > - + { hideSenderToRecipient + ? null + : + } { contentComponent || ( diff --git a/ui/app/components/app/confirm-page-container/index.scss b/ui/app/components/app/confirm-page-container/index.scss index c0277eff5..3fc72c3a6 100644 --- a/ui/app/components/app/confirm-page-container/index.scss +++ b/ui/app/components/app/confirm-page-container/index.scss @@ -5,3 +5,9 @@ @import 'confirm-detail-row/index'; @import 'confirm-page-container-navigation/index'; + +.page-container { + &__content-component-wrapper { + height: 100%; + } +} diff --git a/ui/app/components/app/dai-migration-component/dai-migration-notification.component.js b/ui/app/components/app/dai-migration-component/dai-migration-notification.component.js new file mode 100644 index 000000000..d26358df7 --- /dev/null +++ b/ui/app/components/app/dai-migration-component/dai-migration-notification.component.js @@ -0,0 +1,78 @@ +import { DateTime } from 'luxon' +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import HomeNotification from '../home-notification' + +export default class DaiV1MigrationNotification extends PureComponent { + static contextTypes = { + t: PropTypes.func, + } + + static defaultProps = { + mkrMigrationReminderTimestamp: null, + string: '', + symbol: '', + } + + static propTypes = { + setMkrMigrationReminderTimestamp: PropTypes.func.isRequired, + mkrMigrationReminderTimestamp: PropTypes.string, + string: PropTypes.string, + symbol: PropTypes.string, + } + + remindMeLater = () => { + const nextWeek = DateTime.utc().plus({ + days: 7, + }) + this.props.setMkrMigrationReminderTimestamp(nextWeek.toString()) + } + + render () { + const { t } = this.context + const { mkrMigrationReminderTimestamp, string: balanceString, symbol } = this.props + + if (mkrMigrationReminderTimestamp) { + const reminderDateTime = DateTime.fromISO(mkrMigrationReminderTimestamp, { + zone: 'UTC', + }) + if (reminderDateTime > DateTime.utc()) { + return null + } + } + + if (!balanceString || !symbol) { + return null + } + + if (balanceString === '0') { + return null + } + + return ( + + {t('migrateSai')} +   +
{ + window.open('https://blog.makerdao.com/multi-collateral-dai-is-live/', '_blank', 'noopener') + }} + > + {t('learnMore')}. + +
+ )} + acceptText={t('migrate')} + onAccept={() => { + window.open('https://migrate.makerdao.com', '_blank', 'noopener') + }} + ignoreText={t('remindMeLater')} + onIgnore={this.remindMeLater} + infoText={t('migrateSaiInfo')} + /> + ) + } +} diff --git a/ui/app/components/app/dai-migration-component/dai-migration-notification.container.js b/ui/app/components/app/dai-migration-component/dai-migration-notification.container.js new file mode 100644 index 000000000..175083bce --- /dev/null +++ b/ui/app/components/app/dai-migration-component/dai-migration-notification.container.js @@ -0,0 +1,34 @@ +import { connect } from 'react-redux' +import { compose } from 'recompose' +import DaiMigrationNotification from './dai-migration-notification.component' +import withTokenTracker from '../../../helpers/higher-order-components/with-token-tracker' +import { getSelectedAddress, getDaiV1Token } from '../../../selectors/selectors' +import { setMkrMigrationReminderTimestamp } from '../../../store/actions' + +const mapStateToProps = (state) => { + const { + metamask: { + mkrMigrationReminderTimestamp, + }, + } = state + + const userAddress = getSelectedAddress(state) + const oldDai = getDaiV1Token(state) + + return { + mkrMigrationReminderTimestamp, + userAddress, + token: oldDai, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + setMkrMigrationReminderTimestamp: (t) => dispatch(setMkrMigrationReminderTimestamp(t)), + } +} + +export default compose( + connect(mapStateToProps, mapDispatchToProps), + withTokenTracker, +)(DaiMigrationNotification) diff --git a/ui/app/components/app/dai-migration-component/index.js b/ui/app/components/app/dai-migration-component/index.js new file mode 100644 index 000000000..e3c7cec2b --- /dev/null +++ b/ui/app/components/app/dai-migration-component/index.js @@ -0,0 +1 @@ +export { default } from './dai-migration-notification.container' diff --git a/ui/app/components/app/dropdowns/components/dropdown.js b/ui/app/components/app/dropdowns/components/dropdown.js index 149f063a7..cc966ffa5 100644 --- a/ui/app/components/app/dropdowns/components/dropdown.js +++ b/ui/app/components/app/dropdowns/components/dropdown.js @@ -4,8 +4,6 @@ const h = require('react-hyperscript') const MenuDroppo = require('../../menu-droppo') const extend = require('xtend') -const noop = () => {} - class Dropdown extends Component { render () { const { @@ -55,8 +53,6 @@ class Dropdown extends Component { } Dropdown.defaultProps = { - isOpen: false, - onClick: noop, useCssTransition: false, } diff --git a/ui/app/components/app/dropdowns/components/menu.js b/ui/app/components/app/dropdowns/components/menu.js index 63501eaa9..950ccccb7 100644 --- a/ui/app/components/app/dropdowns/components/menu.js +++ b/ui/app/components/app/dropdowns/components/menu.js @@ -3,7 +3,9 @@ const Component = require('react').Component const h = require('react-hyperscript') inherits(Menu, Component) -function Menu () { Component.call(this) } +function Menu () { + Component.call(this) +} Menu.prototype.render = function () { const { className = '', children, isShowing } = this.props @@ -13,7 +15,9 @@ Menu.prototype.render = function () { } inherits(Item, Component) -function Item () { Component.call(this) } +function Item () { + Component.call(this) +} Item.prototype.render = function () { const { @@ -37,14 +41,18 @@ Item.prototype.render = function () { } inherits(Divider, Component) -function Divider () { Component.call(this) } +function Divider () { + Component.call(this) +} Divider.prototype.render = function () { return h('div.menu__divider') } inherits(CloseArea, Component) -function CloseArea () { Component.call(this) } +function CloseArea () { + Component.call(this) +} CloseArea.prototype.render = function () { return h('div.menu__close-area', { onClick: this.props.onClick }) diff --git a/ui/app/components/app/dropdowns/network-dropdown.js b/ui/app/components/app/dropdowns/network-dropdown.js index e6a24ef11..d36f10d40 100644 --- a/ui/app/components/app/dropdowns/network-dropdown.js +++ b/ui/app/components/app/dropdowns/network-dropdown.js @@ -370,7 +370,9 @@ NetworkDropdown.prototype.renderCustomOption = function (provider) { const props = this.props const network = props.network - if (type !== 'rpc') return null + if (type !== 'rpc') { + return null + } switch (rpcTarget) { diff --git a/ui/app/components/app/dropdowns/tests/dropdown.test.js b/ui/app/components/app/dropdowns/tests/dropdown.test.js index cba4b30b7..f7c1e31b1 100644 --- a/ui/app/components/app/dropdowns/tests/dropdown.test.js +++ b/ui/app/components/app/dropdowns/tests/dropdown.test.js @@ -12,9 +12,9 @@ describe('Dropdown', () => { beforeEach(() => { wrapper = shallow( ) diff --git a/ui/app/components/app/dropdowns/tests/menu.test.js b/ui/app/components/app/dropdowns/tests/menu.test.js index 9f5f13f00..6413c0c2c 100644 --- a/ui/app/components/app/dropdowns/tests/menu.test.js +++ b/ui/app/components/app/dropdowns/tests/menu.test.js @@ -11,7 +11,7 @@ describe('Dropdown Menu Components', () => { beforeEach(() => { wrapper = shallow( - + ) }) @@ -29,10 +29,10 @@ describe('Dropdown Menu Components', () => { beforeEach(() => { wrapper = shallow( ) }) @@ -74,7 +74,7 @@ describe('Dropdown Menu Components', () => { beforeEach(() => { wrapper = shallow() }) diff --git a/ui/app/components/app/dropdowns/tests/network-dropdown-icon.test.js b/ui/app/components/app/dropdowns/tests/network-dropdown-icon.test.js index 67b192c11..ed7b7e253 100644 --- a/ui/app/components/app/dropdowns/tests/network-dropdown-icon.test.js +++ b/ui/app/components/app/dropdowns/tests/network-dropdown-icon.test.js @@ -8,10 +8,10 @@ describe('Network Dropdown Icon', () => { beforeEach(() => { wrapper = shallow() }) 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 7b87b3033..7fb5aa6f4 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 @@ -3,27 +3,16 @@ import PropTypes from 'prop-types' import classnames from 'classnames' import debounce from 'lodash.debounce' -export default class AdvancedTabContent extends Component { +export default class AdvancedGasInputs extends Component { static contextTypes = { t: PropTypes.func, } - constructor (props) { - super(props) - this.state = { - gasPrice: this.props.customGasPrice, - gasLimit: this.props.customGasLimit, - } - this.changeGasPrice = debounce(this.changeGasPrice, 500) - this.changeGasLimit = debounce(this.changeGasLimit, 500) - } - - static propTypes = { updateCustomGasPrice: PropTypes.func, updateCustomGasLimit: PropTypes.func, - customGasPrice: PropTypes.number, - customGasLimit: PropTypes.number, + customGasPrice: PropTypes.number.isRequired, + customGasLimit: PropTypes.number.isRequired, insufficientBalance: PropTypes.bool, customPriceIsSafe: PropTypes.bool, isSpeedUp: PropTypes.bool, @@ -31,6 +20,16 @@ export default class AdvancedTabContent extends Component { showGasLimitInfoModal: PropTypes.func, } + constructor (props) { + super(props) + this.state = { + gasPrice: this.props.customGasPrice, + gasLimit: this.props.customGasLimit, + } + this.changeGasPrice = debounce(this.changeGasPrice, 500) + this.changeGasLimit = debounce(this.changeGasLimit, 500) + } + componentDidUpdate (prevProps) { const { customGasPrice: prevCustomGasPrice, customGasLimit: prevCustomGasLimit } = prevProps const { customGasPrice, customGasLimit } = this.props @@ -50,12 +49,7 @@ export default class AdvancedTabContent extends Component { } changeGasLimit = (e) => { - if (e.target.value < 21000) { - this.setState({ gasLimit: 21000 }) - this.props.updateCustomGasLimit(21000) - } else { - this.props.updateCustomGasLimit(Number(e.target.value)) - } + this.props.updateCustomGasLimit(Number(e.target.value)) } onChangeGasPrice = (e) => { @@ -67,89 +61,83 @@ export default class AdvancedTabContent extends Component { this.props.updateCustomGasPrice(Number(e.target.value)) } - gasInputError ({ labelKey, insufficientBalance, customPriceIsSafe, isSpeedUp, value }) { + gasPriceError ({ insufficientBalance, customPriceIsSafe, isSpeedUp, gasPrice }) { const { t } = this.context - let errorText - let errorType - let isInError = true - if (insufficientBalance) { - errorText = t('insufficientBalance') - errorType = 'error' - } else if (labelKey === 'gasPrice' && isSpeedUp && value === 0) { - errorText = t('zeroGasPriceOnSpeedUpError') - errorType = 'error' - } else if (labelKey === 'gasPrice' && !customPriceIsSafe) { - errorText = t('gasPriceExtremelyLow') - errorType = 'warning' - } else { - isInError = false + return { + errorText: t('insufficientBalance'), + errorType: 'error', + } + } else if (isSpeedUp && gasPrice === 0) { + return { + errorText: t('zeroGasPriceOnSpeedUpError'), + errorType: 'error', + } + } else if (!customPriceIsSafe) { + return { + errorText: t('gasPriceExtremelyLow'), + errorType: 'warning', + } } - return { - isInError, - errorText, - errorType, - } + return {} } - gasInput ({ labelKey, value, onChange, insufficientBalance, customPriceIsSafe, isSpeedUp }) { - const { - isInError, - errorText, - errorType, - } = this.gasInputError({ labelKey, insufficientBalance, customPriceIsSafe, isSpeedUp, value }) + gasLimitError ({ insufficientBalance, gasLimit }) { + const { t } = this.context - return ( -
- -
-
onChange({ target: { value: value + 1 } })} - > - -
-
onChange({ target: { value: Math.max(value - 1, 0) } })} - > - -
-
- { isInError - ?
- { errorText } -
- : null } -
- ) - } + if (insufficientBalance) { + return { + errorText: t('insufficientBalance'), + errorType: 'error', + } + } else if (gasLimit < 21000) { + return { + errorText: t('gasLimitTooLow'), + errorType: 'error', + } + } - infoButton (onClick) { - return + return {} } - renderGasEditRow (gasInputArgs) { + renderGasInput ({ value, onChange, errorComponent, errorType, infoOnClick, label }) { return (
- { this.context.t(gasInputArgs.labelKey) } - { this.infoButton(() => gasInputArgs.infoOnClick()) } + { label } + +
+
+ +
+
onChange({ target: { value: value + 1 } })} + > + +
+
onChange({ target: { value: Math.max(value - 1, 0) } })} + > + +
+
+ { errorComponent }
- { this.gasInput(gasInputArgs) }
) } @@ -162,25 +150,47 @@ export default class AdvancedTabContent extends Component { showGasPriceInfoModal, showGasLimitInfoModal, } = this.props + const { + gasPrice, + gasLimit, + } = this.state + + const { + errorText: gasPriceErrorText, + errorType: gasPriceErrorType, + } = this.gasPriceError({ insufficientBalance, customPriceIsSafe, isSpeedUp, gasPrice }) + const gasPriceErrorComponent = gasPriceErrorType ? +
+ { gasPriceErrorText } +
: + null + + const { + errorText: gasLimitErrorText, + errorType: gasLimitErrorType, + } = this.gasLimitError({ insufficientBalance, gasLimit }) + const gasLimitErrorComponent = gasLimitErrorType ? +
+ { gasLimitErrorText } +
: + null return (
- { this.renderGasEditRow({ - labelKey: 'gasPrice', + { this.renderGasInput({ + label: this.context.t('gasPrice'), value: this.state.gasPrice, onChange: this.onChangeGasPrice, - insufficientBalance, - customPriceIsSafe, - showGWEI: true, - isSpeedUp, + errorComponent: gasPriceErrorComponent, + errorType: gasPriceErrorType, infoOnClick: showGasPriceInfoModal, }) } - { this.renderGasEditRow({ - labelKey: 'gasLimit', + { this.renderGasInput({ + label: this.context.t('gasLimit'), value: this.state.gasLimit, onChange: this.onChangeGasLimit, - insufficientBalance, - customPriceIsSafe, + errorComponent: gasLimitErrorComponent, + errorType: gasLimitErrorType, infoOnClick: showGasLimitInfoModal, }) }
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 73bc13481..4fa0d4d94 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 @@ -12,7 +12,7 @@ function convertGasPriceForInputs (gasPriceInHexWEI) { } function convertGasLimitForInputs (gasLimitInHexWEI) { - return parseInt(gasLimitInHexWEI, 16) + return parseInt(gasLimitInHexWEI, 16) || 0 } const mapDispatchToProps = dispatch => { @@ -25,9 +25,9 @@ const mapDispatchToProps = dispatch => { const mergeProps = (stateProps, dispatchProps, ownProps) => { const {customGasPrice, customGasLimit, updateCustomGasPrice, updateCustomGasLimit} = ownProps return { + ...ownProps, ...stateProps, ...dispatchProps, - ...ownProps, customGasPrice: convertGasPriceForInputs(customGasPrice), customGasLimit: convertGasLimitForInputs(customGasLimit), updateCustomGasPrice: (price) => updateCustomGasPrice(decGWEIToHexWEI(price)), 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 88d28b9ed..306dd03a0 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 @@ -1,9 +1,11 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import classnames from 'classnames' +import { + decGWEIToHexWEI, +} from '../../../../../helpers/utils/conversions.util' import Loading from '../../../../ui/loading-screen' import GasPriceChart from '../../gas-price-chart' -import debounce from 'lodash.debounce' +import AdvancedGasInputs from '../../advanced-gas-inputs' export default class AdvancedTabContent extends Component { static contextTypes = { @@ -13,8 +15,8 @@ export default class AdvancedTabContent extends Component { static propTypes = { updateCustomGasPrice: PropTypes.func, updateCustomGasLimit: PropTypes.func, - customGasPrice: PropTypes.number, - customGasLimit: PropTypes.number, + customModalGasPriceInHex: PropTypes.string, + customModalGasLimitInHex: PropTypes.string, gasEstimatesLoading: PropTypes.bool, millisecondsRemaining: PropTypes.number, transactionFee: PropTypes.string, @@ -26,95 +28,6 @@ export default class AdvancedTabContent extends Component { isEthereumNetwork: PropTypes.bool, } - constructor (props) { - super(props) - - this.debouncedGasLimitReset = debounce((dVal) => { - if (dVal < 21000) { - props.updateCustomGasLimit(21000) - } - }, 1000, { trailing: true }) - this.onChangeGasLimit = (val) => { - props.updateCustomGasLimit(val) - this.debouncedGasLimitReset(val) - } - } - - gasInputError ({ labelKey, insufficientBalance, customPriceIsSafe, isSpeedUp, value }) { - const { t } = this.context - let errorText - let errorType - let isInError = true - - - if (insufficientBalance) { - errorText = t('insufficientBalance') - errorType = 'error' - } else if (labelKey === 'gasPrice' && isSpeedUp && value === 0) { - errorText = t('zeroGasPriceOnSpeedUpError') - errorType = 'error' - } else if (labelKey === 'gasPrice' && !customPriceIsSafe) { - errorText = t('gasPriceExtremelyLow') - errorType = 'warning' - } else { - isInError = false - } - - return { - isInError, - errorText, - errorType, - } - } - - gasInput ({ labelKey, value, onChange, insufficientBalance, customPriceIsSafe, isSpeedUp }) { - const { - isInError, - errorText, - errorType, - } = this.gasInputError({ labelKey, insufficientBalance, customPriceIsSafe, isSpeedUp, value }) - - return ( -
- onChange(Number(event.target.value))} - /> -
-
onChange(value + 1)} - > - -
-
onChange(Math.max(value - 1, 0))} - > - -
-
- { isInError - ?
- { errorText } -
- : null } -
- ) - } - - infoButton (onClick) { - return - } - renderDataSummary (transactionFee, timeRemaining) { return (
@@ -132,46 +45,9 @@ export default class AdvancedTabContent extends Component { ) } - renderGasEditRow (gasInputArgs) { - return ( -
-
- { this.context.t(gasInputArgs.labelKey) } - { this.infoButton(() => {}) } -
- { this.gasInput(gasInputArgs) } -
- ) - } - - renderGasEditRows ({ - customGasPrice, - updateCustomGasPrice, - customGasLimit, - insufficientBalance, - customPriceIsSafe, - isSpeedUp, - }) { - return ( -
- { this.renderGasEditRow({ - labelKey: 'gasPrice', - value: customGasPrice, - onChange: updateCustomGasPrice, - insufficientBalance, - customPriceIsSafe, - showGWEI: true, - isSpeedUp, - }) } - { this.renderGasEditRow({ - labelKey: 'gasLimit', - value: customGasLimit, - onChange: this.onChangeGasLimit, - insufficientBalance, - customPriceIsSafe, - }) } -
- ) + onGasChartUpdate = (price) => { + const { updateCustomGasPrice } = this.props + updateCustomGasPrice(decGWEIToHexWEI(price)) } render () { @@ -180,8 +56,8 @@ export default class AdvancedTabContent extends Component { updateCustomGasPrice, updateCustomGasLimit, timeRemaining, - customGasPrice, - customGasLimit, + customModalGasPriceInHex, + customModalGasLimitInHex, insufficientBalance, gasChartProps, gasEstimatesLoading, @@ -195,20 +71,22 @@ export default class AdvancedTabContent extends Component {
{ this.renderDataSummary(transactionFee, timeRemaining) }
- { this.renderGasEditRows({ - customGasPrice, - updateCustomGasPrice, - customGasLimit, - updateCustomGasLimit, - insufficientBalance, - customPriceIsSafe, - isSpeedUp, - }) } +
+ +
{ isEthereumNetwork ?
{ t('liveGasPricePredictions') }
{!gasEstimatesLoading - ? + ? : }
diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss index c107b5400..18772bc3c 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss @@ -92,137 +92,13 @@ padding-right: 27px; } - &__gas-edit-rows { - height: 73px; + &__gas-inputs { display: flex; flex-flow: row; justify-content: space-between; margin-left: 20px; margin-right: 10px; - margin-top: 9px; - } - - &__gas-edit-row { - display: flex; - flex-flow: column; - - &__label { - color: #313B5E; - font-size: 14px; - display: flex; - justify-content: space-between; - align-items: center; - - .fa-info-circle { - color: $silver; - margin-left: 10px; - cursor: pointer; - } - - .fa-info-circle:hover { - color: $mid-gray; - } - } - - &__error-text { - font-size: 12px; - color: red; - } - - &__warning-text { - font-size: 12px; - color: orange; - } - - &__input-wrapper { - position: relative; - } - - &__input { - /*rtl:ignore*/ - direction: ltr; - border: 1px solid $dusty-gray; - border-radius: 4px; - color: $mid-gray; - font-size: 16px; - height: 24px; - width: 155px; - padding-left: 8px; - padding-top: 2px; - margin-top: 7px; - } - - &__input--error { - border: 1px solid $red; - } - - &__input--warning { - border: 1px solid $orange; - } - - &__input-arrows { - position: absolute; - top: 7px; - /*rtl:ignore*/ - right: 0px; - width: 17px; - height: 24px; - border: 1px solid #dadada; - border-top-right-radius: 4px; - display: flex; - flex-direction: column; - color: #9b9b9b; - font-size: .8em; - border-bottom-right-radius: 4px; - cursor: pointer; - - &__i-wrap { - width: 100%; - height: 100%; - display: flex; - justify-content: center; - cursor: pointer; - } - - &__i-wrap:hover { - background: #4EADE7; - color: $white; - } - - i:hover { - background: #4EADE7; - } - - i { - font-size: 10px; - } - } - - &__input-arrows--error { - border: 1px solid $red; - } - - &__input-arrows--warning { - border: 1px solid $orange; - } - - input[type="number"]::-webkit-inner-spin-button { - -webkit-appearance: none; - -moz-appearance: none; - display: none; - } - - input[type="number"]:hover::-webkit-inner-spin-button { - -webkit-appearance: none; - -moz-appearance: none; - display: none; - } - - &__gwei-symbol { - position: absolute; - top: 8px; - right: 10px; - color: $dusty-gray; - } + margin-top: 10px; + margin-bottom: 20px } } diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js index 683eeda9b..05075f3ba 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js @@ -12,11 +12,7 @@ const propsMethodSpies = { updateCustomGasLimit: sinon.spy(), } -sinon.spy(AdvancedTabContent.prototype, 'renderGasEditRow') -sinon.spy(AdvancedTabContent.prototype, 'gasInput') -sinon.spy(AdvancedTabContent.prototype, 'renderGasEditRows') sinon.spy(AdvancedTabContent.prototype, 'renderDataSummary') -sinon.spy(AdvancedTabContent.prototype, 'gasInputError') describe('AdvancedTabContent Component', function () { let wrapper @@ -25,23 +21,20 @@ describe('AdvancedTabContent Component', function () { wrapper = shallow(, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) + isEthereumNetwork + />) }) afterEach(() => { propsMethodSpies.updateCustomGasPrice.resetHistory() propsMethodSpies.updateCustomGasLimit.resetHistory() - AdvancedTabContent.prototype.renderGasEditRow.resetHistory() - AdvancedTabContent.prototype.gasInput.resetHistory() - AdvancedTabContent.prototype.renderGasEditRows.resetHistory() AdvancedTabContent.prototype.renderDataSummary.resetHistory() }) @@ -59,7 +52,6 @@ describe('AdvancedTabContent Component', function () { const feeChartDiv = advancedTabChildren.at(1) - assert(feeChartDiv.childAt(0).hasClass('advanced-tab__gas-edit-rows')) assert(feeChartDiv.childAt(1).childAt(0).hasClass('advanced-tab__fee-chart__title')) assert(feeChartDiv.childAt(1).childAt(1).is(GasPriceChart)) assert(feeChartDiv.childAt(1).childAt(2).hasClass('advanced-tab__fee-chart__speed-buttons')) @@ -75,31 +67,15 @@ describe('AdvancedTabContent Component', function () { const feeChartDiv = advancedTabChildren.at(1) - assert(feeChartDiv.childAt(0).hasClass('advanced-tab__gas-edit-rows')) assert(feeChartDiv.childAt(1).childAt(0).hasClass('advanced-tab__fee-chart__title')) assert(feeChartDiv.childAt(1).childAt(1).is(Loading)) assert(feeChartDiv.childAt(1).childAt(2).hasClass('advanced-tab__fee-chart__speed-buttons')) }) it('should call renderDataSummary with the expected params', () => { - assert.equal(AdvancedTabContent.prototype.renderGasEditRows.callCount, 1) const renderDataSummaryArgs = AdvancedTabContent.prototype.renderDataSummary.getCall(0).args assert.deepEqual(renderDataSummaryArgs, ['$0.25', 21500]) }) - - it('should call renderGasEditRows with the expected params', () => { - assert.equal(AdvancedTabContent.prototype.renderGasEditRows.callCount, 1) - const renderGasEditRowArgs = AdvancedTabContent.prototype.renderGasEditRows.getCall(0).args - assert.deepEqual(renderGasEditRowArgs, [{ - customGasPrice: 11, - customGasLimit: 23456, - insufficientBalance: false, - customPriceIsSafe: true, - updateCustomGasPrice: propsMethodSpies.updateCustomGasPrice, - updateCustomGasLimit: propsMethodSpies.updateCustomGasLimit, - isSpeedUp: false, - }]) - }) }) describe('renderDataSummary()', () => { @@ -129,237 +105,4 @@ describe('AdvancedTabContent Component', function () { }) }) - describe('renderGasEditRow()', () => { - let gasEditRow - - beforeEach(() => { - AdvancedTabContent.prototype.gasInput.resetHistory() - gasEditRow = shallow(wrapper.instance().renderGasEditRow({ - labelKey: 'mockLabelKey', - someArg: 'argA', - })) - }) - - it('should render the gas-edit-row root node', () => { - assert(gasEditRow.hasClass('advanced-tab__gas-edit-row')) - }) - - it('should render a label and an input', () => { - const gasEditRowChildren = gasEditRow.children() - assert.equal(gasEditRowChildren.length, 2) - assert(gasEditRowChildren.at(0).hasClass('advanced-tab__gas-edit-row__label')) - assert(gasEditRowChildren.at(1).hasClass('advanced-tab__gas-edit-row__input-wrapper')) - }) - - it('should render the label key and info button', () => { - const gasRowLabelChildren = gasEditRow.children().at(0).children() - assert.equal(gasRowLabelChildren.length, 2) - assert(gasRowLabelChildren.at(0), 'mockLabelKey') - assert(gasRowLabelChildren.at(1).hasClass('fa-info-circle')) - }) - - it('should call this.gasInput with the correct args', () => { - const gasInputSpyArgs = AdvancedTabContent.prototype.gasInput.args - assert.deepEqual(gasInputSpyArgs[0], [ { labelKey: 'mockLabelKey', someArg: 'argA' } ]) - }) - }) - - describe('renderGasEditRows()', () => { - let gasEditRows - let tempOnChangeGasLimit - - beforeEach(() => { - tempOnChangeGasLimit = wrapper.instance().onChangeGasLimit - wrapper.instance().onChangeGasLimit = () => 'mockOnChangeGasLimit' - AdvancedTabContent.prototype.renderGasEditRow.resetHistory() - gasEditRows = shallow(wrapper.instance().renderGasEditRows( - 'mockGasPrice', - () => 'mockUpdateCustomGasPriceReturn', - 'mockGasLimit', - () => 'mockUpdateCustomGasLimitReturn', - false - )) - }) - - afterEach(() => { - wrapper.instance().onChangeGasLimit = tempOnChangeGasLimit - }) - - it('should render the gas-edit-rows root node', () => { - assert(gasEditRows.hasClass('advanced-tab__gas-edit-rows')) - }) - - it('should render two rows', () => { - const gasEditRowsChildren = gasEditRows.children() - assert.equal(gasEditRowsChildren.length, 2) - assert(gasEditRowsChildren.at(0).hasClass('advanced-tab__gas-edit-row')) - assert(gasEditRowsChildren.at(1).hasClass('advanced-tab__gas-edit-row')) - }) - - it('should call this.renderGasEditRow twice, with the expected args', () => { - const renderGasEditRowSpyArgs = AdvancedTabContent.prototype.renderGasEditRow.args - assert.equal(renderGasEditRowSpyArgs.length, 2) - assert.deepEqual(renderGasEditRowSpyArgs[0].map(String), [{ - labelKey: 'gasPrice', - value: 'mockGasLimit', - onChange: () => 'mockOnChangeGasLimit', - insufficientBalance: false, - customPriceIsSafe: true, - showGWEI: true, - }].map(String)) - assert.deepEqual(renderGasEditRowSpyArgs[1].map(String), [{ - labelKey: 'gasPrice', - value: 'mockGasPrice', - onChange: () => 'mockUpdateCustomGasPriceReturn', - insufficientBalance: false, - customPriceIsSafe: true, - showGWEI: true, - }].map(String)) - }) - }) - - describe('infoButton()', () => { - let infoButton - - beforeEach(() => { - AdvancedTabContent.prototype.renderGasEditRow.resetHistory() - infoButton = shallow(wrapper.instance().infoButton(() => 'mockOnClickReturn')) - }) - - it('should render the i element', () => { - assert(infoButton.hasClass('fa-info-circle')) - }) - - it('should pass the onClick argument to the i tag onClick prop', () => { - assert(infoButton.props().onClick(), 'mockOnClickReturn') - }) - }) - - describe('gasInput()', () => { - let gasInput - - beforeEach(() => { - AdvancedTabContent.prototype.renderGasEditRow.resetHistory() - AdvancedTabContent.prototype.gasInputError.resetHistory() - gasInput = shallow(wrapper.instance().gasInput({ - labelKey: 'gasPrice', - value: 321, - onChange: value => value + 7, - insufficientBalance: false, - showGWEI: true, - customPriceIsSafe: true, - isSpeedUp: false, - })) - }) - - it('should render the input-wrapper root node', () => { - assert(gasInput.hasClass('advanced-tab__gas-edit-row__input-wrapper')) - }) - - it('should render two children, including an input', () => { - assert.equal(gasInput.children().length, 2) - assert(gasInput.children().at(0).hasClass('advanced-tab__gas-edit-row__input')) - }) - - it('should call the passed onChange method with the value of the input onChange event', () => { - const inputOnChange = gasInput.find('input').props().onChange - assert.equal(inputOnChange({ target: { value: 8} }), 15) - }) - - it('should have two input arrows', () => { - const upArrow = gasInput.find('.fa-angle-up') - assert.equal(upArrow.length, 1) - const downArrow = gasInput.find('.fa-angle-down') - assert.equal(downArrow.length, 1) - }) - - it('should call onChange with the value incremented decremented when its onchange method is called', () => { - const upArrow = gasInput.find('.advanced-tab__gas-edit-row__input-arrows__i-wrap').at(0) - assert.equal(upArrow.props().onClick(), 329) - const downArrow = gasInput.find('.advanced-tab__gas-edit-row__input-arrows__i-wrap').at(1) - assert.equal(downArrow.props().onClick(), 327) - }) - - it('should call gasInputError with the expected params', () => { - assert.equal(AdvancedTabContent.prototype.gasInputError.callCount, 1) - const gasInputErrorArgs = AdvancedTabContent.prototype.gasInputError.getCall(0).args - assert.deepEqual(gasInputErrorArgs, [{ - labelKey: 'gasPrice', - insufficientBalance: false, - customPriceIsSafe: true, - value: 321, - isSpeedUp: false, - }]) - }) - }) - - describe('gasInputError()', () => { - let gasInputError - - beforeEach(() => { - AdvancedTabContent.prototype.renderGasEditRow.resetHistory() - gasInputError = wrapper.instance().gasInputError({ - labelKey: '', - insufficientBalance: false, - customPriceIsSafe: true, - isSpeedUp: false, - }) - }) - - it('should return an insufficientBalance error', () => { - const gasInputError = wrapper.instance().gasInputError({ - labelKey: 'gasPrice', - insufficientBalance: true, - customPriceIsSafe: true, - isSpeedUp: false, - value: 1, - }) - assert.deepEqual(gasInputError, { - isInError: true, - errorText: 'insufficientBalance', - errorType: 'error', - }) - }) - - it('should return a zero gas on retry error', () => { - const gasInputError = wrapper.instance().gasInputError({ - labelKey: 'gasPrice', - insufficientBalance: false, - customPriceIsSafe: false, - isSpeedUp: true, - value: 0, - }) - assert.deepEqual(gasInputError, { - isInError: true, - errorText: 'zeroGasPriceOnSpeedUpError', - errorType: 'error', - }) - }) - - it('should return a low gas warning', () => { - const gasInputError = wrapper.instance().gasInputError({ - labelKey: 'gasPrice', - insufficientBalance: false, - customPriceIsSafe: false, - isSpeedUp: false, - value: 1, - }) - assert.deepEqual(gasInputError, { - isInError: true, - errorText: 'gasPriceExtremelyLow', - errorType: 'warning', - }) - }) - - it('should return isInError false if there is no error', () => { - gasInputError = wrapper.instance().gasInputError({ - labelKey: 'gasPrice', - insufficientBalance: false, - customPriceIsSafe: true, - value: 1, - }) - assert.equal(gasInputError.isInError, false) - }) - }) - }) diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js b/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js index 931611460..c804abe3a 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js @@ -23,7 +23,7 @@ export default class BasicTabContent extends Component { {!gasPriceButtonGroupProps.loading ? : 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 5e557f660..f405cb7b9 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 @@ -8,6 +8,7 @@ import BasicTabContent from './basic-tab-content' export default class GasModalPageContainer extends Component { static contextTypes = { t: PropTypes.func, + metricsEvent: PropTypes.func, } static propTypes = { @@ -15,11 +16,15 @@ export default class GasModalPageContainer extends Component { hideBasic: PropTypes.bool, updateCustomGasPrice: PropTypes.func, updateCustomGasLimit: PropTypes.func, + currentTimeEstimate: PropTypes.string, customGasPrice: PropTypes.number, customGasLimit: PropTypes.number, + insufficientBalance: PropTypes.bool, fetchBasicGasAndTimeEstimates: PropTypes.func, fetchGasEstimates: PropTypes.func, gasPriceButtonGroupProps: PropTypes.object, + gasChartProps: PropTypes.object, + gasEstimatesLoading: PropTypes.bool, infoRowProps: PropTypes.shape({ originalTotalFiat: PropTypes.string, originalTotalEth: PropTypes.string, @@ -37,6 +42,7 @@ export default class GasModalPageContainer extends Component { ]), customPriceIsSafe: PropTypes.bool, isSpeedUp: PropTypes.bool, + isRetry: PropTypes.bool, disableSave: PropTypes.bool, isEthereumNetwork: PropTypes.bool, } @@ -63,35 +69,39 @@ export default class GasModalPageContainer extends Component { ) } - renderAdvancedTabContent ({ - convertThenUpdateCustomGasPrice, - convertThenUpdateCustomGasLimit, - customGasPrice, - customGasLimit, - newTotalFiat, - gasChartProps, - currentTimeEstimate, - insufficientBalance, - gasEstimatesLoading, - customPriceIsSafe, - isSpeedUp, - transactionFee, - isEthereumNetwork, - }) { + renderAdvancedTabContent () { + const { + updateCustomGasPrice, + updateCustomGasLimit, + customModalGasPriceInHex, + customModalGasLimitInHex, + gasChartProps, + currentTimeEstimate, + insufficientBalance, + gasEstimatesLoading, + customPriceIsSafe, + isSpeedUp, + isRetry, + infoRowProps: { + transactionFee, + }, + isEthereumNetwork, + } = this.props + return ( ) @@ -106,7 +116,7 @@ export default class GasModalPageContainer extends Component { {sendAmount}
- {this.context.t('transactionFee')} + {this.context.t('transactionFee')} {transactionFee}
@@ -121,20 +131,27 @@ export default class GasModalPageContainer extends Component { ) } - renderTabs ({ - newTotalFiat, - newTotalEth, - sendAmount, - transactionFee, - }, - { - gasPriceButtonGroupProps, - hideBasic, - ...advancedTabProps - }) { + renderTabs () { + const { + gasPriceButtonGroupProps, + hideBasic, + infoRowProps: { + newTotalFiat, + newTotalEth, + sendAmount, + transactionFee, + }, + } = this.props + let tabsToRender = [ - { name: 'basic', content: this.renderBasicTabContent(gasPriceButtonGroupProps) }, - { name: 'advanced', content: this.renderAdvancedTabContent({ transactionFee, ...advancedTabProps }) }, + { + name: this.context.t('basic'), + content: this.renderBasicTabContent(gasPriceButtonGroupProps), + }, + { + name: this.context.t('advanced'), + content: this.renderAdvancedTabContent(), + }, ] if (hideBasic) { @@ -143,7 +160,7 @@ export default class GasModalPageContainer extends Component { return ( - {tabsToRender.map(({ name, content }, i) => + {tabsToRender.map(({ name, content }, i) =>
{ content } { this.renderInfoRows(newTotalFiat, newTotalEth, sendAmount, transactionFee) } @@ -157,12 +174,11 @@ export default class GasModalPageContainer extends Component { render () { const { cancelAndClose, - infoRowProps, onSubmit, customModalGasPriceInHex, customModalGasLimitInHex, disableSave, - ...tabProps + isSpeedUp, } = this.props return ( @@ -170,16 +186,25 @@ export default class GasModalPageContainer extends Component { cancelAndClose()} onClose={() => cancelAndClose()} onSubmit={() => { + if (isSpeedUp) { + this.context.metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Activity Log', + name: 'Saved "Speed Up"', + }, + }) + } onSubmit(customModalGasLimitInHex, customModalGasPriceInHex) }} submitText={this.context.t('save')} headerCloseText={this.context.t('close')} - hideCancel={true} + hideCancel />
) diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js index c260d6798..c3d214b63 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js @@ -6,6 +6,7 @@ import { setGasLimit, setGasPrice, createSpeedUpTransaction, + createRetryTransaction, hideSidebar, updateSendAmount, setGasTotal, @@ -33,8 +34,6 @@ import { preferencesSelector, } from '../../../../selectors/selectors.js' import { - formatTimeEstimate, - getFastPriceEstimateInHexWEI, getBasicGasEstimateLoadingStatus, getGasEstimatesLoadingStatus, getCustomGasLimit, @@ -46,6 +45,9 @@ import { getBasicGasEstimateBlockTime, isCustomPriceSafe, } from '../../../../selectors/custom-gas' +import { + getTxParams, +} from '../../../../selectors/transactions' import { getTokenBalance, } from '../../../../pages/send/send.selectors' @@ -56,9 +58,9 @@ import { addHexWEIsToDec, subtractHexWEIsToDec, decEthToConvertedCurrency as ethTotalToConvertedCurrency, - decGWEIToHexWEI, hexWEIToDecGWEI, } from '../../../../helpers/utils/conversions.util' +import { getRenderableTimeEstimate } from '../../../../helpers/utils/gas-time-estimates.util' import { formatETHFee, } from '../../../../helpers/utils/formatters' @@ -67,7 +69,6 @@ import { isBalanceSufficient, } from '../../../../pages/send/send.utils' import { addHexPrefix } from 'ethereumjs-util' -import { getAdjacentGasPrices, extrapolateY } from '../gas-price-chart/gas-price-chart.utils' import { getMaxModeOn } from '../../../../pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors' import { calcMaxAmount } from '../../../../pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils' @@ -83,7 +84,7 @@ const mapStateToProps = (state, ownProps) => { const { gasPrice: currentGasPrice, gas: currentGasLimit, value } = getTxParams(state, selectedTransaction) const customModalGasPriceInHex = getCustomGasPrice(state) || currentGasPrice - const customModalGasLimitInHex = getCustomGasLimit(state) || currentGasLimit + const customModalGasLimitInHex = getCustomGasLimit(state) || currentGasLimit || '0x5208' const customGasTotal = calcGasTotal(customModalGasLimitInHex, customModalGasPriceInHex) const gasButtonInfo = getRenderableBasicEstimateData(state, customModalGasLimitInHex) @@ -154,6 +155,7 @@ const mapStateToProps = (state, ownProps) => { }, transaction: txData || transaction, isSpeedUp: transaction.status === 'submitted', + isRetry: transaction.status === 'failed', txId: transaction.id, insufficientBalance, gasEstimatesLoading, @@ -175,8 +177,7 @@ const mapDispatchToProps = dispatch => { }, hideModal: () => dispatch(hideModal()), updateCustomGasPrice, - convertThenUpdateCustomGasPrice: newPrice => updateCustomGasPrice(decGWEIToHexWEI(newPrice)), - convertThenUpdateCustomGasLimit: newLimit => dispatch(setCustomGasLimit(addHexPrefix(newLimit.toString(16)))), + updateCustomGasLimit: newLimit => dispatch(setCustomGasLimit(addHexPrefix(newLimit))), setGasData: (newLimit, newPrice) => { dispatch(setGasLimit(newLimit)) dispatch(setGasPrice(newPrice)) @@ -189,6 +190,9 @@ const mapDispatchToProps = dispatch => { createSpeedUpTransaction: (txId, gasPrice) => { return dispatch(createSpeedUpTransaction(txId, gasPrice)) }, + createRetryTransaction: (txId, gasPrice) => { + return dispatch(createRetryTransaction(txId, gasPrice)) + }, hideGasButtonGroup: () => dispatch(hideGasButtonGroup()), setCustomTimeEstimate: (timeEstimateInSeconds) => dispatch(setCustomTimeEstimate(timeEstimateInSeconds)), hideSidebar: () => dispatch(hideSidebar()), @@ -208,6 +212,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { isConfirm, txId, isSpeedUp, + isRetry, insufficientBalance, maxModeOn, customGasPrice, @@ -219,11 +224,11 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { transaction, } = stateProps const { - updateCustomGasPrice: dispatchUpdateCustomGasPrice, hideGasButtonGroup: dispatchHideGasButtonGroup, setGasData: dispatchSetGasData, updateConfirmTxGasAndCalculate: dispatchUpdateConfirmTxGasAndCalculate, createSpeedUpTransaction: dispatchCreateSpeedUpTransaction, + createRetryTransaction: dispatchCreateRetryTransaction, hideSidebar: dispatchHideSidebar, cancelAndClose: dispatchCancelAndClose, hideModal: dispatchHideModal, @@ -251,6 +256,10 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { dispatchCreateSpeedUpTransaction(txId, gasPrice) dispatchHideSidebar() dispatchCancelAndClose() + } else if (isRetry) { + dispatchCreateRetryTransaction(txId, gasPrice) + dispatchHideSidebar() + dispatchCancelAndClose() } else { dispatchSetGasData(gasLimit, gasPrice) dispatchHideGasButtonGroup() @@ -267,11 +276,11 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { }, gasPriceButtonGroupProps: { ...gasPriceButtonGroupProps, - handleGasPriceSelection: dispatchUpdateCustomGasPrice, + handleGasPriceSelection: otherDispatchProps.updateCustomGasPrice, }, cancelAndClose: () => { dispatchCancelAndClose() - if (isSpeedUp) { + if (isSpeedUp || isRetry) { dispatchHideSidebar() } }, @@ -293,18 +302,6 @@ function calcCustomGasLimit (customGasLimitInHex) { return parseInt(customGasLimitInHex, 16) } -function getTxParams (state, selectedTransaction = {}) { - const { metamask: { send } } = state - const { txParams } = selectedTransaction - return txParams || { - from: send.from, - gas: send.gasLimit || '0x5208', - gasPrice: send.gasPrice || getFastPriceEstimateInHexWEI(state, true), - to: send.to, - value: getSelectedToken(state) ? '0x0' : send.amount, - } -} - function addHexWEIsToRenderableEth (aHexWEI, bHexWEI) { return pipe( addHexWEIsToDec, @@ -326,31 +323,3 @@ function addHexWEIsToRenderableFiat (aHexWEI, bHexWEI, convertedCurrency, conver partialRight(formatCurrency, [convertedCurrency]), )(aHexWEI, bHexWEI) } - -function getRenderableTimeEstimate (currentGasPrice, gasPrices, estimatedTimes) { - const minGasPrice = gasPrices[0] - const maxGasPrice = gasPrices[gasPrices.length - 1] - let priceForEstimation = currentGasPrice - if (currentGasPrice < minGasPrice) { - priceForEstimation = minGasPrice - } else if (currentGasPrice > maxGasPrice) { - priceForEstimation = maxGasPrice - } - - const { - closestLowerValueIndex, - closestHigherValueIndex, - closestHigherValue, - closestLowerValue, - } = getAdjacentGasPrices({ gasPrices, priceToPosition: priceForEstimation }) - - const newTimeEstimate = extrapolateY({ - higherY: estimatedTimes[closestHigherValueIndex], - lowerY: estimatedTimes[closestLowerValueIndex], - higherX: closestHigherValue, - lowerX: closestLowerValue, - xForExtrapolation: priceForEstimation, - }) - - return formatTimeEstimate(newTimeEstimate, currentGasPrice > maxGasPrice, currentGasPrice < minGasPrice) -} diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js b/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js index 7557eefe5..3e416db49 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js @@ -74,12 +74,12 @@ describe('GasModalPageContainer Component', function () { customGasLimit={54321} gasPriceButtonGroupProps={mockGasPriceButtonGroupProps} infoRowProps={mockInfoRowProps} - currentTimeEstimate={'1 min 31 sec'} - customGasPriceInHex={'mockCustomGasPriceInHex'} - customGasLimitInHex={'mockCustomGasLimitInHex'} + currentTimeEstimate="1 min 31 sec" + customGasPriceInHex="mockCustomGasPriceInHex" + customGasLimitInHex="mockCustomGasLimitInHex" insufficientBalance={false} disableSave={false} - />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) + />) }) afterEach(() => { @@ -158,10 +158,7 @@ describe('GasModalPageContainer Component', function () { }) it('should render a Tabs component with "Basic" and "Advanced" tabs', () => { - const renderTabsResult = wrapper.instance().renderTabs(mockInfoRowProps, { - gasPriceButtonGroupProps: mockGasPriceButtonGroupProps, - otherProps: 'mockAdvancedTabProps', - }) + const renderTabsResult = wrapper.instance().renderTabs() const renderedTabs = shallow(renderTabsResult) assert.equal(renderedTabs.props().className, 'tabs') @@ -175,23 +172,10 @@ describe('GasModalPageContainer Component', function () { assert.equal(tabs.at(1).childAt(0).props().className, 'gas-modal-content') }) - it('should call renderBasicTabContent and renderAdvancedTabContent with the expected props', () => { - assert.equal(GP.renderBasicTabContent.callCount, 0) - assert.equal(GP.renderAdvancedTabContent.callCount, 0) - - wrapper.instance().renderTabs(mockInfoRowProps, { gasPriceButtonGroupProps: mockGasPriceButtonGroupProps, otherProps: 'mockAdvancedTabProps' }) - - assert.equal(GP.renderBasicTabContent.callCount, 1) - assert.equal(GP.renderAdvancedTabContent.callCount, 1) - - assert.deepEqual(GP.renderBasicTabContent.getCall(0).args[0], mockGasPriceButtonGroupProps) - assert.deepEqual(GP.renderAdvancedTabContent.getCall(0).args[0], { transactionFee: 'mockTransactionFee', otherProps: 'mockAdvancedTabProps' }) - }) - it('should call renderInfoRows with the expected props', () => { assert.equal(GP.renderInfoRows.callCount, 0) - wrapper.instance().renderTabs(mockInfoRowProps, { gasPriceButtonGroupProps: mockGasPriceButtonGroupProps, otherProps: 'mockAdvancedTabProps' }) + wrapper.instance().renderTabs() assert.equal(GP.renderInfoRows.callCount, 2) @@ -200,11 +184,25 @@ describe('GasModalPageContainer Component', function () { }) it('should not render the basic tab if hideBasic is true', () => { - const renderTabsResult = wrapper.instance().renderTabs(mockInfoRowProps, { - gasPriceButtonGroupProps: mockGasPriceButtonGroupProps, - otherProps: 'mockAdvancedTabProps', - hideBasic: true, - }) + wrapper = shallow( 'mockupdateCustomGasPrice'} + updateCustomGasLimit={() => 'mockupdateCustomGasLimit'} + customGasPrice={21} + customGasLimit={54321} + gasPriceButtonGroupProps={mockGasPriceButtonGroupProps} + infoRowProps={mockInfoRowProps} + currentTimeEstimate="1 min 31 sec" + customGasPriceInHex="mockCustomGasPriceInHex" + customGasLimitInHex="mockCustomGasLimitInHex" + insufficientBalance={false} + disableSave={false} + hideBasic + />) + const renderTabsResult = wrapper.instance().renderTabs() const renderedTabs = shallow(renderTabsResult) const tabs = renderedTabs.find(Tab) @@ -224,28 +222,6 @@ describe('GasModalPageContainer Component', function () { }) }) - describe('renderAdvancedTabContent', () => { - it('should render with the correct props', () => { - const renderAdvancedTabContentResult = wrapper.instance().renderAdvancedTabContent({ - convertThenUpdateCustomGasPrice: () => 'mockConvertThenUpdateCustomGasPrice', - convertThenUpdateCustomGasLimit: () => 'mockConvertThenUpdateCustomGasLimit', - customGasPrice: 123, - customGasLimit: 456, - newTotalFiat: '$0.30', - currentTimeEstimate: '1 min 31 sec', - gasEstimatesLoading: 'mockGasEstimatesLoading', - }) - const advancedTabContentProps = renderAdvancedTabContentResult.props - assert.equal(advancedTabContentProps.updateCustomGasPrice(), 'mockConvertThenUpdateCustomGasPrice') - assert.equal(advancedTabContentProps.updateCustomGasLimit(), 'mockConvertThenUpdateCustomGasLimit') - assert.equal(advancedTabContentProps.customGasPrice, 123) - assert.equal(advancedTabContentProps.customGasLimit, 456) - assert.equal(advancedTabContentProps.timeRemaining, '1 min 31 sec') - assert.equal(advancedTabContentProps.totalFee, '$0.30') - assert.equal(advancedTabContentProps.gasEstimatesLoading, 'mockGasEstimatesLoading') - }) - }) - describe('renderInfoRows', () => { it('should render the info rows with the passed data', () => { const baseClassName = 'gas-modal-content__info-row' diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js b/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js index d5f3837a9..8e3d14ea4 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js @@ -157,6 +157,7 @@ describe('gas-modal-page-container container', () => { }, insufficientBalance: true, isSpeedUp: false, + isRetry: false, txId: 34, isEthereumNetwork: true, isMainnet: true, @@ -297,19 +298,19 @@ describe('gas-modal-page-container container', () => { }) }) - describe('convertThenUpdateCustomGasPrice()', () => { - it('should dispatch a setCustomGasPrice action with the arg passed to convertThenUpdateCustomGasPrice converted to WEI', () => { - mapDispatchToPropsObject.convertThenUpdateCustomGasPrice('0xffff') + describe('updateCustomGasPrice()', () => { + it('should dispatch a setCustomGasPrice action', () => { + mapDispatchToPropsObject.updateCustomGasPrice('0xffff') assert(dispatchSpy.calledOnce) assert(gasActionSpies.setCustomGasPrice.calledOnce) - assert.equal(gasActionSpies.setCustomGasPrice.getCall(0).args[0], '0x3b9a8e653600') + assert.equal(gasActionSpies.setCustomGasPrice.getCall(0).args[0], '0xffff') }) }) - describe('convertThenUpdateCustomGasLimit()', () => { - it('should dispatch a setCustomGasLimit action with the arg passed to convertThenUpdateCustomGasLimit converted to hex', () => { - mapDispatchToPropsObject.convertThenUpdateCustomGasLimit(16) + describe('updateCustomGasLimit()', () => { + it('should dispatch a setCustomGasLimit action', () => { + mapDispatchToPropsObject.updateCustomGasLimit('0x10') assert(dispatchSpy.calledOnce) assert(gasActionSpies.setCustomGasLimit.calledOnce) assert.equal(gasActionSpies.setCustomGasLimit.getCall(0).args[0], '0x10') 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 14952a49a..0412b3381 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 @@ -2,9 +2,11 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import ButtonGroup from '../../../ui/button-group' import Button from '../../../ui/button' +import { GAS_ESTIMATE_TYPES } from '../../../../helpers/constants/common' + const GAS_OBJECT_PROPTYPES_SHAPE = { - label: PropTypes.string, + gasEstimateType: PropTypes.oneOf(Object.values(GAS_ESTIMATE_TYPES)).isRequired, feeInPrimaryCurrency: PropTypes.string, feeInSecondaryCurrency: PropTypes.string, timeEstimate: PropTypes.string, @@ -27,8 +29,19 @@ export default class GasPriceButtonGroup extends Component { showCheck: PropTypes.bool, } + gasEstimateTypeLabel (gasEstimateType) { + if (gasEstimateType === GAS_ESTIMATE_TYPES.SLOW) { + return this.context.t('slow') + } else if (gasEstimateType === GAS_ESTIMATE_TYPES.AVERAGE) { + return this.context.t('average') + } else if (gasEstimateType === GAS_ESTIMATE_TYPES.FAST) { + return this.context.t('fast') + } + throw new Error(`Unrecognized gas estimate type: ${gasEstimateType}`) + } + renderButtonContent ({ - labelKey, + gasEstimateType, feeInPrimaryCurrency, feeInSecondaryCurrency, timeEstimate, @@ -37,7 +50,7 @@ export default class GasPriceButtonGroup extends Component { showCheck, }) { return (
- { labelKey &&
{ this.context.t(labelKey) }
} + { gasEstimateType &&
{ this.gasEstimateTypeLabel(gasEstimateType) }
} { timeEstimate &&
{ timeEstimate }
} { feeInPrimaryCurrency &&
{ feeInPrimaryCurrency }
} { feeInSecondaryCurrency &&
{ feeInSecondaryCurrency }
} diff --git a/ui/app/components/app/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js b/ui/app/components/app/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js index 37840a8a5..85f53d08e 100644 --- a/ui/app/components/app/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js +++ b/ui/app/components/app/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js @@ -156,15 +156,15 @@ describe('GasPriceButtonGroup Component', function () { }) describe('renderButtonContent', () => { - it('should render a label if passed a labelKey', () => { + it('should render a label if passed a gasEstimateType', () => { const renderButtonContentResult = wrapper.instance().renderButtonContent({ - labelKey: 'mockLabelKey', + gasEstimateType: 'SLOW', }, { className: 'someClass', }) const wrappedRenderButtonContentResult = shallow(renderButtonContentResult) assert.equal(wrappedRenderButtonContentResult.childAt(0).children().length, 1) - assert.equal(wrappedRenderButtonContentResult.find('.someClass__label').text(), 'mockLabelKey') + assert.equal(wrappedRenderButtonContentResult.find('.someClass__label').text(), 'slow') }) it('should render a feeInPrimaryCurrency if passed a feeInPrimaryCurrency', () => { @@ -211,7 +211,7 @@ describe('GasPriceButtonGroup Component', function () { it('should render all elements if all args passed', () => { const renderButtonContentResult = wrapper.instance().renderButtonContent({ - labelKey: 'mockLabel', + gasEstimateType: 'SLOW', feeInPrimaryCurrency: 'mockFeeInPrimaryCurrency', feeInSecondaryCurrency: 'mockFeeInSecondaryCurrency', timeEstimate: 'mockTimeEstimate', 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 b941f1cf9..452544abe 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 @@ -1,11 +1,12 @@ import * as d3 from 'd3' import c3 from 'c3' -import BigNumber from 'bignumber.js' - -const newBigSigDig = n => (new BigNumber(n.toPrecision(15))) -const createOp = (a, b, op) => (newBigSigDig(a))[op](newBigSigDig(b)) -const bigNumMinus = (a = 0, b = 0) => createOp(a, b, 'minus') -const bigNumDiv = (a = 0, b = 1) => createOp(a, b, 'div') +import { + extrapolateY, + getAdjacentGasPrices, + newBigSigDig, + bigNumMinus, + bigNumDiv, +} from '../../../../helpers/utils/gas-time-estimates.util' export function handleMouseMove ({ xMousePos, chartXStart, chartWidth, gasPrices, estimatedTimes, chart }) { const { currentPosValue, newTimeEstimate } = getNewXandTimeEstimate({ @@ -66,25 +67,6 @@ 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) => e > priceToPosition) - return { - closestLowerValueIndex, - closestHigherValueIndex, - closestHigherValue: gasPrices[closestHigherValueIndex], - closestLowerValue: gasPrices[closestLowerValueIndex], - } -} - -export function extrapolateY ({ higherY = 0, lowerY = 0, higherX = 0, lowerX = 0, xForExtrapolation = 0 }) { - const slope = bigNumMinus(higherY, lowerY).div(bigNumMinus(higherX, lowerX)) - const newTimeEstimate = slope.times(bigNumMinus(higherX, xForExtrapolation)).minus(newBigSigDig(higherY)).negated() - - return newTimeEstimate.toNumber() -} - - export function getNewXandTimeEstimate ({ xMousePos, chartXStart, chartWidth, gasPrices, estimatedTimes }) { const chartMouseXPos = bigNumMinus(xMousePos, chartXStart) const posPercentile = bigNumDiv(chartMouseXPos, chartWidth) @@ -232,7 +214,9 @@ export function generateChart (gasPrices, estimatedTimes, gasPricesMax, estimate tick: { values: [Math.floor(gasPrices[0]), Math.ceil(gasPricesMax)], outer: false, - format: function (val) { return val + ' GWEI' }, + format: function (val) { + return val + ' GWEI' + }, }, padding: {left: gasPricesMax / 50, right: gasPricesMax / 50}, label: { diff --git a/ui/app/components/app/gas-customization/gas-slider/gas-slider.component.js b/ui/app/components/app/gas-customization/gas-slider/gas-slider.component.js index 5836e7dfc..8233dedb5 100644 --- a/ui/app/components/app/gas-customization/gas-slider/gas-slider.component.js +++ b/ui/app/components/app/gas-customization/gas-slider/gas-slider.component.js @@ -1,7 +1,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -export default class AdvancedTabContent extends Component { +export default class GasSlider extends Component { static propTypes = { onChange: PropTypes.func, lowLabel: PropTypes.string, diff --git a/ui/app/components/app/home-notification/home-notification.component.js b/ui/app/components/app/home-notification/home-notification.component.js index cc86ef6d8..d3d0a0961 100644 --- a/ui/app/components/app/home-notification/home-notification.component.js +++ b/ui/app/components/app/home-notification/home-notification.component.js @@ -17,12 +17,12 @@ export default class HomeNotification extends PureComponent { } static propTypes = { - acceptText: PropTypes.string.isRequired, + acceptText: PropTypes.node.isRequired, onAccept: PropTypes.func, - ignoreText: PropTypes.string, + ignoreText: PropTypes.node, onIgnore: PropTypes.func, - descriptionText: PropTypes.string.isRequired, - infoText: PropTypes.string, + descriptionText: PropTypes.node.isRequired, + infoText: PropTypes.node, classNames: PropTypes.array, } diff --git a/ui/app/components/app/home-notification/index.scss b/ui/app/components/app/home-notification/index.scss index c855a0814..0e081be91 100644 --- a/ui/app/components/app/home-notification/index.scss +++ b/ui/app/components/app/home-notification/index.scss @@ -8,7 +8,7 @@ background: rgba(36, 41, 46, 0.9); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.12); border-radius: 8px; - height: 116px; + min-height: 116px; padding: 16px; @media screen and (min-width: 576px) { @@ -29,6 +29,11 @@ justify-content: space-between; } + &__icon { + height: 16px; + align-self: center; + } + &__text { font-family: Roboto, 'sans-serif'; font-style: normal; @@ -95,6 +100,7 @@ &__buttons { display: flex; width: 100%; + margin-top: 10px; justify-content: flex-start; flex-direction: row-reverse; } diff --git a/ui/app/components/app/index.scss b/ui/app/components/app/index.scss index fa04a502c..6fa385497 100644 --- a/ui/app/components/app/index.scss +++ b/ui/app/components/app/index.scss @@ -85,3 +85,5 @@ @import 'home-notification/index'; @import 'multiple-notifications/index'; + +@import 'signature-request/index'; diff --git a/ui/app/components/app/input-number.js b/ui/app/components/app/input-number.js index 8a6ec725c..ed678755f 100644 --- a/ui/app/components/app/input-number.js +++ b/ui/app/components/app/input-number.js @@ -28,7 +28,9 @@ function removeLeadingZeroes (str) { InputNumber.prototype.setValue = function (newValue) { newValue = removeLeadingZeroes(newValue) - if (newValue && !isValidInput(newValue)) return + if (newValue && !isValidInput(newValue)) { + return + } const { fixed, min = -1, max = Infinity, onChange } = this.props newValue = fixed ? newValue.toFixed(4) : newValue diff --git a/ui/app/components/app/modal/modal.component.js b/ui/app/components/app/modal/modal.component.js index 44b180ac8..f0fdd3bd5 100644 --- a/ui/app/components/app/modal/modal.component.js +++ b/ui/app/components/app/modal/modal.component.js @@ -1,10 +1,13 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import Button from '../../ui/button' +import classnames from 'classnames' export default class Modal extends PureComponent { static propTypes = { children: PropTypes.node, + contentClass: PropTypes.string, + containerClass: PropTypes.string, // Header text headerText: PropTypes.string, onClose: PropTypes.func, @@ -36,10 +39,12 @@ export default class Modal extends PureComponent { onCancel, cancelType, cancelText, + contentClass, + containerClass, } = this.props return ( -
+
{ headerText && (
@@ -53,7 +58,7 @@ export default class Modal extends PureComponent {
) } -
+
{ children }
diff --git a/ui/app/components/app/modal/tests/modal.component.test.js b/ui/app/components/app/modal/tests/modal.component.test.js index 5922177a6..9e964860c 100644 --- a/ui/app/components/app/modal/tests/modal.component.test.js +++ b/ui/app/components/app/modal/tests/modal.component.test.js @@ -110,7 +110,7 @@ describe('Modal Component', () => { cancelText="Cancel" onSubmit={handleSubmit} submitText="Submit" - submitDisabled={true} + submitDisabled headerText="My Header" onClose={handleCancel} /> diff --git a/ui/app/components/app/modals/confirm-remove-account/confirm-remove-account.component.js b/ui/app/components/app/modals/confirm-remove-account/confirm-remove-account.component.js index f35fb85a0..7fe79be5b 100644 --- a/ui/app/components/app/modals/confirm-remove-account/confirm-remove-account.component.js +++ b/ui/app/components/app/modals/confirm-remove-account/confirm-remove-account.component.js @@ -48,7 +48,7 @@ export default class ConfirmRemoveAccount extends Component { diff --git a/ui/app/components/app/modals/deposit-ether-modal.js b/ui/app/components/app/modals/deposit-ether-modal.js index ff2411209..f71e0619e 100644 --- a/ui/app/components/app/modals/deposit-ether-modal.js +++ b/ui/app/components/app/modals/deposit-ether-modal.js @@ -152,6 +152,10 @@ DepositEtherModal.prototype.render = function () { this.renderRow({ logo: h('img.deposit-ether-modal__logo', { src: './images/deposit-eth.svg', + style: { + height: '75px', + width: '75px', + }, }), title: DIRECT_DEPOSIT_ROW_TITLE, text: DIRECT_DEPOSIT_ROW_TEXT, diff --git a/ui/app/components/app/modals/edit-approval-permission/edit-approval-permission.component.js b/ui/app/components/app/modals/edit-approval-permission/edit-approval-permission.component.js new file mode 100644 index 000000000..53ff473e4 --- /dev/null +++ b/ui/app/components/app/modals/edit-approval-permission/edit-approval-permission.component.js @@ -0,0 +1,170 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Modal from '../../modal' +import Identicon from '../../../ui/identicon' +import TextField from '../../../ui/text-field' +import classnames from 'classnames' + +export default class EditApprovalPermission extends PureComponent { + static propTypes = { + hideModal: PropTypes.func.isRequired, + selectedIdentity: PropTypes.object, + tokenAmount: PropTypes.string, + customTokenAmount: PropTypes.string, + tokenSymbol: PropTypes.string, + tokenBalance: PropTypes.string, + setCustomAmount: PropTypes.func, + origin: PropTypes.string, + } + + static contextTypes = { + t: PropTypes.func, + } + + state = { + customSpendLimit: this.props.customTokenAmount, + selectedOptionIsUnlimited: !this.props.customTokenAmount, + } + + renderModalContent () { + const { t } = this.context + const { + hideModal, + selectedIdentity, + tokenAmount, + tokenSymbol, + tokenBalance, + customTokenAmount, + origin, + } = this.props + const { name, address } = selectedIdentity || {} + const { selectedOptionIsUnlimited } = this.state + + return ( +
+
+
+ { t('editPermission') } +
+
hideModal()} + /> +
+
+
+ +
{ name }
+
{ t('balance') }
+
+
+ {`${tokenBalance} ${tokenSymbol}`} +
+
+
+
+ { t('spendLimitPermission') } +
+
+ { t('allowWithdrawAndSpend', [origin]) } +
+
+
this.setState({ selectedOptionIsUnlimited: true })} + > +
+
+ { selectedOptionIsUnlimited &&
} +
+
+
+ { + tokenAmount < tokenBalance + ? t('proposedApprovalLimit') + : t('unlimited') + } +
+
+ { t('spendLimitRequestedBy', [origin]) } +
+
+ {`${tokenAmount} ${tokenSymbol}`} +
+
+
+
+
this.setState({ selectedOptionIsUnlimited: false })} + > +
+
+ { !selectedOptionIsUnlimited &&
} +
+
+
+ { t('customSpendLimit') } +
+
+ { t('enterMaxSpendLimit') } +
+
+ { + this.setState({ customSpendLimit: event.target.value }) + if (selectedOptionIsUnlimited) { + this.setState({ selectedOptionIsUnlimited: false }) + } + }} + fullWidth + margin="dense" + value={ this.state.customSpendLimit } + /> +
+
+
+
+
+ ) + } + + render () { + const { t } = this.context + const { setCustomAmount, hideModal, customTokenAmount } = this.props + const { selectedOptionIsUnlimited, customSpendLimit } = this.state + return ( + { + setCustomAmount(!selectedOptionIsUnlimited ? customSpendLimit : '') + hideModal() + }} + submitText={t('save')} + submitType="primary" + contentClass="edit-approval-permission-modal-content" + containerClass="edit-approval-permission-modal-container" + submitDisabled={ (customSpendLimit === customTokenAmount) && !selectedOptionIsUnlimited } + > + { this.renderModalContent() } + + ) + } +} diff --git a/ui/app/components/app/modals/edit-approval-permission/edit-approval-permission.container.js b/ui/app/components/app/modals/edit-approval-permission/edit-approval-permission.container.js new file mode 100644 index 000000000..ac25fa149 --- /dev/null +++ b/ui/app/components/app/modals/edit-approval-permission/edit-approval-permission.container.js @@ -0,0 +1,18 @@ +import { connect } from 'react-redux' +import { compose } from 'recompose' +import withModalProps from '../../../../helpers/higher-order-components/with-modal-props' +import EditApprovalPermission from './edit-approval-permission.component' +import { getSelectedIdentity } from '../../../../selectors/selectors' + +const mapStateToProps = (state) => { + const modalStateProps = state.appState.modal.modalState.props || {} + return { + selectedIdentity: getSelectedIdentity(state), + ...modalStateProps, + } +} + +export default compose( + withModalProps, + connect(mapStateToProps) +)(EditApprovalPermission) diff --git a/ui/app/components/app/modals/edit-approval-permission/index.js b/ui/app/components/app/modals/edit-approval-permission/index.js new file mode 100644 index 000000000..3f50d3e99 --- /dev/null +++ b/ui/app/components/app/modals/edit-approval-permission/index.js @@ -0,0 +1 @@ +export { default } from './edit-approval-permission.container' diff --git a/ui/app/components/app/modals/edit-approval-permission/index.scss b/ui/app/components/app/modals/edit-approval-permission/index.scss new file mode 100644 index 000000000..f400da4c1 --- /dev/null +++ b/ui/app/components/app/modals/edit-approval-permission/index.scss @@ -0,0 +1,167 @@ +.edit-approval-permission { + width: 100%; + + &__header, + &__account-info { + display: flex; + justify-content: center; + align-items: center; + position: relative; + border-bottom: 1px solid #d2d8dd; + } + + &__header { + padding: 24px; + + &__close { + position: absolute; + right: 24px; + background-image: url("/images/close-gray.svg"); + width: .75rem; + height: .75rem; + cursor: pointer; + } + } + + &__title { + font-weight: bold; + font-size: 18px; + line-height: 25px; + } + + &__account-info { + justify-content: space-between; + padding: 8px 24px; + + &__account, + &__balance { + font-weight: normal; + font-size: 14px; + color: #24292E; + } + + &__account { + display: flex; + align-items: center; + } + + &__name { + margin-left: 8px; + margin-right: 8px; + } + + &__balance { + color: #6A737D; + } + } + + &__edit-section { + padding: 24px; + + &__title { + font-weight: bold; + font-size: 14px; + line-height: 20px; + color: #24292E; + } + + &__description { + font-weight: normal; + font-size: 12px; + line-height: 17px; + color: #6A737D; + margin-top: 8px; + } + + &__option { + display: flex; + align-items: flex-start; + margin-top: 20px; + } + + &__radio-button { + width: 18px; + } + + &__option-text { + display: flex; + flex-direction: column; + } + + &__option-label, + &__option-label--selected { + font-weight: normal; + font-size: 14px; + line-height: 20px; + color: #474B4D; + } + + &__option-label--selected { + color: #037DD6; + } + + &__option-description { + font-weight: normal; + font-size: 12px; + line-height: 17px; + color: #6A737D; + margin-top: 8px; + margin-bottom: 6px; + } + + &__option-value { + font-weight: normal; + font-size: 18px; + line-height: 25px; + color: #24292E; + } + + &__radio-button { + position: relative; + width: 18px; + height: 18px; + display: flex; + justify-content: center; + align-items: center; + margin-right: 4px; + } + + &__radio-button-outline, + &__radio-button-outline--selected { + width: 18px; + height: 18px; + background: #DADCDD; + border-radius: 9px; + position: absolute; + } + + &__radio-button-outline--selected { + background: #037DD6; + } + + &__radio-button-fill { + width: 14px; + height: 14px; + background: white; + border-radius: 7px; + position: absolute; + } + + &__radio-button-dot { + width: 8px; + height: 8px; + background: #037DD6; + border-radius: 4px; + position: absolute; + } + } +} + +.edit-approval-permission-modal-content { + padding: 0px; +} + +.edit-approval-permission-modal-container { + max-height: 550px; + width: 100%; +} 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 43d7bcd74..1e1aaeb74 100644 --- a/ui/app/components/app/modals/export-private-key-modal.js +++ b/ui/app/components/app/modals/export-private-key-modal.js @@ -120,6 +120,7 @@ ExportPrivateKeyModal.prototype.renderButtons = function (privateKey, address, h type: 'secondary', large: true, className: 'export-private-key__button', + disabled: !this.state.password, onClick: () => this.exportAccountAndGetPrivateKey(this.state.password, address), }, this.context.t('confirm')) ) diff --git a/ui/app/components/app/modals/index.scss b/ui/app/components/app/modals/index.scss index d93a41140..da7a27b84 100644 --- a/ui/app/components/app/modals/index.scss +++ b/ui/app/components/app/modals/index.scss @@ -9,3 +9,5 @@ @import 'metametrics-opt-in-modal/index'; @import './add-to-addressbook-modal/index'; + +@import './edit-approval-permission/index'; diff --git a/ui/app/components/app/modals/loading-network-error/loading-network-error.component.js b/ui/app/components/app/modals/loading-network-error/loading-network-error.component.js index 44f71e4b2..6cac4942f 100644 --- a/ui/app/components/app/modals/loading-network-error/loading-network-error.component.js +++ b/ui/app/components/app/modals/loading-network-error/loading-network-error.component.js @@ -12,7 +12,7 @@ const LoadingNetworkError = (props, context) => { submitText={t('tryAgain')} > ) diff --git a/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js b/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js index 1bf7c21b5..6f3225382 100644 --- a/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js +++ b/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js @@ -102,7 +102,7 @@ export default class MetaMetricsOptInModal extends Component { hideModal() }) }} - cancelText={'No Thanks'} + cancelText="No Thanks" hideCancel={false} onSubmit={() => { setParticipateInMetaMetrics(true) @@ -118,8 +118,8 @@ export default class MetaMetricsOptInModal extends Component { hideModal() }) }} - submitText={'I agree'} - submitButtonType={'confirm'} + submitText="I agree" + submitButtonType="confirm" disabled={false} />
diff --git a/ui/app/components/app/modals/modal.js b/ui/app/components/app/modals/modal.js index e65677b62..4a7e7d678 100644 --- a/ui/app/components/app/modals/modal.js +++ b/ui/app/components/app/modals/modal.js @@ -31,6 +31,7 @@ import ClearPlugins from './clear-plugins' import ConfirmCustomizeGasModal from '../gas-customization/gas-modal-page-container' import ConfirmDeleteNetwork from './confirm-delete-network' import AddToAddressBookModal from './add-to-addressbook-modal' +import EditApprovalPermission from './edit-approval-permission' const modalContainerBaseStyle = { transform: 'translate3d(-50%, 0, 0px)', @@ -346,6 +347,31 @@ const MODALS = { }, }, + EDIT_APPROVAL_PERMISSION: { + contents: h(EditApprovalPermission), + mobileModalStyle: { + width: '95vw', + height: '100vh', + top: '50px', + transform: 'none', + left: '0', + right: '0', + margin: '0 auto', + }, + laptopModalStyle: { + width: 'auto', + height: '0px', + top: '80px', + left: '0px', + transform: 'none', + margin: '0 auto', + position: 'relative', + }, + contentStyle: { + borderRadius: '8px', + }, + }, + TRANSACTION_CONFIRMED: { disableBackdropClick: true, contents: h(TransactionConfirmed), 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 afeaef0da..7accf0e58 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 @@ -142,14 +142,14 @@ export default class QrScanner extends Component { renderVideo () { return ( -
+
) } @@ -172,12 +172,12 @@ export default class QrScanner extends Component {
- +
{ title }
-
+
{msg}
{ this.renderVideo() }
-
+
{this.state.msg}
diff --git a/ui/app/components/app/multiple-notifications/multiple-notifications.component.js b/ui/app/components/app/multiple-notifications/multiple-notifications.component.js index 040890e18..f9f6fe887 100644 --- a/ui/app/components/app/multiple-notifications/multiple-notifications.component.js +++ b/ui/app/components/app/multiple-notifications/multiple-notifications.component.js @@ -3,8 +3,13 @@ import classnames from 'classnames' import PropTypes from 'prop-types' export default class MultipleNotifications extends PureComponent { + static defaultProps = { + children: [], + classNames: [], + } + static propTypes = { - notifications: PropTypes.array, + children: PropTypes.array, classNames: PropTypes.array, } @@ -14,11 +19,10 @@ export default class MultipleNotifications extends PureComponent { render () { const { showAll } = this.state - const { notifications, classNames = [] } = this.props - - const notificationsToBeRendered = notifications.filter(notificationConfig => notificationConfig.shouldBeRendered) + const { children, classNames } = this.props - if (notificationsToBeRendered.length === 0) { + const childrenToRender = children.filter(child => child) + if (childrenToRender.length === 0) { return null } @@ -29,12 +33,12 @@ export default class MultipleNotifications extends PureComponent { 'home-notification-wrapper--show-first': !showAll, })} > - { notificationsToBeRendered.map(notificationConfig => notificationConfig.component) } + { childrenToRender }
this.setState({ showAll: !showAll })} > - {notificationsToBeRendered.length > 1 ? 1 ? : null}
diff --git a/ui/app/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js b/ui/app/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js index 4fb8f2d0e..b29df91c0 100644 --- a/ui/app/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js +++ b/ui/app/components/app/permission-page-container/permission-page-container-content/permission-page-container-content.component.js @@ -110,7 +110,7 @@ export default class PermissionPageContainerContent extends PureComponent { {this.renderPermissionApprovalVisual()}

{siteMetadata.name}

-

{'Would like to:'}

+

Would like to:

{this.renderRequestedPermissions()}
+ return case 'customize-gas': - return
+ return
default: return null } diff --git a/ui/app/components/app/sidebars/tests/sidebars-component.test.js b/ui/app/components/app/sidebars/tests/sidebars-component.test.js index e2daea9b6..5f6657dde 100644 --- a/ui/app/components/app/sidebars/tests/sidebars-component.test.js +++ b/ui/app/components/app/sidebars/tests/sidebars-component.test.js @@ -19,8 +19,8 @@ describe('Sidebar Component', function () { wrapper = shallow() }) diff --git a/ui/app/components/app/signature-request-original/index.js b/ui/app/components/app/signature-request-original/index.js new file mode 100644 index 000000000..00a906785 --- /dev/null +++ b/ui/app/components/app/signature-request-original/index.js @@ -0,0 +1 @@ +export { default } from './signature-request-original.container' diff --git a/ui/app/components/app/signature-request-original/signature-request-original.component.js b/ui/app/components/app/signature-request-original/signature-request-original.component.js new file mode 100644 index 000000000..2485ae506 --- /dev/null +++ b/ui/app/components/app/signature-request-original/signature-request-original.component.js @@ -0,0 +1,318 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import ethUtil from 'ethereumjs-util' +import classnames from 'classnames' +import { ObjectInspector } from 'react-inspector' + +import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../../app/scripts/lib/enums' +import { getEnvironmentType } from '../../../../../app/scripts/lib/util' +import Identicon from '../../ui/identicon' +import AccountListItem from '../../../pages/send/account-list-item/account-list-item.component' +import { conversionUtil } from '../../../helpers/utils/conversion-util' +import Button from '../../ui/button' +import { DEFAULT_ROUTE } from '../../../helpers/constants/routes' + +export default class SignatureRequestOriginal extends Component { + static contextTypes = { + t: PropTypes.func.isRequired, + metricsEvent: PropTypes.func.isRequired, + } + + static propTypes = { + balance: PropTypes.string, + cancel: PropTypes.func.isRequired, + clearConfirmTransaction: PropTypes.func.isRequired, + conversionRate: PropTypes.number, + history: PropTypes.object.isRequired, + requesterAddress: PropTypes.string, + selectedAccount: PropTypes.string, + sign: PropTypes.func.isRequired, + txData: PropTypes.object.isRequired, + } + + state = { + selectedAccount: this.props.selectedAccount, + } + + componentDidMount = () => { + if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) { + window.addEventListener('beforeunload', this._beforeUnload) + } + } + + componentWillUnmount = () => { + this._removeBeforeUnload() + } + + _beforeUnload = (event) => { + const { clearConfirmTransaction, cancel } = this.props + const { metricsEvent } = this.context + metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Sign Request', + name: 'Cancel Sig Request Via Notification Close', + }, + }) + clearConfirmTransaction() + cancel(event) + } + + _removeBeforeUnload = () => { + if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) { + window.removeEventListener('beforeunload', this._beforeUnload) + } + } + + renderHeader = () => { + return ( +
+
+ +
+ { this.context.t('sigRequest') } +
+ +
+
+
+
+ ) + } + + renderAccount = () => { + const { selectedAccount } = this.state + + return ( +
+
+ { `${this.context.t('account')}:` } +
+ +
+ +
+
+ ) + } + + renderBalance = () => { + const { balance, conversionRate } = this.props + + const balanceInEther = conversionUtil(balance, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + fromDenomination: 'WEI', + numberOfDecimals: 6, + conversionRate, + }) + + return ( +
+
+ { `${this.context.t('balance')}:` } +
+
+ { `${balanceInEther} ETH` } +
+
+ ) + } + + renderRequestIcon = () => { + const { requesterAddress } = this.props + + return ( +
+ +
+ ) + } + + renderAccountInfo = () => { + return ( +
+ { this.renderAccount() } + { this.renderRequestIcon() } + { this.renderBalance() } +
+ ) + } + + renderRequestInfo = () => { + return ( +
+
+ { this.context.t('yourSigRequested') } +
+
+ ) + } + + msgHexToText = (hex) => { + try { + const stripped = ethUtil.stripHexPrefix(hex) + const buff = Buffer.from(stripped, 'hex') + return buff.length === 32 ? hex : buff.toString('utf8') + } catch (e) { + return hex + } + } + + renderTypedData = (data) => { + const { domain, message } = JSON.parse(data) + return ( +
+ { + domain + ?
+

+ Domain +

+ +
+ : '' + } + { + message + ?
+

+ Message +

+ +
+ : '' + } +
+ ) + } + + renderBody = () => { + let rows + let notice = `${this.context.t('youSign')}:` + + const { txData } = this.props + const { type, msgParams: { data } } = txData + + if (type === 'personal_sign') { + rows = [{ name: this.context.t('message'), value: this.msgHexToText(data) }] + } else if (type === 'eth_signTypedData') { + rows = data + } else if (type === 'eth_sign') { + rows = [{ name: this.context.t('message'), value: data }] + notice = this.context.t('signNotice') + } + + return ( +
+ { this.renderAccountInfo() } + { this.renderRequestInfo() } +
+ { notice } + { + type === 'eth_sign' + ? { + global.platform.openWindow({ + url: 'https://metamask.zendesk.com/hc/en-us/articles/360015488751', + }) + }} + > + { this.context.t('learnMore') } + + : null + } +
+
+ { + rows.map(({ name, value }, index) => { + if (typeof value === 'boolean') { + value = value.toString() + } + return ( +
+
+ { `${name}:` } +
+
+ { value } +
+
+ ) + }) + } +
+
+ ) + } + + renderFooter = () => { + const { cancel, sign } = this.props + + return ( +
+ , + +
+ ) + } + + render = () => { + return ( +
+ { this.renderHeader() } + { this.renderBody() } + { this.renderFooter() } +
+ ) + } +} diff --git a/ui/app/components/app/signature-request-original/signature-request-original.container.js b/ui/app/components/app/signature-request-original/signature-request-original.container.js new file mode 100644 index 000000000..be891b4db --- /dev/null +++ b/ui/app/components/app/signature-request-original/signature-request-original.container.js @@ -0,0 +1,72 @@ +import { connect } from 'react-redux' +import { compose } from 'recompose' +import { withRouter } from 'react-router-dom' + +import actions from '../../../store/actions' +import { + getSelectedAccount, + getCurrentAccountWithSendEtherInfo, + getSelectedAddress, + conversionRateSelector, +} from '../../../selectors/selectors.js' +import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck' +import SignatureRequestOriginal from './signature-request-original.component' + +function mapStateToProps (state) { + return { + balance: getSelectedAccount(state).balance, + selectedAccount: getCurrentAccountWithSendEtherInfo(state), + selectedAddress: getSelectedAddress(state), + requester: null, + requesterAddress: null, + conversionRate: conversionRateSelector(state), + } +} + +function mapDispatchToProps (dispatch) { + return { + goHome: () => dispatch(actions.goHome()), + clearConfirmTransaction: () => dispatch(clearConfirmTransaction()), + } +} + +function mergeProps (stateProps, dispatchProps, ownProps) { + const { + signPersonalMessage, + signTypedMessage, + cancelPersonalMessage, + cancelTypedMessage, + signMessage, + cancelMessage, + txData, + } = ownProps + + const { type } = txData + + let cancel + let sign + if (type === 'personal_sign') { + cancel = cancelPersonalMessage + sign = signPersonalMessage + } else if (type === 'eth_signTypedData') { + cancel = cancelTypedMessage + sign = signTypedMessage + } else if (type === 'eth_sign') { + cancel = cancelMessage + sign = signMessage + } + + return { + ...ownProps, + ...stateProps, + ...dispatchProps, + txData, + cancel, + sign, + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps, mergeProps) +)(SignatureRequestOriginal) diff --git a/ui/app/components/app/signature-request.js b/ui/app/components/app/signature-request.js deleted file mode 100644 index f89cdd2d6..000000000 --- a/ui/app/components/app/signature-request.js +++ /dev/null @@ -1,336 +0,0 @@ -const Component = require('react').Component -const PropTypes = require('prop-types') -const h = require('react-hyperscript') -const inherits = require('util').inherits -import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../app/scripts/lib/enums' -import { getEnvironmentType } from '../../../../app/scripts/lib/util' -import Identicon from '../ui/identicon' -const connect = require('react-redux').connect -const ethUtil = require('ethereumjs-util') -const classnames = require('classnames') -const { compose } = require('recompose') -const { withRouter } = require('react-router-dom') -const { ObjectInspector } = require('react-inspector') - -import AccountDropdownMini from '../ui/account-dropdown-mini' - -const actions = require('../../store/actions') -const { conversionUtil } = require('../../helpers/utils/conversion-util') - -const { - getSelectedAccount, - getSelectedAddress, - conversionRateSelector, -} = require('../../selectors/selectors.js') - -import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck' -import Button from '../ui/button' - -const { DEFAULT_ROUTE } = require('../../helpers/constants/routes') - -// TODO:lps this doesn't work, get the account from the sig request instead -function mapStateToProps (state) { - return { - balance: getSelectedAccount(state).balance, - selectedAddress: getSelectedAddress(state), - requester: null, - requesterAddress: null, - conversionRate: conversionRateSelector(state), - } -} - -function mapDispatchToProps (dispatch) { - return { - goHome: () => dispatch(actions.goHome()), - clearConfirmTransaction: () => dispatch(clearConfirmTransaction()), - } -} - -function mergeProps (stateProps, dispatchProps, ownProps) { - const { - signPersonalMessage, - signTypedMessage, - cancelPersonalMessage, - cancelTypedMessage, - signMessage, - cancelMessage, - txData, - } = ownProps - - const { type } = txData - - let cancel - let sign - if (type === 'personal_sign') { - cancel = cancelPersonalMessage - sign = signPersonalMessage - } else if (type === 'eth_signTypedData') { - cancel = cancelTypedMessage - sign = signTypedMessage - } else if (type === 'eth_sign') { - cancel = cancelMessage - sign = signMessage - } - - return { - ...stateProps, - ...dispatchProps, - ...ownProps, - txData, - cancel, - sign, - } -} - -SignatureRequest.contextTypes = { - t: PropTypes.func, - metricsEvent: PropTypes.func, -} - -module.exports = compose( - withRouter, - connect(mapStateToProps, mapDispatchToProps, mergeProps) -)(SignatureRequest) - -inherits(SignatureRequest, Component) - -function SignatureRequest (_) { - Component.call(this) -} - -SignatureRequest.prototype.componentDidMount = function () { - const { clearConfirmTransaction, cancel } = this.props - const { metricsEvent } = this.context - if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) { - window.onbeforeunload = event => { - metricsEvent({ - eventOpts: { - category: 'Transactions', - action: 'Sign Request', - name: 'Cancel Sig Request Via Notification Close', - }, - }) - clearConfirmTransaction() - cancel(event) - } - } -} - -SignatureRequest.prototype.renderHeader = function () { - return h('div.request-signature__header', [ - - h('div.request-signature__header-background'), - - h('div.request-signature__header__text', this.context.t('sigRequest')), - - h('div.request-signature__header__tip-container', [ - h('div.request-signature__header__tip'), - ]), - - ]) -} - -SignatureRequest.prototype.renderAccountDropdown = function () { - - return h('div.request-signature__account', [ - - h('div.request-signature__account-text', [this.context.t('account') + ':']), - - h(AccountDropdownMini, { - disabled: true, - }), - - ]) -} - -SignatureRequest.prototype.renderBalance = function () { - const { balance, conversionRate } = this.props - - const balanceInEther = conversionUtil(balance, { - fromNumericBase: 'hex', - toNumericBase: 'dec', - fromDenomination: 'WEI', - numberOfDecimals: 6, - conversionRate, - }) - - return h('div.request-signature__balance', [ - - h('div.request-signature__balance-text', `${this.context.t('balance')}:`), - - h('div.request-signature__balance-value', `${balanceInEther} ETH`), - - ]) -} - -SignatureRequest.prototype.renderAccountInfo = function () { - return h('div.request-signature__account-info', [ - - this.renderAccountDropdown(), - - this.renderRequestIcon(), - - this.renderBalance(), - - ]) -} - -SignatureRequest.prototype.renderRequestIcon = function () { - const { requesterAddress } = this.props - - return h('div.request-signature__request-icon', [ - h(Identicon, { - diameter: 40, - address: requesterAddress, - }), - ]) -} - -SignatureRequest.prototype.renderRequestInfo = function () { - return h('div.request-signature__request-info', [ - - h('div.request-signature__headline', [ - this.context.t('yourSigRequested'), - ]), - - ]) -} - -SignatureRequest.prototype.msgHexToText = function (hex) { - try { - const stripped = ethUtil.stripHexPrefix(hex) - const buff = Buffer.from(stripped, 'hex') - return buff.length === 32 ? hex : buff.toString('utf8') - } catch (e) { - return hex - } -} - -// eslint-disable-next-line react/display-name -SignatureRequest.prototype.renderTypedData = function (data) { - const { domain, message } = JSON.parse(data) - return [ - h('div.request-signature__typed-container', [ - domain ? h('div', [ - h('h1', 'Domain'), - h(ObjectInspector, { data: domain, expandLevel: 1, name: 'domain' }), - ]) : '', - message ? h('div', [ - h('h1', 'Message'), - h(ObjectInspector, { data: message, expandLevel: 1, name: 'message' }), - ]) : '', - ]), - ] -} - -SignatureRequest.prototype.renderBody = function () { - let rows - let notice = this.context.t('youSign') + ':' - - const { txData } = this.props - const { type, msgParams: { data, version } } = txData - - if (type === 'personal_sign') { - rows = [{ name: this.context.t('message'), value: this.msgHexToText(data) }] - } else if (type === 'eth_signTypedData') { - rows = data - } else if (type === 'eth_sign') { - rows = [{ name: this.context.t('message'), value: data }] - notice = [this.context.t('signNotice'), - h('span.request-signature__help-link', { - onClick: () => { - global.platform.openWindow({ - url: 'https://metamask.zendesk.com/hc/en-us/articles/360015488751', - }) - }, - }, this.context.t('learnMore'))] - } - - return h('div.request-signature__body', {}, [ - - this.renderAccountInfo(), - - this.renderRequestInfo(), - - h('div.request-signature__notice', { - className: classnames({ - 'request-signature__notice': type === 'personal_sign' || type === 'eth_signTypedData', - 'request-signature__warning': type === 'eth_sign', - }), - }, [notice]), - - h('div.request-signature__rows', - type === 'eth_signTypedData' && (version === 'V3' || version === 'V4') ? - this.renderTypedData(data) : - rows.map(({ name, value }) => { - if (typeof value === 'boolean') { - value = value.toString() - } - return h('div.request-signature__row', [ - h('div.request-signature__row-title', [`${name}:`]), - h('div.request-signature__row-value', value), - ]) - }), - ), - ]) -} - -SignatureRequest.prototype.renderFooter = function () { - const { cancel, sign } = this.props - - return h('div.request-signature__footer', [ - h(Button, { - type: 'default', - large: true, - className: 'request-signature__footer__cancel-button', - onClick: event => { - cancel(event).then(() => { - this.context.metricsEvent({ - eventOpts: { - category: 'Transactions', - action: 'Sign Request', - name: 'Cancel', - }, - }) - this.props.clearConfirmTransaction() - this.props.history.push(DEFAULT_ROUTE) - }) - }, - }, this.context.t('cancel')), - h(Button, { - type: 'secondary', - large: true, - className: 'request-signature__footer__sign-button', - onClick: event => { - sign(event).then(() => { - this.context.metricsEvent({ - eventOpts: { - category: 'Transactions', - action: 'Sign Request', - name: 'Confirm', - }, - }) - this.props.clearConfirmTransaction() - this.props.history.push(DEFAULT_ROUTE) - }) - }, - }, this.context.t('sign')), - ]) -} - -SignatureRequest.prototype.render = function () { - return ( - - h('div.request-signature__container', [ - - this.renderHeader(), - - this.renderBody(), - - this.renderFooter(), - - ]) - - ) - -} diff --git a/ui/app/components/app/signature-request/index.js b/ui/app/components/app/signature-request/index.js new file mode 100644 index 000000000..b1c8a1960 --- /dev/null +++ b/ui/app/components/app/signature-request/index.js @@ -0,0 +1 @@ +export { default } from './signature-request.container' diff --git a/ui/app/components/app/signature-request/index.scss b/ui/app/components/app/signature-request/index.scss new file mode 100644 index 000000000..69115681f --- /dev/null +++ b/ui/app/components/app/signature-request/index.scss @@ -0,0 +1,96 @@ +@import 'signature-request-footer/index'; +@import 'signature-request-header/index'; +@import 'signature-request-message/index'; + +.signature-request { + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-width: 0; + + @media screen and (min-width: 576px) { + flex: initial; + } +} + +.signature-request-header { + flex: 1; + + .network-display__container { + padding: 0; + justify-content: flex-end; + } + + .network-display__name { + font-size: 12px; + white-space: nowrap; + font-weight: 500; + } +} + +.signature-request-content { + flex: 1 40%; + margin-top: 1rem; + display: flex; + align-items: center; + flex-direction: column; + margin-bottom: 25px; + min-height: min-content; + + &__title { + font-family: Roboto; + font-style: normal; + font-weight: 500; + font-size: 18px; + } + + &__identicon-container { + padding: 1rem; + flex: 1; + position: relative; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + } + + &__identicon-border { + height: 75px; + width: 75px; + border-radius: 50%; + border: 1px solid white; + position: absolute; + box-shadow: 0 2px 2px 0.5px rgba(0, 0, 0, 0.19); + } + + &__identicon-initial { + position: absolute; + font-family: Roboto; + font-style: normal; + font-weight: 500; + font-size: 60px; + color: white; + z-index: 1; + text-shadow: 0px 4px 6px rgba(0, 0, 0, 0.422); + } + + &__info { + font-size: 12px; + } + + &__info--bolded { + font-size: 16px; + font-weight: 500; + } + + p { + color: #999999; + font-size: 0.8rem; + } + + .identicon {} +} + +.signature-request-footer { + flex: 1 1 auto; +} \ No newline at end of file diff --git a/ui/app/components/app/signature-request/signature-request-footer/index.js b/ui/app/components/app/signature-request/signature-request-footer/index.js new file mode 100644 index 000000000..11d0b3944 --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request-footer/index.js @@ -0,0 +1 @@ +export { default } from './signature-request-footer.component' diff --git a/ui/app/components/app/signature-request/signature-request-footer/index.scss b/ui/app/components/app/signature-request/signature-request-footer/index.scss new file mode 100644 index 000000000..d8c6b36d6 --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request-footer/index.scss @@ -0,0 +1,18 @@ +.signature-request-footer { + display: flex; + border-top: 1px solid #d2d8dd; + + button { + text-transform: uppercase; + flex: 1; + margin: 1rem 0.5rem; + border-radius: 3px; + } + + button:first-child() { + margin-left: 1rem; + } + button:last-child() { + margin-right: 1rem; + } +} \ No newline at end of file diff --git a/ui/app/components/app/signature-request/signature-request-footer/signature-request-footer.component.js b/ui/app/components/app/signature-request/signature-request-footer/signature-request-footer.component.js new file mode 100644 index 000000000..591b9a03a --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request-footer/signature-request-footer.component.js @@ -0,0 +1,24 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Button from '../../../ui/button' + +export default class SignatureRequestFooter extends PureComponent { + static propTypes = { + cancelAction: PropTypes.func.isRequired, + signAction: PropTypes.func.isRequired, + } + + static contextTypes = { + t: PropTypes.func, + } + + render () { + const { cancelAction, signAction } = this.props + return ( +
+ + +
+ ) + } +} diff --git a/ui/app/components/app/signature-request/signature-request-header/index.js b/ui/app/components/app/signature-request/signature-request-header/index.js new file mode 100644 index 000000000..fa596383a --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request-header/index.js @@ -0,0 +1 @@ +export { default } from './signature-request-header.component' diff --git a/ui/app/components/app/signature-request/signature-request-header/index.scss b/ui/app/components/app/signature-request/signature-request-header/index.scss new file mode 100644 index 000000000..7a33f85f2 --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request-header/index.scss @@ -0,0 +1,25 @@ +.signature-request-header { + display: flex; + padding: 1rem; + border-bottom: 1px solid $geyser; + justify-content: space-between; + font-size: .75rem; + + &--account, &--network { + flex: 1; + } + + &--account { + display: flex; + align-items: center; + + .account-list-item__account-name { + font-size: 12px; + font-weight: 500; + } + + .account-list-item__top-row { + margin: 0px; + } + } +} \ No newline at end of file diff --git a/ui/app/components/app/signature-request/signature-request-header/signature-request-header.component.js b/ui/app/components/app/signature-request/signature-request-header/signature-request-header.component.js new file mode 100644 index 000000000..3ac0c9afb --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request-header/signature-request-header.component.js @@ -0,0 +1,29 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import AccountListItem from '../../../../pages/send/account-list-item/account-list-item.component' +import NetworkDisplay from '../../network-display' + +export default class SignatureRequestHeader extends PureComponent { + static propTypes = { + selectedAccount: PropTypes.object.isRequired, + } + + render () { + const { selectedAccount } = this.props + + return ( +
+
+ {selectedAccount && } + {name} +
+
+ +
+
+ ) + } +} diff --git a/ui/app/components/app/signature-request/signature-request-message/index.js b/ui/app/components/app/signature-request/signature-request-message/index.js new file mode 100644 index 000000000..e62265a5f --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request-message/index.js @@ -0,0 +1 @@ +export { default } from './signature-request-message.component' diff --git a/ui/app/components/app/signature-request/signature-request-message/index.scss b/ui/app/components/app/signature-request/signature-request-message/index.scss new file mode 100644 index 000000000..aec597f89 --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request-message/index.scss @@ -0,0 +1,67 @@ +.signature-request-message { + flex: 1 60%; + display: flex; + flex-direction: column; + + &__title { + font-weight: 500; + font-size: 14px; + color: #636778; + margin-left: 12px; + } + + h2 { + flex: 1 1 0; + text-align: left; + font-size: 0.8rem; + border-bottom: 1px solid #d2d8dd; + padding: 0.5rem; + margin: 0; + color: #ccc; + } + + &--root { + flex: 1 100%; + background-color: #f8f9fb; + padding-bottom: 0.5rem; + overflow: auto; + padding-left: 12px; + padding-right: 12px; + width: 360px; + font-family: monospace; + + @media screen and (min-width: 576px) { + width: auto; + } + } + + &__type-title { + font-family: monospace; + font-style: normal; + font-weight: normal; + font-size: 14px; + margin-left: 12px; + margin-top: 6px; + margin-bottom: 10px; + } + + &--node, &--node-leaf { + padding-left: 0.8rem; + + &-label { + color: #5B5D67; + } + + &-value { + color: black; + margin-left: 0.5rem; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + } + + &--node-leaf { + display: flex; + } +} \ No newline at end of file diff --git a/ui/app/components/app/signature-request/signature-request-message/signature-request-message.component.js b/ui/app/components/app/signature-request/signature-request-message/signature-request-message.component.js new file mode 100644 index 000000000..16b6c3bea --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request-message/signature-request-message.component.js @@ -0,0 +1,50 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' + +export default class SignatureRequestMessage extends PureComponent { + static propTypes = { + data: PropTypes.object.isRequired, + } + + static contextTypes = { + t: PropTypes.func, + } + + renderNode (data) { + return ( +
+ {Object.entries(data).map(([ label, value ], i) => ( +
+ {label}: + { + typeof value === 'object' && value !== null ? + this.renderNode(value) + : {value} + } +
+ ))} +
+ ) + } + + + render () { + const { data } = this.props + + return ( +
+
{this.context.t('signatureRequest1')}
+
+
{this.context.t('signatureRequest1')}
+ {this.renderNode(data)} +
+
+ ) + } +} diff --git a/ui/app/components/app/signature-request/signature-request.component.js b/ui/app/components/app/signature-request/signature-request.component.js new file mode 100644 index 000000000..b108c02c4 --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request.component.js @@ -0,0 +1,82 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Header from './signature-request-header' +import Footer from './signature-request-footer' +import Message from './signature-request-message' +import { ENVIRONMENT_TYPE_NOTIFICATION } from './signature-request.constants' +import { getEnvironmentType } from '../../../../../app/scripts/lib/util' +import Identicon from '../../ui/identicon' + +export default class SignatureRequest extends PureComponent { + static propTypes = { + txData: PropTypes.object.isRequired, + selectedAccount: PropTypes.shape({ + address: PropTypes.string, + balance: PropTypes.string, + name: PropTypes.string, + }).isRequired, + + clearConfirmTransaction: PropTypes.func.isRequired, + cancel: PropTypes.func.isRequired, + sign: PropTypes.func.isRequired, + } + + static contextTypes = { + t: PropTypes.func, + metricsEvent: PropTypes.func, + } + + componentDidMount () { + const { clearConfirmTransaction, cancel } = this.props + const { metricsEvent } = this.context + if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) { + window.addEventListener('beforeunload', (event) => { + metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Sign Request', + name: 'Cancel Sig Request Via Notification Close', + }, + }) + clearConfirmTransaction() + cancel(event) + }) + } + } + + formatWallet (wallet) { + return `${wallet.slice(0, 8)}...${wallet.slice(wallet.length - 8, wallet.length)}` + } + + render () { + const { + selectedAccount, + txData: { msgParams: { data, origin, from: senderWallet }}, + cancel, + sign, + } = this.props + const { message, domain = {} } = JSON.parse(data) + + return ( +
+
+
+
{this.context.t('sigRequest')}
+
+
{ domain.name && domain.name[0] }
+
+ +
+
{domain.name}
+
{origin}
+
{this.formatWallet(senderWallet)}
+
+ +
+
+ ) + } +} diff --git a/ui/app/components/app/signature-request/signature-request.constants.js b/ui/app/components/app/signature-request/signature-request.constants.js new file mode 100644 index 000000000..9cf241928 --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request.constants.js @@ -0,0 +1,3 @@ +import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../../app/scripts/lib/enums' + +export { ENVIRONMENT_TYPE_NOTIFICATION } diff --git a/ui/app/components/app/signature-request/signature-request.container.js b/ui/app/components/app/signature-request/signature-request.container.js new file mode 100644 index 000000000..0b09c1a64 --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request.container.js @@ -0,0 +1,72 @@ +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { compose } from 'recompose' +import SignatureRequest from './signature-request.component' +import { goHome } from '../../../store/actions' +import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck' +import { + getSelectedAccount, + getCurrentAccountWithSendEtherInfo, + getSelectedAddress, + accountsWithSendEtherInfoSelector, + conversionRateSelector, +} from '../../../selectors/selectors.js' + +function mapStateToProps (state) { + return { + balance: getSelectedAccount(state).balance, + selectedAccount: getCurrentAccountWithSendEtherInfo(state), + selectedAddress: getSelectedAddress(state), + accounts: accountsWithSendEtherInfoSelector(state), + conversionRate: conversionRateSelector(state), + } +} + +function mapDispatchToProps (dispatch) { + return { + goHome: () => dispatch(goHome()), + clearConfirmTransaction: () => dispatch(clearConfirmTransaction()), + } +} + +function mergeProps (stateProps, dispatchProps, ownProps) { + const { + signPersonalMessage, + signTypedMessage, + cancelPersonalMessage, + cancelTypedMessage, + signMessage, + cancelMessage, + txData, + } = ownProps + + const { type } = txData + + let cancel + let sign + + if (type === 'personal_sign') { + cancel = cancelPersonalMessage + sign = signPersonalMessage + } else if (type === 'eth_signTypedData') { + cancel = cancelTypedMessage + sign = signTypedMessage + } else if (type === 'eth_sign') { + cancel = cancelMessage + sign = signMessage + } + + return { + ...stateProps, + ...dispatchProps, + ...ownProps, + txData, + cancel, + sign, + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps, mergeProps) +)(SignatureRequest) diff --git a/ui/app/components/app/signature-request/tests/signature-request.test.js b/ui/app/components/app/signature-request/tests/signature-request.test.js new file mode 100644 index 000000000..68b114dd8 --- /dev/null +++ b/ui/app/components/app/signature-request/tests/signature-request.test.js @@ -0,0 +1,25 @@ +import React from 'react' +import assert from 'assert' +import shallow from '../../../../../lib/shallow-with-context' +import SignatureRequest from '../signature-request.component' + + +describe('Signature Request Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow() + }) + + describe('render', () => { + it('should render a div with one child', () => { + assert(wrapper.is('div')) + assert.equal(wrapper.length, 1) + assert(wrapper.hasClass('signature-request')) + }) + }) +}) diff --git a/ui/app/components/app/token-list.js b/ui/app/components/app/token-list.js index 977503d81..c9f239800 100644 --- a/ui/app/components/app/token-list.js +++ b/ui/app/components/app/token-list.js @@ -121,7 +121,9 @@ TokenList.prototype.createFreshTokenTracker = function () { this.tracker.removeListener('error', this.showError) } - if (!global.ethereumProvider) return + if (!global.ethereumProvider) { + return + } const { userAddress } = this.props this.tracker = new TokenTracker({ @@ -170,7 +172,9 @@ TokenList.prototype.componentDidUpdate = function (prevProps) { const oldTokensLength = tokens ? tokens.length : 0 const tokensLengthUnchanged = oldTokensLength === newTokens.length - if (tokensLengthUnchanged && shouldUpdateTokens) return + if (tokensLengthUnchanged && shouldUpdateTokens) { + return + } this.setState({ isLoading: true }) this.createFreshTokenTracker() @@ -184,7 +188,9 @@ TokenList.prototype.updateBalances = function (tokens) { } TokenList.prototype.componentWillUnmount = function () { - if (!this.tracker) return + if (!this.tracker) { + return + } this.tracker.stop() this.tracker.removeListener('update', this.balanceUpdater) this.tracker.removeListener('error', this.showError) diff --git a/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.component.test.js b/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.component.test.js index b070b76ea..6625a46a9 100644 --- a/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.component.test.js +++ b/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.component.test.js @@ -90,7 +90,7 @@ describe('TransactionActivityLog Component', () => { onCancel={() => {}} onRetry={() => {}} primaryTransactionStatus="pending" - isEarliestNonce={true} + isEarliestNonce />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } } ) diff --git a/ui/app/components/app/transaction-list-item-details/index.js b/ui/app/components/app/transaction-list-item-details/index.js index 0e878d032..83bd53e7d 100644 --- a/ui/app/components/app/transaction-list-item-details/index.js +++ b/ui/app/components/app/transaction-list-item-details/index.js @@ -1 +1 @@ -export { default } from './transaction-list-item-details.component' +export { default } from './transaction-list-item-details.container' diff --git a/ui/app/components/app/transaction-list-item-details/tests/transaction-list-item-details.component.test.js b/ui/app/components/app/transaction-list-item-details/tests/transaction-list-item-details.component.test.js index 583980d26..69360fec6 100644 --- a/ui/app/components/app/transaction-list-item-details/tests/transaction-list-item-details.component.test.js +++ b/ui/app/components/app/transaction-list-item-details/tests/transaction-list-item-details.component.test.js @@ -70,7 +70,7 @@ describe('TransactionListItemDetails Component', () => { const wrapper = shallow( , { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } } ) 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 237a2c286..f27c74970 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 @@ -17,15 +17,24 @@ export default class TransactionListItemDetails extends PureComponent { metricsEvent: PropTypes.func, } + static defaultProps = { + recipientEns: null, + } + static propTypes = { onCancel: PropTypes.func, onRetry: PropTypes.func, showCancel: PropTypes.bool, + showSpeedUp: PropTypes.bool, showRetry: PropTypes.bool, isEarliestNonce: PropTypes.bool, cancelDisabled: PropTypes.bool, transactionGroup: PropTypes.object, + recipientEns: PropTypes.string, + recipientAddress: PropTypes.string.isRequired, rpcPrefs: PropTypes.object, + senderAddress: PropTypes.string.isRequired, + tryReverseResolveAddress: PropTypes.func.isRequired, } state = { @@ -81,6 +90,12 @@ export default class TransactionListItemDetails extends PureComponent { }) } + async componentDidMount () { + const { recipientAddress, tryReverseResolveAddress } = this.props + + tryReverseResolveAddress(recipientAddress) + } + renderCancel () { const { t } = this.context const { @@ -123,14 +138,18 @@ export default class TransactionListItemDetails extends PureComponent { const { justCopied } = this.state const { transactionGroup, + showSpeedUp, showRetry, onCancel, onRetry, + recipientEns, + recipientAddress, rpcPrefs: { blockExplorerUrl } = {}, + senderAddress, isEarliestNonce, } = this.props const { primaryTransaction: transaction } = transactionGroup - const { hash, txParams: { to, from } = {} } = transaction + const { hash } = transaction return (
@@ -138,7 +157,7 @@ export default class TransactionListItemDetails extends PureComponent {
{ t('details') }
{ - showRetry && ( + showSpeedUp && ( + { + showRetry && + + + }
@@ -179,8 +209,9 @@ export default class TransactionListItemDetails extends PureComponent { { this.context.metricsEvent({ eventOpts: { diff --git a/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.container.js b/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.container.js new file mode 100644 index 000000000..50f93f497 --- /dev/null +++ b/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.container.js @@ -0,0 +1,28 @@ +import { connect } from 'react-redux' +import TransactionListItemDetails from './transaction-list-item-details.component' +import { checksumAddress } from '../../../helpers/utils/util' +import { tryReverseResolveAddress } from '../../../store/actions' + +const mapStateToProps = (state, ownProps) => { + const { metamask } = state + const { + ensResolutionsByAddress, + } = metamask + const { recipientAddress } = ownProps + const address = checksumAddress(recipientAddress) + const recipientEns = ensResolutionsByAddress[address] || '' + + return { + recipientEns, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + tryReverseResolveAddress: (address) => { + return dispatch(tryReverseResolveAddress(address)) + }, + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(TransactionListItemDetails) diff --git a/ui/app/components/app/transaction-list-item/index.scss b/ui/app/components/app/transaction-list-item/index.scss index 4dba4b2c3..9804ecd97 100644 --- a/ui/app/components/app/transaction-list-item/index.scss +++ b/ui/app/components/app/transaction-list-item/index.scss @@ -13,18 +13,20 @@ width: 100%; padding: 16px 20px; display: grid; - grid-template-columns: 45px 1fr 1fr 1fr; + grid-template-columns: 45px 1fr 1fr 1fr 1fr; grid-template-areas: - "identicon action status primary-amount" - "identicon nonce status secondary-amount"; + "identicon action status estimated-time primary-amount" + "identicon nonce status estimated-time secondary-amount"; + grid-template-rows: 24px; @media screen and (max-width: $break-small) { padding: .5rem 1rem; grid-template-columns: 45px 5fr 3fr; grid-template-areas: - "nonce nonce nonce" - "identicon action primary-amount" - "identicon status secondary-amount"; + "nonce nonce nonce nonce" + "identicon action estimated-time primary-amount" + "identicon status estimated-time secondary-amount"; + grid-template-rows: auto 24px; } &:hover { @@ -63,6 +65,18 @@ } } + &__estimated-time { + grid-area: estimated-time; + grid-row: 1 / span 2; + align-self: center; + + @media screen and (max-width: $break-small) { + grid-row: 3; + grid-column: 4; + font-size: small; + } + } + &__nonce { font-size: .75rem; color: #5e6064; @@ -125,6 +139,7 @@ &__expander { max-height: 0px; width: 100%; + overflow: hidden; &--show { max-height: 1000px; 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 47c94acf2..9ab0105f9 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 @@ -7,10 +7,13 @@ import TransactionAction from '../transaction-action' import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' import TokenCurrencyDisplay from '../../ui/token-currency-display' import TransactionListItemDetails from '../transaction-list-item-details' +import TransactionTimeRemaining from '../transaction-time-remaining' import { CONFIRM_TRANSACTION_ROUTE } from '../../../helpers/constants/routes' import { UNAPPROVED_STATUS, TOKEN_METHOD_TRANSFER } from '../../../helpers/constants/transactions' import { PRIMARY, SECONDARY } from '../../../helpers/constants/common' import { getStatusKey } from '../../../helpers/utils/transactions.util' +import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../../app/scripts/lib/enums' +import { getEnvironmentType } from '../../../../../app/scripts/lib/util' export default class TransactionListItem extends PureComponent { static propTypes = { @@ -24,7 +27,7 @@ export default class TransactionListItem extends PureComponent { showCancelModal: PropTypes.func, showCancel: PropTypes.bool, hasEnoughCancelGas: PropTypes.bool, - showRetry: PropTypes.bool, + showSpeedUp: PropTypes.bool, isEarliestNonce: PropTypes.bool, showFiat: PropTypes.bool, token: PropTypes.object, @@ -38,6 +41,8 @@ export default class TransactionListItem extends PureComponent { data: PropTypes.string, getContractMethodData: PropTypes.func, isDeposit: PropTypes.bool, + transactionTimeFeatureActive: PropTypes.bool, + firstPendingTransactionId: PropTypes.number, } static defaultProps = { @@ -52,6 +57,13 @@ export default class TransactionListItem extends PureComponent { showTransactionDetails: false, } + componentDidMount () { + if (this.props.data) { + this.props.getContractMethodData(this.props.data) + } + + } + handleClick = () => { const { transaction, @@ -113,6 +125,14 @@ export default class TransactionListItem extends PureComponent { const retryId = id || initialTransactionId + this.context.metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Activity Log', + name: 'Clicked "Speed Up"', + }, + }) + return fetchBasicGasAndTimeEstimates() .then(basicEstimates => fetchGasEstimates(basicEstimates.blockTime)) .then(retryTransaction(retryId, gasPrice)) @@ -154,12 +174,6 @@ export default class TransactionListItem extends PureComponent { ) } - componentDidMount () { - if (this.props.data) { - this.props.getContractMethodData(this.props.data) - } - } - render () { const { assetImages, @@ -169,18 +183,26 @@ export default class TransactionListItem extends PureComponent { primaryTransaction, showCancel, hasEnoughCancelGas, - showRetry, + showSpeedUp, tokenData, transactionGroup, rpcPrefs, isEarliestNonce, + firstPendingTransactionId, + transactionTimeFeatureActive, } = this.props const { txParams = {} } = transaction const { showTransactionDetails } = this.state + const fromAddress = txParams.from const toAddress = tokenData ? tokenData.params && tokenData.params[0] && tokenData.params[0].value || txParams.to : txParams.to + const isFullScreen = getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_FULLSCREEN + const showEstimatedTime = transactionTimeFeatureActive && + (transaction.id === firstPendingTransactionId) && + isFullScreen + return (
+ { showEstimatedTime + ? + : null + } { this.renderPrimaryCurrency() } { this.renderSecondaryCurrency() }
@@ -225,12 +254,15 @@ export default class TransactionListItem extends PureComponent {
) 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 27b9e2608..26ccec1f7 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 @@ -8,12 +8,19 @@ import { getTokenData } from '../../../helpers/utils/transactions.util' import { getHexGasTotal, increaseLastGasPrice } from '../../../helpers/utils/confirm-tx.util' import { formatDate } from '../../../helpers/utils/util' import { - fetchBasicGasAndTimeEstimates, fetchGasEstimates, + fetchBasicGasAndTimeEstimates, setCustomGasPriceForRetry, setCustomGasLimit, } from '../../../ducks/gas/gas.duck' -import { getIsMainnet, preferencesSelector, getSelectedAddress, conversionRateSelector, getKnownMethodData } from '../../../selectors/selectors' +import { + getIsMainnet, + preferencesSelector, + getSelectedAddress, + conversionRateSelector, + getKnownMethodData, + getFeatureFlags, +} from '../../../selectors/selectors' import { isBalanceSufficient } from '../../../pages/send/send.utils' const mapStateToProps = (state, ownProps) => { @@ -21,10 +28,10 @@ const mapStateToProps = (state, ownProps) => { const { showFiatInTestnets } = preferencesSelector(state) const isMainnet = getIsMainnet(state) const { transactionGroup: { primaryTransaction } = {} } = ownProps - const { txParams: { gas: gasLimit, gasPrice, data, to } = {} } = primaryTransaction + const { txParams: { gas: gasLimit, gasPrice, data } = {}, transactionCategory } = primaryTransaction const selectedAddress = getSelectedAddress(state) const selectedAccountBalance = accounts[selectedAddress].balance - const isDeposit = selectedAddress === to + const isDeposit = transactionCategory === 'incoming' const selectRpcInfo = frequentRpcListDetail.find(rpcInfo => rpcInfo.rpcUrl === provider.rpcTarget) const { rpcPrefs } = selectRpcInfo || {} @@ -38,6 +45,8 @@ const mapStateToProps = (state, ownProps) => { conversionRate: conversionRateSelector(state), }) + const transactionTimeFeatureActive = getFeatureFlags(state).transactionTime + return { methodData: getKnownMethodData(state, data) || {}, showFiat: (isMainnet || !!showFiatInTestnets), @@ -45,6 +54,7 @@ const mapStateToProps = (state, ownProps) => { hasEnoughCancelGas, rpcPrefs, isDeposit, + transactionTimeFeatureActive, } } diff --git a/ui/app/components/app/transaction-list/transaction-list.component.js b/ui/app/components/app/transaction-list/transaction-list.component.js index 00ae1c657..0e0540257 100644 --- a/ui/app/components/app/transaction-list/transaction-list.component.js +++ b/ui/app/components/app/transaction-list/transaction-list.component.js @@ -22,22 +22,53 @@ export default class TransactionList extends PureComponent { selectedToken: PropTypes.object, updateNetworkNonce: PropTypes.func, assetImages: PropTypes.object, + fetchBasicGasAndTimeEstimates: PropTypes.func, + fetchGasEstimates: PropTypes.func, + transactionTimeFeatureActive: PropTypes.bool, + firstPendingTransactionId: PropTypes.number, } componentDidMount () { - this.props.updateNetworkNonce() + const { + pendingTransactions, + updateNetworkNonce, + fetchBasicGasAndTimeEstimates, + fetchGasEstimates, + transactionTimeFeatureActive, + } = this.props + + updateNetworkNonce() + + if (transactionTimeFeatureActive && pendingTransactions.length) { + fetchBasicGasAndTimeEstimates() + .then(({ blockTime }) => fetchGasEstimates(blockTime)) + } } componentDidUpdate (prevProps) { const { pendingTransactions: prevPendingTransactions = [] } = prevProps - const { pendingTransactions = [], updateNetworkNonce } = this.props + const { + pendingTransactions = [], + updateNetworkNonce, + fetchBasicGasAndTimeEstimates, + fetchGasEstimates, + transactionTimeFeatureActive, + } = this.props if (pendingTransactions.length > prevPendingTransactions.length) { updateNetworkNonce() } + + const transactionTimeFeatureWasActivated = !prevProps.transactionTimeFeatureActive && transactionTimeFeatureActive + const pendingTransactionAdded = pendingTransactions.length > 0 && prevPendingTransactions.length === 0 + + if (transactionTimeFeatureActive && pendingTransactions.length > 0 && (transactionTimeFeatureWasActivated || pendingTransactionAdded)) { + fetchBasicGasAndTimeEstimates() + .then(({ blockTime }) => fetchGasEstimates(blockTime)) + } } - shouldShowRetry = (transactionGroup, isEarliestNonce) => { + shouldShowSpeedUp = (transactionGroup, isEarliestNonce) => { const { transactions = [], hasRetried } = transactionGroup const [earliestTransaction = {}] = transactions const { submittedTime } = earliestTransaction @@ -87,7 +118,7 @@ export default class TransactionList extends PureComponent { } renderTransaction (transactionGroup, index, isPendingTx = false) { - const { selectedToken, assetImages } = this.props + const { selectedToken, assetImages, firstPendingTransactionId } = this.props const { transactions = [] } = transactionGroup return transactions[0].key === TRANSACTION_TYPE_SHAPESHIFT @@ -100,11 +131,12 @@ export default class TransactionList extends PureComponent { ) } diff --git a/ui/app/components/app/transaction-list/transaction-list.container.js b/ui/app/components/app/transaction-list/transaction-list.container.js index 67a24588b..4da044b2a 100644 --- a/ui/app/components/app/transaction-list/transaction-list.container.js +++ b/ui/app/components/app/transaction-list/transaction-list.container.js @@ -6,23 +6,30 @@ import { nonceSortedCompletedTransactionsSelector, nonceSortedPendingTransactionsSelector, } from '../../../selectors/transactions' -import { getSelectedAddress, getAssetImages } from '../../../selectors/selectors' +import { getSelectedAddress, getAssetImages, getFeatureFlags } from '../../../selectors/selectors' import { selectedTokenSelector } from '../../../selectors/tokens' import { updateNetworkNonce } from '../../../store/actions' +import { fetchBasicGasAndTimeEstimates, fetchGasEstimates } from '../../../ducks/gas/gas.duck' -const mapStateToProps = state => { +const mapStateToProps = (state) => { + const pendingTransactions = nonceSortedPendingTransactionsSelector(state) + const firstPendingTransactionId = pendingTransactions[0] && pendingTransactions[0].primaryTransaction.id return { completedTransactions: nonceSortedCompletedTransactionsSelector(state), - pendingTransactions: nonceSortedPendingTransactionsSelector(state), + pendingTransactions, + firstPendingTransactionId, selectedToken: selectedTokenSelector(state), selectedAddress: getSelectedAddress(state), assetImages: getAssetImages(state), + transactionTimeFeatureActive: getFeatureFlags(state).transactionTime, } } const mapDispatchToProps = dispatch => { return { updateNetworkNonce: address => dispatch(updateNetworkNonce(address)), + fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)), + fetchBasicGasAndTimeEstimates: () => dispatch(fetchBasicGasAndTimeEstimates()), } } diff --git a/ui/app/components/app/transaction-time-remaining/index.js b/ui/app/components/app/transaction-time-remaining/index.js new file mode 100644 index 000000000..87c6821d8 --- /dev/null +++ b/ui/app/components/app/transaction-time-remaining/index.js @@ -0,0 +1 @@ +export { default } from './transaction-time-remaining.container' diff --git a/ui/app/components/app/transaction-time-remaining/transaction-time-remaining.component.js b/ui/app/components/app/transaction-time-remaining/transaction-time-remaining.component.js new file mode 100644 index 000000000..c9598d69b --- /dev/null +++ b/ui/app/components/app/transaction-time-remaining/transaction-time-remaining.component.js @@ -0,0 +1,52 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import { calcTransactionTimeRemaining } from './transaction-time-remaining.util' + +export default class TransactionTimeRemaining extends PureComponent { + static propTypes = { + className: PropTypes.string, + initialTimeEstimate: PropTypes.number, + submittedTime: PropTypes.number, + } + + constructor (props) { + super(props) + const { initialTimeEstimate, submittedTime } = props + this.state = { + timeRemaining: calcTransactionTimeRemaining(initialTimeEstimate, submittedTime), + } + this.interval = setInterval( + () => this.setState({ timeRemaining: calcTransactionTimeRemaining(initialTimeEstimate, submittedTime) }), + 1000 + ) + } + + componentDidUpdate (prevProps) { + const { initialTimeEstimate, submittedTime } = this.props + if (initialTimeEstimate !== prevProps.initialTimeEstimate) { + clearInterval(this.interval) + const calcedTimeRemaining = calcTransactionTimeRemaining(initialTimeEstimate, submittedTime) + this.setState({ timeRemaining: calcedTimeRemaining }) + this.interval = setInterval( + () => this.setState({ timeRemaining: calcTransactionTimeRemaining(initialTimeEstimate, submittedTime) }), + 1000 + ) + } + } + + componentWillUnmount () { + clearInterval(this.interval) + } + + render () { + const { className } = this.props + const { timeRemaining } = this.state + + return ( +
+ { timeRemaining } +
+ + ) + } +} diff --git a/ui/app/components/app/transaction-time-remaining/transaction-time-remaining.container.js b/ui/app/components/app/transaction-time-remaining/transaction-time-remaining.container.js new file mode 100644 index 000000000..65eeaa0c3 --- /dev/null +++ b/ui/app/components/app/transaction-time-remaining/transaction-time-remaining.container.js @@ -0,0 +1,41 @@ +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { compose } from 'recompose' +import TransactionTimeRemaining from './transaction-time-remaining.component' +import { + getTxParams, +} from '../../../selectors/transactions' +import { + getEstimatedGasPrices, + getEstimatedGasTimes, +} from '../../../selectors/custom-gas' +import { getRawTimeEstimateData } from '../../../helpers/utils/gas-time-estimates.util' +import { hexWEIToDecGWEI } from '../../../helpers/utils/conversions.util' + +const mapStateToProps = (state, ownProps) => { + const { transaction } = ownProps + const { gasPrice: currentGasPrice } = getTxParams(state, transaction) + const customGasPrice = calcCustomGasPrice(currentGasPrice) + const gasPrices = getEstimatedGasPrices(state) + const estimatedTimes = getEstimatedGasTimes(state) + + const { + newTimeEstimate: initialTimeEstimate, + } = getRawTimeEstimateData(customGasPrice, gasPrices, estimatedTimes) + + const submittedTime = transaction.submittedTime + + return { + initialTimeEstimate, + submittedTime, + } +} + +export default compose( + withRouter, + connect(mapStateToProps) +)(TransactionTimeRemaining) + +function calcCustomGasPrice (customGasPriceInHex) { + return Number(hexWEIToDecGWEI(customGasPriceInHex)) +} diff --git a/ui/app/components/app/transaction-time-remaining/transaction-time-remaining.util.js b/ui/app/components/app/transaction-time-remaining/transaction-time-remaining.util.js new file mode 100644 index 000000000..0ba81edfc --- /dev/null +++ b/ui/app/components/app/transaction-time-remaining/transaction-time-remaining.util.js @@ -0,0 +1,13 @@ +import { formatTimeEstimate } from '../../../helpers/utils/gas-time-estimates.util' + +export function calcTransactionTimeRemaining (initialTimeEstimate, submittedTime) { + const currentTime = (new Date()).getTime() + const timeElapsedSinceSubmission = (currentTime - submittedTime) / 1000 + const timeRemainingOnEstimate = initialTimeEstimate - timeElapsedSinceSubmission + + const renderingTimeRemainingEstimate = timeRemainingOnEstimate < 30 + ? '< 30 s' + : formatTimeEstimate(timeRemainingOnEstimate) + + return renderingTimeRemainingEstimate +} diff --git a/ui/app/components/app/transaction-view-balance/transaction-view-balance.component.js b/ui/app/components/app/transaction-view-balance/transaction-view-balance.component.js index feb701dbe..73905574e 100644 --- a/ui/app/components/app/transaction-view-balance/transaction-view-balance.component.js +++ b/ui/app/components/app/transaction-view-balance/transaction-view-balance.component.js @@ -53,7 +53,7 @@ export default class TransactionViewBalance extends PureComponent { value={balance} type={PRIMARY} ethNumberOfDecimals={4} - hideTitle={true} + hideTitle /> { balanceIsCached ? * : null @@ -69,7 +69,7 @@ export default class TransactionViewBalance extends PureComponent { value={balance} type={SECONDARY} ethNumberOfDecimals={4} - hideTitle={true} + hideTitle /> ) } diff --git a/ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js b/ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js index 51b2a3c4f..122115477 100644 --- a/ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js +++ b/ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js @@ -18,7 +18,7 @@ describe('UserPreferencedCurrencyDisplay Component', () => { it('should pass all props to the CurrencyDisplay child component', () => { const wrapper = shallow( diff --git a/ui/app/components/ui/account-dropdown-mini/account-dropdown-mini.container.js b/ui/app/components/ui/account-dropdown-mini/account-dropdown-mini.container.js index d590691d2..738ba00a9 100644 --- a/ui/app/components/ui/account-dropdown-mini/account-dropdown-mini.container.js +++ b/ui/app/components/ui/account-dropdown-mini/account-dropdown-mini.container.js @@ -42,7 +42,9 @@ class AccountDropdownMiniContainer extends Component { selectedAccount={selectedAccount} onSelect={account => { this.setState({ selectedAccount: account }) - if (onSelect) onSelect(account) + if (onSelect) { + onSelect(account) + } }} dropdownOpen={this.state.accountDropdownOpen} openDropdown={() => this.setState({ accountDropdownOpen: true })} diff --git a/ui/app/components/ui/account-dropdown-mini/tests/account-dropdown-mini.component.test.js b/ui/app/components/ui/account-dropdown-mini/tests/account-dropdown-mini.component.test.js index 9691f38aa..56176559e 100644 --- a/ui/app/components/ui/account-dropdown-mini/tests/account-dropdown-mini.component.test.js +++ b/ui/app/components/ui/account-dropdown-mini/tests/account-dropdown-mini.component.test.js @@ -62,7 +62,7 @@ describe('AccountDropdownMini', () => { ) @@ -94,7 +94,7 @@ describe('AccountDropdownMini', () => { selectedAccount={{ address: '0x1', name: 'account1', balance: '0x1' }} accounts={accounts} dropdownOpen={false} - disabled={true} + disabled /> ) diff --git a/ui/app/components/ui/button-group/tests/button-group-component.test.js b/ui/app/components/ui/button-group/tests/button-group-component.test.js index f2e512445..663d86c74 100644 --- a/ui/app/components/ui/button-group/tests/button-group-component.test.js +++ b/ui/app/components/ui/button-group/tests/button-group-component.test.js @@ -12,9 +12,9 @@ sinon.spy(ButtonGroup.prototype, 'handleButtonClick') sinon.spy(ButtonGroup.prototype, 'renderButtons') const mockButtons = [ - , - , - , + , + , + , ] describe('ButtonGroup Component', function () { diff --git a/ui/app/components/ui/currency-input/tests/currency-input.component.test.js b/ui/app/components/ui/currency-input/tests/currency-input.component.test.js index 6d4612e3c..43fa65d3a 100644 --- a/ui/app/components/ui/currency-input/tests/currency-input.component.test.js +++ b/ui/app/components/ui/currency-input/tests/currency-input.component.test.js @@ -130,7 +130,7 @@ describe('CurrencyInput Component', () => { fiatSuffix="USD" nativeSuffix="ETH" useFiat - hideFiat={true} + hideFiat nativeCurrency="ETH" currentCurrency="usd" conversionRate={231.06} diff --git a/ui/app/components/ui/eth-balance.js b/ui/app/components/ui/eth-balance.js index 7d577b716..63d0057c5 100644 --- a/ui/app/components/ui/eth-balance.js +++ b/ui/app/components/ui/eth-balance.js @@ -43,8 +43,12 @@ EthBalanceComponent.prototype.render = function () { ) } EthBalanceComponent.prototype.renderBalance = function (value) { - if (value === 'None') return value - if (value === '...') return value + if (value === 'None') { + return value + } + if (value === '...') { + return value + } const { conversionRate, diff --git a/ui/app/components/ui/export-text-container/export-text-container.component.js b/ui/app/components/ui/export-text-container/export-text-container.component.js index c632e8f26..21fd5ecec 100644 --- a/ui/app/components/ui/export-text-container/export-text-container.component.js +++ b/ui/app/components/ui/export-text-container/export-text-container.component.js @@ -12,7 +12,7 @@ class ExportTextContainer extends Component { return ( h('.export-text-container', [ h('.export-text-container__text-container', [ - h('.export-text-container__text', text), + h('.export-text-container__text.notranslate', text), ]), h('.export-text-container__buttons-container', [ h('.export-text-container__button.export-text-container__button--copy', { diff --git a/ui/app/components/ui/fiat-value.js b/ui/app/components/ui/fiat-value.js index 02111ba49..cabdd479d 100644 --- a/ui/app/components/ui/fiat-value.js +++ b/ui/app/components/ui/fiat-value.js @@ -17,7 +17,9 @@ FiatValue.prototype.render = function () { const value = formatBalance(props.value, 6) - if (value === 'None') return value + if (value === 'None') { + return value + } var fiatDisplayNumber, fiatTooltipNumber var splitBalance = value.split(' ') diff --git a/ui/app/components/ui/mascot.js b/ui/app/components/ui/mascot.js index 3b0d3e31b..e54a464db 100644 --- a/ui/app/components/ui/mascot.js +++ b/ui/app/components/ui/mascot.js @@ -46,7 +46,9 @@ Mascot.prototype.componentWillUnmount = function () { Mascot.prototype.handleAnimationEvents = function () { // only setup listeners once - if (this.animations) return + if (this.animations) { + return + } this.animations = this.props.animationEventEmitter this.animations.on('point', this.lookAt.bind(this)) this.animations.on('setFollowMouse', this.logo.setFollowMouse.bind(this.logo)) diff --git a/ui/app/components/ui/metafox-logo/tests/metafox-logo.component.test.js b/ui/app/components/ui/metafox-logo/tests/metafox-logo.component.test.js index c794a004f..015416b97 100644 --- a/ui/app/components/ui/metafox-logo/tests/metafox-logo.component.test.js +++ b/ui/app/components/ui/metafox-logo/tests/metafox-logo.component.test.js @@ -16,7 +16,7 @@ describe('MetaFoxLogo', () => { it('does not set icon height and width when unsetIconHeight is true', () => { const wrapper = mount( - + ) assert.equal(wrapper.find('img.app-header__metafox-logo--icon').prop('width'), null) diff --git a/ui/app/components/ui/page-container/page-container-footer/tests/page-container-footer.component.test.js b/ui/app/components/ui/page-container/page-container-footer/tests/page-container-footer.component.test.js index 64efabab0..36a3b67d3 100644 --- a/ui/app/components/ui/page-container/page-container-footer/tests/page-container-footer.component.test.js +++ b/ui/app/components/ui/page-container/page-container-footer/tests/page-container-footer.component.test.js @@ -12,12 +12,12 @@ describe('Page Footer', () => { beforeEach(() => { wrapper = shallow() }) diff --git a/ui/app/components/ui/page-container/page-container-header/tests/page-container-header.component.test.js b/ui/app/components/ui/page-container/page-container-header/tests/page-container-header.component.test.js index 59304b2bd..5328451e9 100644 --- a/ui/app/components/ui/page-container/page-container-header/tests/page-container-header.component.test.js +++ b/ui/app/components/ui/page-container/page-container-header/tests/page-container-header.component.test.js @@ -13,13 +13,13 @@ describe('Page Container Header', () => { onClose = sinon.spy() wrapper = shallow() }) diff --git a/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js b/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js index 186d22c71..c74523077 100644 --- a/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js +++ b/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js @@ -5,7 +5,7 @@ import Identicon from '../identicon' import Tooltip from '../tooltip-v2' import copyToClipboard from 'copy-to-clipboard' import { DEFAULT_VARIANT, CARDS_VARIANT, FLAT_VARIANT } from './sender-to-recipient.constants' -import { checksumAddress } from '../../../helpers/utils/util' +import { checksumAddress, addressSlicer } from '../../../helpers/utils/util' const variantHash = { [DEFAULT_VARIANT]: 'sender-to-recipient--default', @@ -18,6 +18,7 @@ export default class SenderToRecipient extends PureComponent { senderName: PropTypes.string, senderAddress: PropTypes.string, recipientName: PropTypes.string, + recipientEns: PropTypes.string, recipientAddress: PropTypes.string, recipientNickname: PropTypes.string, t: PropTypes.func, @@ -61,14 +62,28 @@ export default class SenderToRecipient extends PureComponent { return ( {t('copiedExclamation')}

+ : addressOnly + ?

{t('copyAddress')}

+ : ( +

+ {addressSlicer(checksummedSenderAddress)}
+ {t('copyAddress')} +

+ ) + } wrapperClassName="sender-to-recipient__tooltip-wrapper" containerClassName="sender-to-recipient__tooltip-container" onHidden={() => this.setState({ senderAddressCopied: false })} >
- { addressOnly ? `${t('from')}: ` : '' } - { addressOnly ? checksummedSenderAddress : senderName } + { + addressOnly + ? {`${t('from')}: ${checksummedSenderAddress}`} + : senderName + }
) @@ -91,7 +106,9 @@ export default class SenderToRecipient extends PureComponent { renderRecipientWithAddress () { const { t } = this.context - const { recipientName, recipientAddress, recipientNickname, addressOnly, onRecipientClick, audit = {} } = this.props + const { + recipientEns, recipientName, recipientAddress, recipientNickname, addressOnly, onRecipientClick, audit = {}, + } = this.props const checksummedRecipientAddress = checksumAddress(recipientAddress) return ( @@ -111,7 +128,18 @@ export default class SenderToRecipient extends PureComponent { { this.renderRecipientIdenticon() } {t('copiedExclamation')}

+ : (addressOnly && !recipientNickname && !recipientEns) + ?

{t('copyAddress')}

+ : ( +

+ {addressSlicer(checksummedRecipientAddress)}
+ {t('copyAddress')} +

+ ) + } wrapperClassName="sender-to-recipient__tooltip-wrapper" containerClassName="sender-to-recipient__tooltip-container" onHidden={() => this.setState({ recipientAddressCopied: false })} @@ -120,8 +148,8 @@ export default class SenderToRecipient extends PureComponent { { addressOnly ? `${t('to')}: ` : '' } { addressOnly - ? checksummedRecipientAddress - : (recipientNickname || recipientName || this.context.t('newContract')) + ? (recipientNickname || recipientEns || checksummedRecipientAddress) + : (recipientNickname || recipientEns || recipientName || this.context.t('newContract')) }
diff --git a/ui/app/components/ui/snackbar/index.js b/ui/app/components/ui/snackbar/index.js new file mode 100644 index 000000000..3d3e0394d --- /dev/null +++ b/ui/app/components/ui/snackbar/index.js @@ -0,0 +1 @@ +export { default } from './snackbar.component' diff --git a/ui/app/components/ui/snackbar/index.scss b/ui/app/components/ui/snackbar/index.scss new file mode 100644 index 000000000..5cfab7a9b --- /dev/null +++ b/ui/app/components/ui/snackbar/index.scss @@ -0,0 +1,11 @@ +.snackbar { + padding: .75rem 1rem; + font-size: 0.75rem; + color: $Blue-600; + min-width: 360px; + width: fit-content; + + background: $Blue-000; + border: 1px solid $Blue-200; + border-radius: 6px; +} diff --git a/ui/app/components/ui/snackbar/snackbar.component.js b/ui/app/components/ui/snackbar/snackbar.component.js new file mode 100644 index 000000000..8945341fe --- /dev/null +++ b/ui/app/components/ui/snackbar/snackbar.component.js @@ -0,0 +1,18 @@ +import React from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' + +const Snackbar = ({ className = '', content }) => { + return ( +
+ { content } +
+ ) +} + +Snackbar.propTypes = { + className: PropTypes.string, + content: PropTypes.string.isRequired, +} + +module.exports = Snackbar diff --git a/ui/app/components/ui/tooltip-v2.js b/ui/app/components/ui/tooltip-v2.js index b54026794..c7f94506c 100644 --- a/ui/app/components/ui/tooltip-v2.js +++ b/ui/app/components/ui/tooltip-v2.js @@ -7,7 +7,7 @@ export default class Tooltip extends PureComponent { arrow: true, children: null, containerClassName: '', - hideOnClick: false, + html: null, onHidden: null, position: 'left', size: 'small', @@ -21,6 +21,7 @@ export default class Tooltip extends PureComponent { children: PropTypes.node, containerClassName: PropTypes.string, disabled: PropTypes.bool, + html: PropTypes.node, onHidden: PropTypes.func, position: PropTypes.oneOf([ 'top', @@ -38,9 +39,9 @@ export default class Tooltip extends PureComponent { } render () { - const {arrow, children, containerClassName, disabled, position, size, title, trigger, onHidden, wrapperClassName, style } = this.props + const {arrow, children, containerClassName, disabled, position, html, size, title, trigger, onHidden, wrapperClassName, style } = this.props - if (!title) { + if (!title && !html) { return (
{children} @@ -51,6 +52,7 @@ export default class Tooltip extends PureComponent { return (
{ this.unitInput = ref }} + ref={ref => { + this.unitInput = ref + }} disabled={maxModeOn} /> { diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss index c0d1a80f4..1ad91b397 100644 --- a/ui/app/css/itcss/components/index.scss +++ b/ui/app/css/itcss/components/index.scss @@ -1,5 +1,6 @@ @import '../../../components/ui/button/buttons'; @import '../../../components/ui/dialog/dialog'; +@import '../../../components/ui/snackbar/index'; @import './footer.scss'; diff --git a/ui/app/css/itcss/components/new-account.scss b/ui/app/css/itcss/components/new-account.scss index 162aac38f..778025243 100644 --- a/ui/app/css/itcss/components/new-account.scss +++ b/ui/app/css/itcss/components/new-account.scss @@ -50,6 +50,7 @@ color: $curious-blue; border-bottom: 3px solid $curious-blue; cursor: initial; + pointer-events: none; } } } diff --git a/ui/app/css/itcss/components/wallet-balance.scss b/ui/app/css/itcss/components/wallet-balance.scss index 3c3349ae0..872435e4c 100644 --- a/ui/app/css/itcss/components/wallet-balance.scss +++ b/ui/app/css/itcss/components/wallet-balance.scss @@ -68,6 +68,5 @@ $wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and ( width: 50px; height: 50px; border: 1px solid $alto; - padding: 5px; background: $white; } diff --git a/ui/app/css/itcss/tools/utilities.scss b/ui/app/css/itcss/tools/utilities.scss index 209614c6b..81eb18d06 100644 --- a/ui/app/css/itcss/tools/utilities.scss +++ b/ui/app/css/itcss/tools/utilities.scss @@ -141,11 +141,11 @@ } .cursor-pointer:hover { - transform: scale(1.1); + transform: scale(1.05); } .cursor-pointer:active { - transform: scale(.95); + transform: scale(.97); } .cursor-disabled { diff --git a/ui/app/ducks/index.js b/ui/app/ducks/index.js index 6808252ef..f9c8821b0 100644 --- a/ui/app/ducks/index.js +++ b/ui/app/ducks/index.js @@ -67,7 +67,9 @@ window.getCleanAppState = function () { window.logStateString = function (cb) { const state = window.getCleanAppState() global.platform.getPlatformInfo((err, platform) => { - if (err) return cb(err) + if (err) { + return cb(err) + } state.platform = platform const stateString = JSON.stringify(state, null, 2) cb(null, stateString) diff --git a/ui/app/ducks/metamask/metamask.js b/ui/app/ducks/metamask/metamask.js index f1fa7cd03..23437610f 100644 --- a/ui/app/ducks/metamask/metamask.js +++ b/ui/app/ducks/metamask/metamask.js @@ -25,6 +25,7 @@ function reduceMetamask (state, action) { tokenExchangeRates: {}, tokens: [], pendingTokens: {}, + customNonceValue: '', send: { gasLimit: null, gasPrice: null, @@ -57,6 +58,7 @@ function reduceMetamask (state, action) { knownMethodData: {}, participateInMetaMetrics: null, metaMetricsSendCount: 0, + nextNonce: null, }, state.metamask) switch (action.type) { @@ -188,7 +190,10 @@ function reduceMetamask (state, action) { gasLimit: action.value, }, }) - + case actions.UPDATE_CUSTOM_NONCE: + return extend(metamaskState, { + customNonceValue: action.value, + }) case actions.UPDATE_GAS_PRICE: return extend(metamaskState, { send: { @@ -412,6 +417,12 @@ function reduceMetamask (state, action) { }) } + case actions.SET_NEXT_NONCE: { + return extend(metamaskState, { + nextNonce: action.value, + }) + } + default: return metamaskState diff --git a/ui/app/helpers/constants/common.js b/ui/app/helpers/constants/common.js index a0d6e65b3..5055f88a7 100644 --- a/ui/app/helpers/constants/common.js +++ b/ui/app/helpers/constants/common.js @@ -12,3 +12,9 @@ export const NETWORK_TYPES = { ROPSTEN: 'ropsten', GOERLI: 'goerli', } + +export const GAS_ESTIMATE_TYPES = { + SLOW: 'SLOW', + AVERAGE: 'AVERAGE', + FAST: 'FAST', +} diff --git a/ui/app/helpers/higher-order-components/i18n-provider.js b/ui/app/helpers/higher-order-components/i18n-provider.js index 1360cf5fd..d530c3425 100644 --- a/ui/app/helpers/higher-order-components/i18n-provider.js +++ b/ui/app/helpers/higher-order-components/i18n-provider.js @@ -3,12 +3,15 @@ const connect = require('react-redux').connect const PropTypes = require('prop-types') const { withRouter } = require('react-router-dom') const { compose } = require('recompose') -const t = require('../utils/i18n-helper').getMessage +const { getMessage } = require('../utils/i18n-helper') class I18nProvider extends Component { tOrDefault = (key, defaultValue, ...args) => { + if (!key) { + return defaultValue + } const { localeMessages: { current, en } = {}, currentLocale } = this.props - return t(currentLocale, current, key, ...args) || t(currentLocale, en, key, ...args) || defaultValue + return getMessage(currentLocale, current, key, ...args) || getMessage(currentLocale, en, key, ...args) || defaultValue } getChildContext () { @@ -22,11 +25,7 @@ class I18nProvider extends Component { * @return {string|undefined|null} The localized message if available */ t (key, ...args) { - if (key === undefined || key === null) { - return key - } - - return t(currentLocale, current, key, ...args) || t(currentLocale, en, key, ...args) || `[${key}]` + return getMessage(currentLocale, current, key, ...args) || getMessage(currentLocale, en, key, ...args) || `[${key}]` }, tOrDefault: this.tOrDefault, tOrKey: (key, ...args) => { diff --git a/ui/app/helpers/higher-order-components/with-token-tracker/with-token-tracker.component.js b/ui/app/helpers/higher-order-components/with-token-tracker/with-token-tracker.component.js index 36f6a6efd..8025dd5bc 100644 --- a/ui/app/helpers/higher-order-components/with-token-tracker/with-token-tracker.component.js +++ b/ui/app/helpers/higher-order-components/with-token-tracker/with-token-tracker.component.js @@ -15,6 +15,7 @@ export default function withTokenTracker (WrappedComponent) { this.state = { string: '', symbol: '', + balance: '', error: null, } @@ -78,8 +79,8 @@ export default function withTokenTracker (WrappedComponent) { if (!this.tracker.running) { return } - const [{ string, symbol }] = tokens - this.setState({ string, symbol, error: null }) + const [{ string, symbol, balance }] = tokens + this.setState({ string, symbol, error: null, balance }) } removeListeners () { @@ -91,13 +92,13 @@ export default function withTokenTracker (WrappedComponent) { } render () { - const { string, symbol, error } = this.state - + const { balance, string, symbol, error } = this.state return ( ) diff --git a/ui/app/helpers/utils/gas-time-estimates.util.js b/ui/app/helpers/utils/gas-time-estimates.util.js new file mode 100644 index 000000000..7e143a028 --- /dev/null +++ b/ui/app/helpers/utils/gas-time-estimates.util.js @@ -0,0 +1,99 @@ +import BigNumber from 'bignumber.js' + +export function newBigSigDig (n) { + return new BigNumber((new BigNumber(String(n))).toPrecision(15)) +} + +const createOp = (a, b, op) => (newBigSigDig(a))[op](newBigSigDig(b)) + +export function bigNumMinus (a = 0, b = 0) { + return createOp(a, b, 'minus') +} + +export function bigNumDiv (a = 0, b = 1) { + return createOp(a, b, 'div') +} + +export function extrapolateY ({ higherY = 0, lowerY = 0, higherX = 0, lowerX = 0, xForExtrapolation = 0 }) { + const slope = bigNumMinus(higherY, lowerY).div(bigNumMinus(higherX, lowerX)) + const newTimeEstimate = slope.times(bigNumMinus(higherX, xForExtrapolation)).minus(newBigSigDig(higherY)).negated() + + return newTimeEstimate.toNumber() +} + +export function getAdjacentGasPrices ({ gasPrices, priceToPosition }) { + const closestLowerValueIndex = gasPrices.findIndex((e, i, a) => e <= priceToPosition && a[i + 1] >= priceToPosition) + const closestHigherValueIndex = gasPrices.findIndex((e) => e > priceToPosition) + return { + closestLowerValueIndex, + closestHigherValueIndex, + closestHigherValue: gasPrices[closestHigherValueIndex], + closestLowerValue: gasPrices[closestLowerValueIndex], + } +} + +export function formatTimeEstimate (totalSeconds, greaterThanMax, lessThanMin) { + const minutes = Math.floor(totalSeconds / 60) + const seconds = Math.floor(totalSeconds % 60) + + if (!minutes && !seconds) { + return '...' + } + + let symbol = '~' + if (greaterThanMax) { + symbol = '< ' + } else if (lessThanMin) { + symbol = '> ' + } + + const formattedMin = `${minutes ? minutes + ' min' : ''}` + const formattedSec = `${seconds ? seconds + ' sec' : ''}` + const formattedCombined = formattedMin && formattedSec + ? `${symbol}${formattedMin} ${formattedSec}` + : symbol + (formattedMin || formattedSec) + + return formattedCombined +} + +export function getRawTimeEstimateData (currentGasPrice, gasPrices, estimatedTimes) { + const minGasPrice = gasPrices[0] + const maxGasPrice = gasPrices[gasPrices.length - 1] + let priceForEstimation = currentGasPrice + if (currentGasPrice < minGasPrice) { + priceForEstimation = minGasPrice + } else if (currentGasPrice > maxGasPrice) { + priceForEstimation = maxGasPrice + } + + const { + closestLowerValueIndex, + closestHigherValueIndex, + closestHigherValue, + closestLowerValue, + } = getAdjacentGasPrices({ gasPrices, priceToPosition: priceForEstimation }) + + const newTimeEstimate = extrapolateY({ + higherY: estimatedTimes[closestHigherValueIndex], + lowerY: estimatedTimes[closestLowerValueIndex], + higherX: closestHigherValue, + lowerX: closestLowerValue, + xForExtrapolation: priceForEstimation, + }) + + return { + newTimeEstimate, + minGasPrice, + maxGasPrice, + } +} + +export function getRenderableTimeEstimate (currentGasPrice, gasPrices, estimatedTimes) { + const { + newTimeEstimate, + minGasPrice, + maxGasPrice, + } = getRawTimeEstimateData(currentGasPrice, gasPrices, estimatedTimes) + + return formatTimeEstimate(newTimeEstimate, currentGasPrice > maxGasPrice, currentGasPrice < minGasPrice) +} diff --git a/ui/app/helpers/utils/i18n-helper.js b/ui/app/helpers/utils/i18n-helper.js index b9720acc7..cd3f1527d 100644 --- a/ui/app/helpers/utils/i18n-helper.js +++ b/ui/app/helpers/utils/i18n-helper.js @@ -1,7 +1,10 @@ // cross-browser connection to extension i18n API const log = require('loglevel') +const Sentry = require('@sentry/browser') const warned = {} +const missingMessageErrors = {} + /** * Returns a localized message for the given key * @param {string} localeCode The code for the current locale @@ -10,12 +13,21 @@ const warned = {} * @param {string[]} substitutions A list of message substitution replacements * @return {null|string} The localized message */ -const getMessage = (localeCode, localeMessages, key, substitutions) => { +export const getMessage = (localeCode, localeMessages, key, substitutions) => { if (!localeMessages) { return null } if (!localeMessages[key]) { - if (!warned[localeCode] || !warned[localeCode][key]) { + if (localeCode === 'en') { + if (!missingMessageErrors[key]) { + missingMessageErrors[key] = new Error(`Unable to find value of key "${key}" for locale "${localeCode}"`) + Sentry.captureException(missingMessageErrors[key]) + log.error(missingMessageErrors[key]) + if (process.env.IN_TEST === 'true') { + throw missingMessageErrors[key] + } + } + } else if (!warned[localeCode] || !warned[localeCode][key]) { if (!warned[localeCode]) { warned[localeCode] = {} } @@ -36,7 +48,7 @@ const getMessage = (localeCode, localeMessages, key, substitutions) => { return phrase } -async function fetchLocale (localeCode) { +export async function fetchLocale (localeCode) { try { const response = await fetch(`./_locales/${localeCode}/messages.json`) return await response.json() @@ -46,7 +58,3 @@ async function fetchLocale (localeCode) { } } -module.exports = { - getMessage, - fetchLocale, -} diff --git a/ui/app/helpers/utils/metametrics.util.js b/ui/app/helpers/utils/metametrics.util.js index 50270c6a8..560d8bd9e 100644 --- a/ui/app/helpers/utils/metametrics.util.js +++ b/ui/app/helpers/utils/metametrics.util.js @@ -67,7 +67,7 @@ const customDimensionsNameIdMap = { } function composeUrlRefParamAddition (previousPath, confirmTransactionOrigin) { - const externalOrigin = confirmTransactionOrigin && confirmTransactionOrigin !== 'MetaMask' + const externalOrigin = confirmTransactionOrigin && confirmTransactionOrigin !== 'metamask' return `&urlref=${externalOrigin ? 'EXTERNAL' : encodeURIComponent(previousPath.replace(/chrome-extension:\/\/\w+/, METAMETRICS_TRACKING_URL))}` } diff --git a/ui/app/helpers/utils/token-util.js b/ui/app/helpers/utils/token-util.js index 831d85131..2c4f67fd0 100644 --- a/ui/app/helpers/utils/token-util.js +++ b/ui/app/helpers/utils/token-util.js @@ -128,6 +128,11 @@ export function calcTokenAmount (value, decimals) { return new BigNumber(String(value)).div(multiplier) } +export function calcTokenValue (value, decimals) { + const multiplier = Math.pow(10, Number(decimals || 0)) + return new BigNumber(String(value)).times(multiplier) +} + export function getTokenValue (tokenParams = []) { const valueData = tokenParams.find(param => param.name === '_value') return valueData && valueData.value diff --git a/ui/app/helpers/utils/util.js b/ui/app/helpers/utils/util.js index 2dc8a7ad4..2662c41aa 100644 --- a/ui/app/helpers/utils/util.js +++ b/ui/app/helpers/utils/util.js @@ -75,13 +75,19 @@ function isEthNetwork (netId) { } function valuesFor (obj) { - if (!obj) return [] + if (!obj) { + return [] + } return Object.keys(obj) - .map(function (key) { return obj[key] }) + .map(function (key) { + return obj[key] + }) } function addressSummary (address, firstSegLength = 10, lastSegLength = 4, includeHex = true) { - if (!address) return '' + if (!address) { + return '' + } let checked = checksumAddress(address) if (!includeHex) { checked = ethUtil.stripHexPrefix(checked) @@ -90,29 +96,37 @@ function addressSummary (address, firstSegLength = 10, lastSegLength = 4, includ } function miniAddressSummary (address) { - if (!address) return '' + if (!address) { + return '' + } var checked = checksumAddress(address) return checked ? checked.slice(0, 4) + '...' + checked.slice(-4) : '...' } function isValidAddress (address) { var prefixed = ethUtil.addHexPrefix(address) - if (address === '0x0000000000000000000000000000000000000000') return false + if (address === '0x0000000000000000000000000000000000000000') { + return false + } return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed) } function isValidENSAddress (address) { - return address.match(/^.{7,}\.(eth|test)$/) + return address.match(/^.{3,}\.(eth|test|xyz)$/) } function isInvalidChecksumAddress (address) { var prefixed = ethUtil.addHexPrefix(address) - if (address === '0x0000000000000000000000000000000000000000') return false + if (address === '0x0000000000000000000000000000000000000000') { + return false + } return !isAllOneCase(prefixed) && !ethUtil.isValidChecksumAddress(prefixed) && ethUtil.isValidAddress(prefixed) } function isAllOneCase (address) { - if (!address) return true + if (!address) { + return true + } var lower = address.toLowerCase() var upper = address.toUpperCase() return address === lower || address === upper @@ -120,7 +134,9 @@ function isAllOneCase (address) { // Takes wei Hex, returns wei BN, even if input is null function numericBalance (balance) { - if (!balance) return new ethUtil.BN(0, 16) + if (!balance) { + return new ethUtil.BN(0, 16) + } var stripped = ethUtil.stripHexPrefix(balance) return new ethUtil.BN(stripped, 16) } @@ -134,7 +150,9 @@ function parseBalance (balance) { beforeDecimal = weiString.length > 18 ? weiString.slice(0, weiString.length - 18) : '0' afterDecimal = ('000000000000000000' + wei).slice(-18).replace(trailingZeros, '') - if (afterDecimal === '') { afterDecimal = '0' } + if (afterDecimal === '') { + afterDecimal = '0' + } return [beforeDecimal, afterDecimal] } @@ -149,7 +167,9 @@ function formatBalance (balance, decimalsToKeep, needsParse = true, ticker = 'ET if (beforeDecimal === '0') { if (afterDecimal !== '0') { var sigFigs = afterDecimal.match(/^0*(.{2})/) // default: grabs 2 most significant digits - if (sigFigs) { afterDecimal = sigFigs[0] } + if (sigFigs) { + afterDecimal = sigFigs[0] + } formatted = '0.' + afterDecimal + ` ${ticker}` } } else { @@ -335,7 +355,11 @@ function isValidAddressHead (address) { } function stringify (val) { - if (typeof val === 'string') return val - if (typeof val === 'object') return safeStringify(val, null, 2) + if (typeof val === 'string') { + return val + } + if (typeof val === 'object') { + return safeStringify(val, null, 2) + } return val.toString() } diff --git a/ui/app/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js b/ui/app/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js new file mode 100644 index 000000000..38644541d --- /dev/null +++ b/ui/app/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js @@ -0,0 +1,226 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import Identicon from '../../../components/ui/identicon' +import { + addressSummary, +} from '../../../helpers/utils/util' +import { formatCurrency } from '../../../helpers/utils/confirm-tx.util' + +export default class ConfirmApproveContent extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + amount: PropTypes.string, + txFeeTotal: PropTypes.string, + tokenAmount: PropTypes.string, + customTokenAmount: PropTypes.string, + tokenSymbol: PropTypes.string, + siteImage: PropTypes.string, + tokenAddress: PropTypes.string, + showCustomizeGasModal: PropTypes.func, + showEditApprovalPermissionModal: PropTypes.func, + origin: PropTypes.string, + setCustomAmount: PropTypes.func, + tokenBalance: PropTypes.string, + data: PropTypes.string, + toAddress: PropTypes.string, + currentCurrency: PropTypes.string, + fiatTransactionTotal: PropTypes.string, + ethTransactionTotal: PropTypes.string, + } + + state = { + showFullTxDetails: false, + } + + renderApproveContentCard ({ + symbol, + title, + showEdit, + onEditClick, + content, + footer, + noBorder, + }) { + return ( +
+
+
{ symbol }
+
{ title }
+ { showEdit &&
onEditClick()} + >Edit
} +
+
+ { content } +
+ { footer } +
+ ) + } + + // TODO: Add "Learn Why" with link to the feeAssociatedRequest text + renderTransactionDetailsContent () { + const { t } = this.context + const { + currentCurrency, + ethTransactionTotal, + fiatTransactionTotal, + } = this.props + return ( +
+
+ { t('feeAssociatedRequest') } +
+
+
+ { formatCurrency(fiatTransactionTotal, currentCurrency) } +
+
+ { `${ethTransactionTotal} ETH` } +
+
+
+ ) + } + + renderPermissionContent () { + const { t } = this.context + const { customTokenAmount, tokenAmount, tokenSymbol, origin, toAddress } = this.props + + return ( +
+
{ t('accessAndSpendNotice', [origin]) }
+
+
{ t('amountWithColon') }
+
{ `${customTokenAmount || tokenAmount} ${tokenSymbol}` }
+
+
+
{ t('toWithColon') }
+
{ addressSummary(toAddress) }
+
+
+ ) + } + + renderDataContent () { + const { t } = this.context + const { data } = this.props + return ( +
+
{ t('functionApprove') }
+
{ data }
+
+ ) + } + + render () { + const { t } = this.context + const { + siteImage, + tokenAmount, + customTokenAmount, + origin, + tokenSymbol, + showCustomizeGasModal, + showEditApprovalPermissionModal, + setCustomAmount, + tokenBalance, + } = this.props + const { showFullTxDetails } = this.state + + return ( +
+
+ +
+
+ { t('allowOriginSpendToken', [origin, tokenSymbol]) } +
+
+ { t('trustSiteApprovePermission', [origin, tokenSymbol]) } +
+
+
showEditApprovalPermissionModal({ customTokenAmount, tokenAmount, tokenSymbol, setCustomAmount, tokenBalance, origin })} + > + { t('editPermission') } +
+
+
+ {this.renderApproveContentCard({ + symbol: , + title: 'Transaction Fee', + showEdit: true, + onEditClick: showCustomizeGasModal, + content: this.renderTransactionDetailsContent(), + noBorder: !showFullTxDetails, + footer:
this.setState({ showFullTxDetails: !this.state.showFullTxDetails })} + > +
+
+ View full transaction details +
+ +
+
, + })} +
+ + { + showFullTxDetails + ? ( +
+
+ {this.renderApproveContentCard({ + symbol: , + title: 'Permission', + content: this.renderPermissionContent(), + showEdit: true, + onEditClick: () => showEditApprovalPermissionModal({ + customTokenAmount, + tokenAmount, + tokenSymbol, + tokenBalance, + setCustomAmount, + }), + })} +
+
+ {this.renderApproveContentCard({ + symbol: , + title: 'Data', + content: this.renderDataContent(), + noBorder: true, + })} +
+
+ ) + : null + } +
+ ) + } +} diff --git a/ui/app/pages/confirm-approve/confirm-approve-content/index.js b/ui/app/pages/confirm-approve/confirm-approve-content/index.js new file mode 100644 index 000000000..8f225387a --- /dev/null +++ b/ui/app/pages/confirm-approve/confirm-approve-content/index.js @@ -0,0 +1 @@ +export { default } from './confirm-approve-content.component' diff --git a/ui/app/pages/confirm-approve/confirm-approve-content/index.scss b/ui/app/pages/confirm-approve/confirm-approve-content/index.scss new file mode 100644 index 000000000..7d3018f6e --- /dev/null +++ b/ui/app/pages/confirm-approve/confirm-approve-content/index.scss @@ -0,0 +1,306 @@ +.confirm-approve-content { + display: flex; + flex-flow: column; + align-items: center; + width: 100%; + height: 100%; + + font-family: Roboto; + font-style: normal; + + &__identicon-wrapper { + display: flex; + width: 100%; + justify-content: center; + margin-top: 22px; + padding-left: 24px; + padding-right: 24px; + } + + &__full-tx-content { + display: flex; + flex-flow: column; + align-items: center; + width: 390px; + font-family: Roboto; + font-style: normal; + padding-left: 24px; + padding-right: 24px; + } + + &__card-wrapper { + width: 100%; + } + + &__title { + font-weight: normal; + font-size: 24px; + line-height: 34px; + width: 100%; + display: flex; + justify-content: center; + text-align: center; + margin-top: 22px; + padding-left: 24px; + padding-right: 24px; + } + + &__description { + font-weight: normal; + font-size: 14px; + line-height: 20px; + margin-top: 16px; + margin-bottom: 16px; + color: #6A737D; + text-align: center; + padding-left: 24px; + padding-right: 24px; + } + + &__card, + &__card--no-border { + display: flex; + flex-flow: column; + border-bottom: 1px solid #D2D8DD; + position: relative; + padding-left: 24px; + padding-right: 24px; + + &__bold-text { + font-weight: bold; + font-size: 14px; + line-height: 20px; + } + + &__thin-text { + font-weight: normal; + font-size: 12px; + line-height: 17px; + color: #6A737D; + } + } + + &__card--no-border { + border-bottom: none; + } + + &__card-header { + display: flex; + flex-flow: row; + margin-top: 20px; + align-items: center; + position: relative; + + &__symbol { + width: auto; + } + + &__symbol--aligned { + width: 100%; + } + + &__title, &__title-value { + font-weight: bold; + font-size: 14px; + line-height: 20px; + } + + &__title { + width: 100%; + margin-left: 16px; + } + + &__title--aligned { + margin-left: 27px; + position: absolute; + width: auto; + } + } + + &__card-content { + margin-top: 6px; + margin-bottom: 12px; + } + + &__card-content--aligned { + margin-left: 42px; + } + + &__transaction-total-symbol { + width: 16px; + display: flex; + justify-content: center; + align-items: center; + height: 16px; + + &__x { + display: flex; + justify-content: center; + align-items: center; + + div { + width: 22px; + height: 2px; + background: #037DD6; + position: absolute; + } + + div:first-of-type { + transform: rotate(45deg); + } + + div:last-of-type { + transform: rotate(-45deg); + } + } + + &__circle { + width: 14px; + height: 14px; + border: 2px solid #037DD6; + border-radius: 50%; + background: white; + position: absolute; + } + } + + &__transaction-details-content { + display: flex; + flex-flow: row; + justify-content: space-between; + + .confirm-approve-content__small-text { + width: 160px; + } + + &__fee { + display: flex; + flex-flow: column; + align-items: flex-end; + text-align: right; + } + + &__primary-fee { + font-weight: bold; + font-size: 18px; + line-height: 25px; + color: #000000; + } + + &__secondary-fee { + font-weight: normal; + font-size: 14px; + line-height: 20px; + color: #8C8E94; + } + } + + &__view-full-tx-button-wrapper { + display: flex; + flex-flow: row; + margin-bottom: 16px; + justify-content: center; + + i { + margin-left: 6px; + display: flex; + color: #3099f2; + align-items: center; + } + } + + &__view-full-tx-button { + display: flex; + flex-flow: row; + } + + &__edit-submission-button-container { + display: flex; + flex-flow: row; + padding-top: 15px; + padding-bottom: 30px; + border-bottom: 1px solid #D2D8DD; + width: 100%; + justify-content: center; + padding-left: 24px; + padding-right: 24px; + } + + &__large-text { + font-size: 18px; + line-height: 25px; + color: #24292E; + } + + &__medium-link-text { + font-size: 14px; + line-height: 20px; + font-weight: 500; + color: #037DD6; + } + + &__medium-text, + &__label { + font-weight: normal; + font-size: 14px; + line-height: 20px; + color: #24292E; + } + + &__label { + font-weight: bold; + margin-right: 4px; + } + + &__small-text, &__small-blue-text, &__info-row { + font-weight: normal; + font-size: 12px; + line-height: 17px; + color: #6A737D; + } + + &__small-blue-text { + color: #037DD6; + } + + &__info-row { + display: flex; + justify-content: space-between; + margin-bottom: 6px; + } + + &__data, + &__permission { + width: 100%; + } + + &__permission { + .flex-row { + margin-top: 14px; + } + } + + &__data { + &__data-block { + overflow-wrap: break-word; + margin-right: 16px; + margin-top: 12px; + } + } + + &__footer { + display: flex; + align-items: flex-end; + margin-top: 16px; + padding-left: 34px; + padding-right: 24px; + + .confirm-approve-content__small-text { + margin-left: 16px; + } + } +} + +.confirm-approve-content--full { + height: auto; +} diff --git a/ui/app/pages/confirm-approve/confirm-approve.component.js b/ui/app/pages/confirm-approve/confirm-approve.component.js index b71eaa1d4..e8c44cd4f 100644 --- a/ui/app/pages/confirm-approve/confirm-approve.component.js +++ b/ui/app/pages/confirm-approve/confirm-approve.component.js @@ -1,20 +1,111 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import ConfirmTokenTransactionBase from '../confirm-token-transaction-base' +import ConfirmTransactionBase from '../confirm-transaction-base' +import ConfirmApproveContent from './confirm-approve-content' +import { getCustomTxParamsData } from './confirm-approve.util' +import { + calcTokenAmount, +} from '../../helpers/utils/token-util' export default class ConfirmApprove extends Component { + static contextTypes = { + t: PropTypes.func, + } + static propTypes = { + tokenAddress: PropTypes.string, + toAddress: PropTypes.string, tokenAmount: PropTypes.number, tokenSymbol: PropTypes.string, + fiatTransactionTotal: PropTypes.string, + ethTransactionTotal: PropTypes.string, + contractExchangeRate: PropTypes.number, + conversionRate: PropTypes.number, + currentCurrency: PropTypes.string, + showCustomizeGasModal: PropTypes.func, + showEditApprovalPermissionModal: PropTypes.func, + origin: PropTypes.string, + siteImage: PropTypes.string, + tokenTrackerBalance: PropTypes.string, + data: PropTypes.string, + decimals: PropTypes.number, + txData: PropTypes.object, + } + + static defaultProps = { + tokenAmount: 0, + } + + state = { + customPermissionAmount: '', + } + + componentDidUpdate (prevProps) { + const { tokenAmount } = this.props + + if (tokenAmount !== prevProps.tokenAmount) { + this.setState({ customPermissionAmount: tokenAmount }) + } } render () { - const { tokenAmount, tokenSymbol } = this.props + const { + toAddress, + tokenAddress, + tokenSymbol, + tokenAmount, + showCustomizeGasModal, + showEditApprovalPermissionModal, + origin, + siteImage, + tokenTrackerBalance, + data, + decimals, + txData, + currentCurrency, + ethTransactionTotal, + fiatTransactionTotal, + ...restProps + } = this.props + const { customPermissionAmount } = this.state + + const tokensText = `${tokenAmount} ${tokenSymbol}` + + const tokenBalance = tokenTrackerBalance + ? Number(calcTokenAmount(tokenTrackerBalance, decimals)).toPrecision(9) + : '' return ( - { + this.setState({ customPermissionAmount: newAmount }) + }} + customTokenAmount={String(customPermissionAmount)} + tokenAmount={String(tokenAmount)} + origin={origin} + tokenSymbol={tokenSymbol} + tokenBalance={tokenBalance} + showCustomizeGasModal={() => showCustomizeGasModal(txData)} + showEditApprovalPermissionModal={showEditApprovalPermissionModal} + data={data} + toAddress={toAddress} + currentCurrency={currentCurrency} + ethTransactionTotal={ethTransactionTotal} + fiatTransactionTotal={fiatTransactionTotal} + />} + hideSenderToRecipient + customTxParamsData={customPermissionAmount + ? getCustomTxParamsData(data, { customPermissionAmount, tokenAmount, decimals }) + : null + } + {...restProps} /> ) } diff --git a/ui/app/pages/confirm-approve/confirm-approve.container.js b/ui/app/pages/confirm-approve/confirm-approve.container.js index 5f8bb8f0b..43f5aab90 100644 --- a/ui/app/pages/confirm-approve/confirm-approve.container.js +++ b/ui/app/pages/confirm-approve/confirm-approve.container.js @@ -1,15 +1,102 @@ import { connect } from 'react-redux' +import { compose } from 'recompose' +import { withRouter } from 'react-router-dom' +import { + contractExchangeRateSelector, + transactionFeeSelector, +} from '../../selectors/confirm-transaction' +import { showModal } from '../../store/actions' +import { tokenSelector } from '../../selectors/tokens' +import { + getTokenData, +} from '../../helpers/utils/transactions.util' +import withTokenTracker from '../../helpers/higher-order-components/with-token-tracker' +import { + calcTokenAmount, + getTokenToAddress, + getTokenValue, +} from '../../helpers/utils/token-util' import ConfirmApprove from './confirm-approve.component' -import { approveTokenAmountAndToAddressSelector } from '../../selectors/confirm-transaction' -const mapStateToProps = state => { - const { confirmTransaction: { tokenProps: { tokenSymbol } = {} } } = state - const { tokenAmount } = approveTokenAmountAndToAddressSelector(state) +const mapStateToProps = (state, ownProps) => { + const { match: { params = {} } } = ownProps + const { id: paramsTransactionId } = params + const { + confirmTransaction, + metamask: { currentCurrency, conversionRate, selectedAddressTxList, approvedOrigins, selectedAddress }, + } = state + const { + txData: { id: transactionId, txParams: { to: tokenAddress, data } = {} } = {}, + } = confirmTransaction + + const transaction = selectedAddressTxList.find(({ id }) => id === (Number(paramsTransactionId) || transactionId)) || {} + + const { + ethTransactionTotal, + fiatTransactionTotal, + } = transactionFeeSelector(state, transaction) + const tokens = tokenSelector(state) + const currentToken = tokens && tokens.find(({ address }) => tokenAddress === address) + const { decimals, symbol: tokenSymbol } = currentToken || {} + + const tokenData = getTokenData(data) + const tokenValue = tokenData && getTokenValue(tokenData.params) + const toAddress = tokenData && getTokenToAddress(tokenData.params) + const tokenAmount = tokenData && calcTokenAmount(tokenValue, decimals).toNumber() + const contractExchangeRate = contractExchangeRateSelector(state) + + const { origin } = transaction + const formattedOrigin = origin + ? origin[0].toUpperCase() + origin.slice(1) + : '' + + const { siteImage } = approvedOrigins[origin] || {} return { + toAddress, + tokenAddress, tokenAmount, + currentCurrency, + conversionRate, + contractExchangeRate, + fiatTransactionTotal, + ethTransactionTotal, tokenSymbol, + siteImage, + token: { address: tokenAddress }, + userAddress: selectedAddress, + origin: formattedOrigin, + data, + decimals: Number(decimals), + txData: transaction, } } -export default connect(mapStateToProps)(ConfirmApprove) +const mapDispatchToProps = (dispatch) => { + return { + showCustomizeGasModal: (txData) => dispatch(showModal({ name: 'CUSTOMIZE_GAS', txData })), + showEditApprovalPermissionModal: ({ + tokenAmount, + customTokenAmount, + tokenSymbol, + tokenBalance, + setCustomAmount, + origin, + }) => dispatch(showModal({ + name: 'EDIT_APPROVAL_PERMISSION', + tokenAmount, + customTokenAmount, + tokenSymbol, + tokenBalance, + setCustomAmount, + origin, + })), + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps), + withTokenTracker, +)(ConfirmApprove) + diff --git a/ui/app/pages/confirm-approve/confirm-approve.util.js b/ui/app/pages/confirm-approve/confirm-approve.util.js new file mode 100644 index 000000000..be77c65f9 --- /dev/null +++ b/ui/app/pages/confirm-approve/confirm-approve.util.js @@ -0,0 +1,28 @@ +import { decimalToHex } from '../../helpers/utils/conversions.util' +import { calcTokenValue } from '../../helpers/utils/token-util.js' + +export function getCustomTxParamsData (data, { customPermissionAmount, tokenAmount, decimals }) { + if (customPermissionAmount) { + const tokenValue = decimalToHex(calcTokenValue(tokenAmount, decimals)) + + const re = new RegExp('(^.+)' + tokenValue + '$') + const matches = re.exec(data) + + if (!matches || !matches[1]) { + return data + } + let dataWithoutCurrentAmount = matches[1] + const customPermissionValue = decimalToHex(calcTokenValue(Number(customPermissionAmount), decimals)) + + const differenceInLengths = customPermissionValue.length - tokenValue.length + const zeroModifier = dataWithoutCurrentAmount.length - differenceInLengths + if (differenceInLengths > 0) { + dataWithoutCurrentAmount = dataWithoutCurrentAmount.slice(0, zeroModifier) + } else if (differenceInLengths < 0) { + dataWithoutCurrentAmount = dataWithoutCurrentAmount.padEnd(zeroModifier, 0) + } + + const customTxParamsData = dataWithoutCurrentAmount + customPermissionValue + return customTxParamsData + } +} diff --git a/ui/app/pages/confirm-approve/index.scss b/ui/app/pages/confirm-approve/index.scss new file mode 100644 index 000000000..18d7c29e8 --- /dev/null +++ b/ui/app/pages/confirm-approve/index.scss @@ -0,0 +1 @@ +@import 'confirm-approve-content/index'; diff --git a/ui/app/pages/confirm-deploy-contract/confirm-deploy-contract.component.js b/ui/app/pages/confirm-deploy-contract/confirm-deploy-contract.component.js index c90ccc917..2bb2b8156 100644 --- a/ui/app/pages/confirm-deploy-contract/confirm-deploy-contract.component.js +++ b/ui/app/pages/confirm-deploy-contract/confirm-deploy-contract.component.js @@ -56,7 +56,7 @@ export default class ConfirmDeployContract extends Component { render () { return ( ) diff --git a/ui/app/pages/confirm-send-ether/confirm-send-ether.component.js b/ui/app/pages/confirm-send-ether/confirm-send-ether.component.js index 68280f624..6bc252dbb 100644 --- a/ui/app/pages/confirm-send-ether/confirm-send-ether.component.js +++ b/ui/app/pages/confirm-send-ether/confirm-send-ether.component.js @@ -30,7 +30,7 @@ export default class ConfirmSendEther extends Component { return ( this.handleEdit(confirmTransactionData)} /> 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 1fb33d472..e24ad093a 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 @@ -14,7 +14,9 @@ import { import { CONFIRMED_STATUS, DROPPED_STATUS } from '../../helpers/constants/transactions' import UserPreferencedCurrencyDisplay from '../../components/app/user-preferenced-currency-display' import { PRIMARY, SECONDARY } from '../../helpers/constants/common' +import { hexToDecimal } from '../../helpers/utils/conversions.util' import AdvancedGasInputs from '../../components/app/gas-customization/advanced-gas-inputs' +import TextField from '../../components/ui/text-field' export default class ConfirmTransactionBase extends Component { static contextTypes = { @@ -50,6 +52,9 @@ export default class ConfirmTransactionBase extends Component { isTxReprice: PropTypes.bool, methodData: PropTypes.object, nonce: PropTypes.string, + useNonceField: PropTypes.bool, + customNonceValue: PropTypes.string, + updateCustomNonce: PropTypes.func, assetImage: PropTypes.string, sendTransaction: PropTypes.func, showCustomizeGasModal: PropTypes.func, @@ -59,6 +64,7 @@ export default class ConfirmTransactionBase extends Component { tokenData: PropTypes.object, tokenProps: PropTypes.object, toName: PropTypes.string, + toEns: PropTypes.string, toNickname: PropTypes.string, transactionStatus: PropTypes.string, txData: PropTypes.object, @@ -97,11 +103,17 @@ export default class ConfirmTransactionBase extends Component { hideFiatConversion: PropTypes.bool, transactionCategory: PropTypes.string, recipientAudit: PropTypes.object, + getNextNonce: PropTypes.func, + nextNonce: PropTypes.number, + tryReverseResolveAddress: PropTypes.func.isRequired, + hideSenderToRecipient: PropTypes.bool, + showAccountInHeader: PropTypes.bool, } state = { submitting: false, submitError: null, + submitWarning: '', } componentDidUpdate (prevProps) { @@ -110,11 +122,21 @@ export default class ConfirmTransactionBase extends Component { showTransactionConfirmedModal, history, clearConfirmTransaction, + nextNonce, + customNonceValue, } = this.props const { transactionStatus: prevTxStatus } = prevProps const statusUpdated = transactionStatus !== prevTxStatus const txDroppedOrConfirmed = transactionStatus === DROPPED_STATUS || transactionStatus === CONFIRMED_STATUS + if (nextNonce !== prevProps.nextNonce || customNonceValue !== prevProps.customNonceValue) { + if (customNonceValue > nextNonce) { + this.setState({ submitWarning: this.context.t('nextNonceWarning', [nextNonce]) }) + } else { + this.setState({ submitWarning: '' }) + } + } + if (statusUpdated && txDroppedOrConfirmed) { showTransactionConfirmedModal({ onSubmit: () => { @@ -155,7 +177,7 @@ export default class ConfirmTransactionBase extends Component { } } - if (customGas.gasLimit < 21000) { + if (hexToDecimal(customGas.gasLimit) < 21000) { return { valid: false, errorKey: GAS_LIMIT_TOO_LOW_ERROR_KEY, @@ -205,11 +227,16 @@ export default class ConfirmTransactionBase extends Component { hexTransactionFee, hexTransactionTotal, hideDetails, + useNonceField, + customNonceValue, + updateCustomNonce, advancedInlineGasShown, customGas, insufficientBalance, updateGasAndCalculate, hideFiatConversion, + nextNonce, + getNextNonce, } = this.props if (hideDetails) { @@ -235,13 +262,13 @@ export default class ConfirmTransactionBase extends Component { customGasPrice={customGas.gasPrice} customGasLimit={customGas.gasLimit} insufficientBalance={insufficientBalance} - customPriceIsSafe={true} + customPriceIsSafe isSpeedUp={false} /> : null }
-
+
+ {useNonceField ?
+
+
+ { this.context.t('nonceFieldHeading') } +
+
+ { + if (!value.length || Number(value) < 0) { + updateCustomNonce('') + } else { + updateCustomNonce(String(Math.floor(value))) + } + getNextNonce() + }} + fullWidth + margin="dense" + value={ customNonceValue || '' } + /> +
+
+
: null}
) ) @@ -338,7 +390,8 @@ export default class ConfirmTransactionBase extends Component { showRejectTransactionsConfirmationModal({ unapprovedTxCount, - async onSubmit () { + onSubmit: async () => { + this._removeBeforeUnload() await cancelAllTransactions() clearConfirmTransaction() history.push(DEFAULT_ROUTE) @@ -348,21 +401,33 @@ export default class ConfirmTransactionBase extends Component { handleCancel () { const { metricsEvent } = this.context - const { onCancel, txData, cancelTransaction, history, clearConfirmTransaction, actionKey, txData: { origin }, methodData = {} } = this.props + const { + onCancel, + txData, + cancelTransaction, + history, + clearConfirmTransaction, + actionKey, + txData: { origin }, + methodData = {}, + updateCustomNonce, + } = this.props + this._removeBeforeUnload() + metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Confirm Screen', + name: 'Cancel', + }, + customVariables: { + recipientKnown: null, + functionType: actionKey || getMethodName(methodData.name) || 'contractInteraction', + origin, + }, + }) + updateCustomNonce('') if (onCancel) { - metricsEvent({ - eventOpts: { - category: 'Transactions', - action: 'Confirm Screen', - name: 'Cancel', - }, - customVariables: { - recipientKnown: null, - functionType: actionKey || getMethodName(methodData.name) || 'contractInteraction', - origin, - }, - }) onCancel(txData) } else { cancelTransaction(txData) @@ -375,7 +440,19 @@ export default class ConfirmTransactionBase extends Component { handleSubmit () { const { metricsEvent } = this.context - const { txData: { origin }, sendTransaction, clearConfirmTransaction, txData, history, onSubmit, actionKey, metaMetricsSendCount = 0, setMetaMetricsSendCount, methodData = {} } = this.props + const { + txData: { origin }, + sendTransaction, + clearConfirmTransaction, + txData, + history, + onSubmit, + actionKey, + metaMetricsSendCount = 0, + setMetaMetricsSendCount, + methodData = {}, + updateCustomNonce, + } = this.props const { submitting } = this.state if (submitting) { @@ -386,6 +463,7 @@ export default class ConfirmTransactionBase extends Component { submitting: true, submitError: null, }, () => { + this._removeBeforeUnload() metricsEvent({ eventOpts: { category: 'Transactions', @@ -407,6 +485,7 @@ export default class ConfirmTransactionBase extends Component { this.setState({ submitting: false, }) + updateCustomNonce('') }) } else { sendTransaction(txData) @@ -416,6 +495,7 @@ export default class ConfirmTransactionBase extends Component { submitting: false, }, () => { history.push(DEFAULT_ROUTE) + updateCustomNonce('') }) }) .catch(error => { @@ -423,6 +503,7 @@ export default class ConfirmTransactionBase extends Component { submitting: false, submitError: error.message, }) + updateCustomNonce('') }) } }) @@ -493,9 +574,31 @@ export default class ConfirmTransactionBase extends Component { } } - componentDidMount () { + _beforeUnload = () => { const { txData: { origin, id } = {}, cancelTransaction } = this.props const { metricsEvent } = this.context + metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Confirm Screen', + name: 'Cancel Tx Via Notification Close', + }, + customVariables: { + origin, + }, + }) + cancelTransaction({ id }) + } + + _removeBeforeUnload = () => { + if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) { + window.removeEventListener('beforeunload', this._beforeUnload) + } + } + + componentDidMount () { + const { toAddress, txData: { origin } = {}, getNextNonce, tryReverseResolveAddress } = this.props + const { metricsEvent } = this.context metricsEvent({ eventOpts: { category: 'Transactions', @@ -508,20 +611,15 @@ export default class ConfirmTransactionBase extends Component { }) if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) { - window.onbeforeunload = () => { - metricsEvent({ - eventOpts: { - category: 'Transactions', - action: 'Confirm Screen', - name: 'Cancel Tx Via Notification Close', - }, - customVariables: { - origin, - }, - }) - cancelTransaction({ id }) - } + window.addEventListener('beforeunload', this._beforeUnload) } + + getNextNonce() + tryReverseResolveAddress(toAddress) + } + + componentWillUnmount () { + this._removeBeforeUnload() } render () { @@ -531,6 +629,7 @@ export default class ConfirmTransactionBase extends Component { fromAddress, toName, toAddress, + toEns, toNickname, methodData, valid: propsValid = true, @@ -544,13 +643,16 @@ export default class ConfirmTransactionBase extends Component { contentComponent, onEdit, nonce, + customNonceValue, assetImage, warning, unapprovedTxCount, transactionCategory, recipientAudit, + hideSenderToRecipient, + showAccountInHeader, } = this.props - const { submitting, submitError } = this.state + const { submitting, submitError, submitWarning } = this.state const { name } = methodData const { valid, errorKey } = this.getErrorKey() @@ -559,8 +661,10 @@ export default class ConfirmTransactionBase extends Component { this.handleCancel()} onSubmit={() => this.handleSubmit()} recipientAudit={recipientAudit} + hideSenderToRecipient={hideSenderToRecipient} /> ) } diff --git a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js index a79476425..c37922821 100644 --- a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -8,7 +8,18 @@ import { clearConfirmTransaction, } from '../../ducks/confirm-transaction/confirm-transaction.duck' -import { clearSend, cancelTx, cancelTxs, updateAndApproveTx, showModal, setMetaMetricsSendCount, updateTransaction } from '../../store/actions' +import { + updateCustomNonce, + clearSend, + cancelTx, + cancelTxs, + updateAndApproveTx, + showModal, + setMetaMetricsSendCount, + updateTransaction, + getNextNonce, + tryReverseResolveAddress, +} from '../../store/actions' import { INSUFFICIENT_FUNDS_ERROR_KEY, GAS_LIMIT_TOO_LOW_ERROR_KEY, @@ -18,7 +29,7 @@ import { isBalanceSufficient, calcGasTotal } from '../send/send.utils' import { conversionGreaterThan } from '../../helpers/utils/conversion-util' import { MIN_GAS_LIMIT_DEC } from '../send/send.constants' import { checksumAddress, addressSlicer, valuesFor } from '../../helpers/utils/util' -import { getMetaMaskAccounts, getAdvancedInlineGasShown, preferencesSelector, getIsMainnet, getKnownMethodData } from '../../selectors/selectors' +import { getMetaMaskAccounts, getCustomNonceValue, getUseNonceField, getAdvancedInlineGasShown, preferencesSelector, getIsMainnet, getKnownMethodData } from '../../selectors/selectors' import { transactionFeeSelector } from '../../selectors/confirm-transaction' const casedContractMap = Object.keys(contractMap).reduce((acc, base) => { @@ -28,14 +39,21 @@ const casedContractMap = Object.keys(contractMap).reduce((acc, base) => { } }, {}) +let customNonceValue = '' +const customNonceMerge = txData => customNonceValue ? ({ + ...txData, + customNonceValue, +}) : txData + const mapStateToProps = (state, ownProps) => { - const { toAddress: propsToAddress, match: { params = {} } } = ownProps + const { toAddress: propsToAddress, customTxParamsData, match: { params = {} } } = ownProps const { id: paramsTransactionId } = params const { showFiatInTestnets } = preferencesSelector(state) const isMainnet = getIsMainnet(state) const { confirmTransaction, metamask } = state const { addressAudits, + ensResolutionsByAddress, conversionRate, identities, addressBook, @@ -46,6 +64,7 @@ const mapStateToProps = (state, ownProps) => { network, unapprovedTxs, metaMetricsSendCount, + nextNonce, } = metamask const { tokenData, @@ -80,7 +99,9 @@ const mapStateToProps = (state, ownProps) => { const recipientAudits = addressAudits[txParamsToAddress] || {} const mostRecentAudit = Object.values(recipientAudits).sort((a, b) => a.timestamp > b.timestamp).find(audit => audit) - const addressBookObject = addressBook[checksumAddress(toAddress)] + const checksummedAddress = checksumAddress(toAddress) + const addressBookObject = addressBook[checksummedAddress] + const toEns = ensResolutionsByAddress[checksummedAddress] || '' const toNickname = addressBookObject ? addressBookObject.name : '' const isTxReprice = Boolean(lastGasPrice) const transactionStatus = transaction ? transaction.status : '' @@ -116,11 +137,23 @@ const mapStateToProps = (state, ownProps) => { const methodData = getKnownMethodData(state, data) || {} + let fullTxData = { ...txData, ...transaction } + if (customTxParamsData) { + fullTxData = { + ...fullTxData, + txParams: { + ...fullTxData.txParams, + data: customTxParamsData, + }, + } + } + return { balance, fromAddress, fromName, toAddress, + toEns, toName, toNickname, ethTransactionAmount, @@ -132,7 +165,7 @@ const mapStateToProps = (state, ownProps) => { hexTransactionAmount, hexTransactionFee, hexTransactionTotal, - txData: { ...txData, ...transaction }, + txData: fullTxData, tokenData, methodData, tokenProps, @@ -150,17 +183,27 @@ const mapStateToProps = (state, ownProps) => { gasPrice, }, advancedInlineGasShown: getAdvancedInlineGasShown(state), + useNonceField: getUseNonceField(state), + customNonceValue: getCustomNonceValue(state), insufficientBalance, hideSubtitle: (!isMainnet && !showFiatInTestnets), hideFiatConversion: (!isMainnet && !showFiatInTestnets), metaMetricsSendCount, transactionCategory, recipientAudit: mostRecentAudit, + nextNonce, } } -const mapDispatchToProps = dispatch => { +export const mapDispatchToProps = dispatch => { return { + tryReverseResolveAddress: (address) => { + return dispatch(tryReverseResolveAddress(address)) + }, + updateCustomNonce: value => { + customNonceValue = value + dispatch(updateCustomNonce(value)) + }, clearConfirmTransaction: () => dispatch(clearConfirmTransaction()), clearSend: () => dispatch(clearSend()), showTransactionConfirmedModal: ({ onSubmit }) => { @@ -177,8 +220,9 @@ const mapDispatchToProps = dispatch => { }, cancelTransaction: ({ id }) => dispatch(cancelTx({ id })), cancelAllTransactions: (txList) => dispatch(cancelTxs(txList)), - sendTransaction: txData => dispatch(updateAndApproveTx(txData)), + sendTransaction: txData => dispatch(updateAndApproveTx(customNonceMerge(txData))), setMetaMetricsSendCount: val => dispatch(setMetaMetricsSendCount(val)), + getNextNonce: () => dispatch(getNextNonce()), } } diff --git a/ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.container.test.js b/ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.container.test.js new file mode 100644 index 000000000..a8045d0e0 --- /dev/null +++ b/ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.container.test.js @@ -0,0 +1,20 @@ +import assert from 'assert' +import { mapDispatchToProps } from '../confirm-transaction-base.container' + +describe('Confirm Transaction Base Container', () => { + it('should map dispatch to props correctly', () => { + const props = mapDispatchToProps(() => 'mockDispatch') + + assert.ok(typeof props.updateCustomNonce === 'function') + assert.ok(typeof props.clearConfirmTransaction === 'function') + assert.ok(typeof props.clearSend === 'function') + assert.ok(typeof props.showTransactionConfirmedModal === 'function') + assert.ok(typeof props.showCustomizeGasModal === 'function') + assert.ok(typeof props.updateGasAndCalculate === 'function') + assert.ok(typeof props.showRejectTransactionsConfirmationModal === 'function') + assert.ok(typeof props.cancelTransaction === 'function') + assert.ok(typeof props.cancelAllTransactions === 'function') + assert.ok(typeof props.sendTransaction === 'function') + assert.ok(typeof props.setMetaMetricsSendCount === 'function') + }) +}) diff --git a/ui/app/pages/confirm-transaction/conf-tx.js b/ui/app/pages/confirm-transaction/conf-tx.js index 4f3868bc8..79d7f5e4f 100644 --- a/ui/app/pages/confirm-transaction/conf-tx.js +++ b/ui/app/pages/confirm-transaction/conf-tx.js @@ -9,7 +9,8 @@ const txHelper = require('../../../lib/tx-helper') const log = require('loglevel') const R = require('ramda') -const SignatureRequest = require('../../components/app/signature-request') +const SignatureRequest = require('../../components/app/signature-request').default +const SignatureRequestOriginal = require('../../components/app/signature-request-original').default const Loading = require('../../components/ui/loading-screen') const { DEFAULT_ROUTE } = require('../../helpers/constants/routes') @@ -137,34 +138,45 @@ ConfirmTxScreen.prototype.getTxData = function () { : unconfTxList[index] } +ConfirmTxScreen.prototype.signatureSelect = function (type, version) { + // Temporarily direct only v3 and v4 requests to new code. + if (type === 'eth_signTypedData' && (version === 'V3' || version === 'V4')) { + return SignatureRequest + } + + return SignatureRequestOriginal +} + ConfirmTxScreen.prototype.render = function () { const props = this.props const { currentCurrency, blockGasLimit, + conversionRate, } = props var txData = this.getTxData() || {} - const { msgParams } = txData + const { msgParams, type, msgParams: { version } } = txData log.debug('msgParams detected, rendering pending msg') - return msgParams - ? h(SignatureRequest, { - // Properties - txData: txData, - key: txData.id, - identities: props.identities, - currentCurrency, - blockGasLimit, - // Actions - signMessage: this.signMessage.bind(this, txData), - signPersonalMessage: this.signPersonalMessage.bind(this, txData), - signTypedMessage: this.signTypedMessage.bind(this, txData), - cancelMessage: this.cancelMessage.bind(this, txData), - cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), - cancelTypedMessage: this.cancelTypedMessage.bind(this, txData), - }) - : h(Loading) + return msgParams ? h(this.signatureSelect(type, version), { + // Properties + txData: txData, + key: txData.id, + selectedAddress: props.selectedAddress, + accounts: props.accounts, + identities: props.identities, + conversionRate, + currentCurrency, + blockGasLimit, + // Actions + signMessage: this.signMessage.bind(this, txData), + signPersonalMessage: this.signPersonalMessage.bind(this, txData), + signTypedMessage: this.signTypedMessage.bind(this, txData), + cancelMessage: this.cancelMessage.bind(this, txData), + cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), + cancelTypedMessage: this.cancelTypedMessage.bind(this, txData), + }) : h(Loading) } ConfirmTxScreen.prototype.signMessage = function (msgData, event) { diff --git a/ui/app/pages/confirm-transaction/confirm-transaction.component.js b/ui/app/pages/confirm-transaction/confirm-transaction.component.js index 9e7b01baf..eb87c57ba 100644 --- a/ui/app/pages/confirm-transaction/confirm-transaction.component.js +++ b/ui/app/pages/confirm-transaction/confirm-transaction.component.js @@ -23,6 +23,10 @@ import { } from '../../helpers/constants/routes' export default class ConfirmTransaction extends Component { + static contextTypes = { + metricsEvent: PropTypes.func, + } + static propTypes = { history: PropTypes.object.isRequired, totalUnapprovedCount: PropTypes.number.isRequired, @@ -39,6 +43,9 @@ export default class ConfirmTransaction extends Component { paramsTransactionId: PropTypes.string, getTokenParams: PropTypes.func, isTokenMethodAction: PropTypes.bool, + fullScreenVsPopupTestGroup: PropTypes.string, + trackABTest: PropTypes.bool, + conversionRate: PropTypes.number, } componentDidMount () { @@ -53,6 +60,8 @@ export default class ConfirmTransaction extends Component { paramsTransactionId, getTokenParams, isTokenMethodAction, + fullScreenVsPopupTestGroup, + trackABTest, } = this.props if (!totalUnapprovedCount && !send.to) { @@ -66,7 +75,19 @@ export default class ConfirmTransaction extends Component { getTokenParams(to) } const txId = transactionId || paramsTransactionId - if (txId) this.props.setTransactionToConfirm(txId) + if (txId) { + this.props.setTransactionToConfirm(txId) + } + + if (trackABTest) { + this.context.metricsEvent({ + eventOpts: { + category: 'abtesting', + action: 'fullScreenVsPopup', + name: fullScreenVsPopupTestGroup === 'fullScreen' ? 'fullscreen' : 'original', + }, + }) + } } componentDidUpdate (prevProps) { @@ -100,7 +121,6 @@ export default class ConfirmTransaction extends Component { // Show routes when state.confirmTransaction has been set and when either the ID in the params // isn't specified or is specified and matches the ID in state.confirmTransaction in order to // support URLs of /confirm-transaction or /confirm-transaction/ - return transactionId && (!paramsTransactionId || paramsTransactionId === transactionId) ? ( diff --git a/ui/app/pages/confirm-transaction/confirm-transaction.container.js b/ui/app/pages/confirm-transaction/confirm-transaction.container.js index 6da855df2..7c3986441 100644 --- a/ui/app/pages/confirm-transaction/confirm-transaction.container.js +++ b/ui/app/pages/confirm-transaction/confirm-transaction.container.js @@ -20,7 +20,15 @@ import ConfirmTransaction from './confirm-transaction.component' import { unconfirmedTransactionsListSelector } from '../../selectors/confirm-transaction' const mapStateToProps = (state, ownProps) => { - const { metamask: { send, unapprovedTxs }, confirmTransaction } = state + const { + metamask: { + send, + unapprovedTxs, + abTests: { fullScreenVsPopup }, + conversionRate, + }, + confirmTransaction, + } = state const { match: { params = {} } } = ownProps const { id } = params @@ -31,6 +39,8 @@ const mapStateToProps = (state, ownProps) => { : {} const { id: transactionId, transactionCategory } = transaction + const trackABTest = false + return { totalUnapprovedCount: totalUnconfirmed, send, @@ -42,6 +52,9 @@ const mapStateToProps = (state, ownProps) => { unconfirmedTransactions, transaction, isTokenMethodAction: isTokenMethodAction(transactionCategory), + trackABTest, + fullScreenVsPopupTestGroup: fullScreenVsPopup, + conversionRate, } } 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 3b45e7293..b1323b467 100644 --- a/ui/app/pages/create-account/connect-hardware/connect-screen.js +++ b/ui/app/pages/create-account/connect-hardware/connect-screen.js @@ -104,7 +104,9 @@ class ConnectScreen extends Component { scrollToTutorial = () => { - if (this.referenceNode) this.referenceNode.scrollIntoView({behavior: 'smooth'}) + if (this.referenceNode) { + this.referenceNode.scrollIntoView({behavior: 'smooth'}) + } } renderLearnMore () { @@ -141,7 +143,9 @@ class ConnectScreen extends Component { ] return h('.hw-tutorial', { - ref: node => { this.referenceNode = node }, + ref: node => { + this.referenceNode = node + }, }, steps.map((step) => ( h('div.hw-connect', {}, [ diff --git a/ui/app/pages/create-account/create-account.component.js b/ui/app/pages/create-account/create-account.component.js new file mode 100644 index 000000000..aa05af975 --- /dev/null +++ b/ui/app/pages/create-account/create-account.component.js @@ -0,0 +1,79 @@ +import React, { Component } from 'react' +import { Switch, Route, matchPath } from 'react-router-dom' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import NewAccountCreateForm from './new-account.container' +import NewAccountImportForm from './import-account' +import ConnectHardwareForm from './connect-hardware' +import { + NEW_ACCOUNT_ROUTE, + IMPORT_ACCOUNT_ROUTE, + CONNECT_HARDWARE_ROUTE, +} from '../../helpers/constants/routes' + +export default class CreateAccountPage extends Component { + renderTabs () { + const { history, location: { pathname }} = this.props + const getClassNames = path => classnames('new-account__tabs__tab', { + 'new-account__tabs__selected': matchPath(pathname, { + path, + exact: true, + }), + }) + + return ( +
+
history.push(NEW_ACCOUNT_ROUTE)}>{ + this.context.t('create') + }
+
history.push(IMPORT_ACCOUNT_ROUTE)}>{ + this.context.t('import') + }
+
history.push(CONNECT_HARDWARE_ROUTE)}>{ + this.context.t('connect') + }
+
+ ) + } + + render () { + return ( +
+
+
+ {this.renderTabs()} +
+
+
+ + + + + +
+
+ ) + } +} + +CreateAccountPage.propTypes = { + location: PropTypes.object, + history: PropTypes.object, + t: PropTypes.func, +} + +CreateAccountPage.contextTypes = { + t: PropTypes.func, +} diff --git a/ui/app/pages/create-account/create-account.container.js b/ui/app/pages/create-account/create-account.container.js new file mode 100644 index 000000000..04205cfea --- /dev/null +++ b/ui/app/pages/create-account/create-account.container.js @@ -0,0 +1,20 @@ +import { connect } from 'react-redux' +import actions from '../../store/actions' +import { getCurrentViewContext } from '../../selectors/selectors' +import CreateAccountPage from './create-account.component' + +const mapStateToProps = state => ({ + displayedForm: getCurrentViewContext(state), +}) + +const mapDispatchToProps = dispatch => ({ + displayForm: form => dispatch(actions.setNewAccountForm(form)), + showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)), + showExportPrivateKeyModal: () => { + dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' })) + }, + hideModal: () => dispatch(actions.hideModal()), + setAccountLabel: (address, label) => dispatch(actions.setAccountLabel(address, label)), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(CreateAccountPage) diff --git a/ui/app/pages/create-account/index.js b/ui/app/pages/create-account/index.js index ce84db028..165c3b397 100644 --- a/ui/app/pages/create-account/index.js +++ b/ui/app/pages/create-account/index.js @@ -1,113 +1 @@ -const Component = require('react').Component -const { Switch, Route, matchPath } = require('react-router-dom') -const PropTypes = require('prop-types') -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('../../store/actions') -const { getCurrentViewContext } = require('../../selectors/selectors') -const classnames = require('classnames') -const NewAccountCreateForm = require('./new-account') -const NewAccountImportForm = require('./import-account') -const ConnectHardwareForm = require('./connect-hardware') -const { - NEW_ACCOUNT_ROUTE, - IMPORT_ACCOUNT_ROUTE, - CONNECT_HARDWARE_ROUTE, -} = require('../../helpers/constants/routes') - -class CreateAccountPage extends Component { - renderTabs () { - const { history, location } = this.props - - return h('div.new-account__tabs', [ - h('div.new-account__tabs__tab', { - className: classnames('new-account__tabs__tab', { - 'new-account__tabs__selected': matchPath(location.pathname, { - path: NEW_ACCOUNT_ROUTE, exact: true, - }), - }), - onClick: () => history.push(NEW_ACCOUNT_ROUTE), - }, [ - this.context.t('create'), - ]), - - h('div.new-account__tabs__tab', { - className: classnames('new-account__tabs__tab', { - 'new-account__tabs__selected': matchPath(location.pathname, { - path: IMPORT_ACCOUNT_ROUTE, exact: true, - }), - }), - onClick: () => history.push(IMPORT_ACCOUNT_ROUTE), - }, [ - this.context.t('import'), - ]), - h( - 'div.new-account__tabs__tab', - { - className: classnames('new-account__tabs__tab', { - 'new-account__tabs__selected': matchPath(location.pathname, { - path: CONNECT_HARDWARE_ROUTE, - exact: true, - }), - }), - onClick: () => history.push(CONNECT_HARDWARE_ROUTE), - }, - this.context.t('connect') - ), - ]) - } - - render () { - return h('div.new-account', {}, [ - h('div.new-account__header', [ - h('div.new-account__title', this.context.t('newAccount')), - this.renderTabs(), - ]), - h('div.new-account__form', [ - h(Switch, [ - h(Route, { - exact: true, - path: NEW_ACCOUNT_ROUTE, - component: NewAccountCreateForm, - }), - h(Route, { - exact: true, - path: IMPORT_ACCOUNT_ROUTE, - component: NewAccountImportForm, - }), - h(Route, { - exact: true, - path: CONNECT_HARDWARE_ROUTE, - component: ConnectHardwareForm, - }), - ]), - ]), - ]) - } -} - -CreateAccountPage.propTypes = { - location: PropTypes.object, - history: PropTypes.object, - t: PropTypes.func, -} - -CreateAccountPage.contextTypes = { - t: PropTypes.func, -} - -const mapStateToProps = state => ({ - displayedForm: getCurrentViewContext(state), -}) - -const mapDispatchToProps = dispatch => ({ - displayForm: form => dispatch(actions.setNewAccountForm(form)), - showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)), - showExportPrivateKeyModal: () => { - dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' })) - }, - hideModal: () => dispatch(actions.hideModal()), - setAccountLabel: (address, label) => dispatch(actions.setAccountLabel(address, label)), -}) - -module.exports = connect(mapStateToProps, mapDispatchToProps)(CreateAccountPage) +export { default } from './create-account.container' diff --git a/ui/app/pages/create-account/new-account.component.js b/ui/app/pages/create-account/new-account.component.js new file mode 100644 index 000000000..6dc6419b5 --- /dev/null +++ b/ui/app/pages/create-account/new-account.component.js @@ -0,0 +1,91 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { DEFAULT_ROUTE } from '../../helpers/constants/routes' +import Button from '../../components/ui/button' + +export default class NewAccountCreateForm extends Component { + constructor (props, context) { + super(props) + const { newAccountNumber = 0 } = props + + this.state = { + newAccountName: '', + defaultAccountName: context.t('newAccountNumberName', [newAccountNumber]), + } + } + + render () { + const { newAccountName, defaultAccountName } = this.state + const { history, createAccount } = this.props + const createClick = _ => { + createAccount(newAccountName || defaultAccountName) + .then(() => { + this.context.metricsEvent({ + eventOpts: { + category: 'Accounts', + action: 'Add New Account', + name: 'Added New Account', + }, + }) + history.push(DEFAULT_ROUTE) + }) + .catch((e) => { + this.context.metricsEvent({ + eventOpts: { + category: 'Accounts', + action: 'Add New Account', + name: 'Error', + }, + customVariables: { + errorMessage: e.message, + }, + }) + }) + } + + return ( +
+
+ {this.context.t('accountName')} +
+
+ this.setState({ newAccountName: event.target.value })} + /> +
+
+ + +
+
+ ) + } +} + +NewAccountCreateForm.propTypes = { + hideModal: PropTypes.func, + showImportPage: PropTypes.func, + showConnectPage: PropTypes.func, + createAccount: PropTypes.func, + numberOfExistingAccounts: PropTypes.number, + newAccountNumber: PropTypes.number, + history: PropTypes.object, + t: PropTypes.func, +} + +NewAccountCreateForm.contextTypes = { + t: PropTypes.func, + metricsEvent: PropTypes.func, +} diff --git a/ui/app/pages/create-account/new-account.container.js b/ui/app/pages/create-account/new-account.container.js new file mode 100644 index 000000000..9f3af5003 --- /dev/null +++ b/ui/app/pages/create-account/new-account.container.js @@ -0,0 +1,35 @@ +import { connect } from 'react-redux' +import actions from '../../store/actions' +import NewAccountCreateForm from './new-account.component' + +const mapStateToProps = state => { + const { metamask: { network, selectedAddress, identities = {} } } = state + const numberOfExistingAccounts = Object.keys(identities).length + const newAccountNumber = numberOfExistingAccounts + 1 + + return { + network, + address: selectedAddress, + numberOfExistingAccounts, + newAccountNumber, + } +} + +const mapDispatchToProps = dispatch => { + return { + toCoinbase: address => dispatch(actions.buyEth({ network: '1', address, amount: 0 })), + hideModal: () => dispatch(actions.hideModal()), + createAccount: newAccountName => { + return dispatch(actions.addNewAccount()) + .then(newAccountAddress => { + if (newAccountName) { + dispatch(actions.setAccountLabel(newAccountAddress, newAccountName)) + } + }) + }, + showImportPage: () => dispatch(actions.showImportPage()), + showConnectPage: () => dispatch(actions.showConnectPage()), + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(NewAccountCreateForm) diff --git a/ui/app/pages/create-account/new-account.js b/ui/app/pages/create-account/new-account.js deleted file mode 100644 index d19e6bc38..000000000 --- a/ui/app/pages/create-account/new-account.js +++ /dev/null @@ -1,130 +0,0 @@ -const { Component } = require('react') -const PropTypes = require('prop-types') -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('../../store/actions') -const { DEFAULT_ROUTE } = require('../../helpers/constants/routes') -import Button from '../../components/ui/button' - -class NewAccountCreateForm extends Component { - constructor (props, context) { - super(props) - - const { numberOfExistingAccounts = 0 } = props - const newAccountNumber = numberOfExistingAccounts + 1 - - this.state = { - newAccountName: '', - defaultAccountName: context.t('newAccountNumberName', [newAccountNumber]), - } - } - - render () { - const { newAccountName, defaultAccountName } = this.state - const { history, createAccount } = this.props - - return h('div.new-account-create-form', [ - - h('div.new-account-create-form__input-label', {}, [ - this.context.t('accountName'), - ]), - - h('div.new-account-create-form__input-wrapper', {}, [ - h('input.new-account-create-form__input', { - value: newAccountName, - placeholder: defaultAccountName, - onChange: event => this.setState({ newAccountName: event.target.value }), - }, []), - ]), - - h('div.new-account-create-form__buttons', {}, [ - - h(Button, { - type: 'default', - large: true, - className: 'new-account-create-form__button', - onClick: () => history.push(DEFAULT_ROUTE), - }, [this.context.t('cancel')]), - - h(Button, { - type: 'secondary', - large: true, - className: 'new-account-create-form__button', - onClick: () => { - createAccount(newAccountName || defaultAccountName) - .then(() => { - this.context.metricsEvent({ - eventOpts: { - category: 'Accounts', - action: 'Add New Account', - name: 'Added New Account', - }, - }) - history.push(DEFAULT_ROUTE) - }) - .catch((e) => { - this.context.metricsEvent({ - eventOpts: { - category: 'Accounts', - action: 'Add New Account', - name: 'Error', - }, - customVariables: { - errorMessage: e.message, - }, - }) - }) - }, - }, [this.context.t('create')]), - - ]), - - ]) - } -} - -NewAccountCreateForm.propTypes = { - hideModal: PropTypes.func, - showImportPage: PropTypes.func, - showConnectPage: PropTypes.func, - createAccount: PropTypes.func, - numberOfExistingAccounts: PropTypes.number, - history: PropTypes.object, - t: PropTypes.func, -} - -const mapStateToProps = state => { - const { metamask: { network, selectedAddress, identities = {} } } = state - const numberOfExistingAccounts = Object.keys(identities).length - - return { - network, - address: selectedAddress, - numberOfExistingAccounts, - } -} - -const mapDispatchToProps = dispatch => { - return { - toCoinbase: address => dispatch(actions.buyEth({ network: '1', address, amount: 0 })), - hideModal: () => dispatch(actions.hideModal()), - createAccount: newAccountName => { - return dispatch(actions.addNewAccount()) - .then(newAccountAddress => { - if (newAccountName) { - dispatch(actions.setAccountLabel(newAccountAddress, newAccountName)) - } - }) - }, - showImportPage: () => dispatch(actions.showImportPage()), - showConnectPage: () => dispatch(actions.showConnectPage()), - } -} - -NewAccountCreateForm.contextTypes = { - t: PropTypes.func, - metricsEvent: PropTypes.func, -} - -module.exports = connect(mapStateToProps, mapDispatchToProps)(NewAccountCreateForm) - diff --git a/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js b/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js index 605b6ed92..e1c0b21ed 100644 --- a/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js +++ b/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js @@ -50,7 +50,7 @@ export default class ImportWithSeedPhrase extends PureComponent { } componentWillMount () { - window.onbeforeunload = () => this.context.metricsEvent({ + this._onBeforeUnload = () => this.context.metricsEvent({ eventOpts: { category: 'Onboarding', action: 'Import Seed Phrase', @@ -61,6 +61,11 @@ export default class ImportWithSeedPhrase extends PureComponent { errorMessage: this.state.seedPhraseError, }, }) + window.addEventListener('beforeunload', this._onBeforeUnload) + } + + componentWillUnmount () { + window.removeEventListener('beforeunload', this._onBeforeUnload) } handleSeedPhraseChange (seedPhrase) { diff --git a/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.component.js b/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.component.js index 8cbf4d69f..f603c827b 100644 --- a/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.component.js +++ b/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.component.js @@ -1,8 +1,10 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import Button from '../../../components/ui/button' +import Snackbar from '../../../components/ui/snackbar' import MetaFoxLogo from '../../../components/ui/metafox-logo' import { DEFAULT_ROUTE } from '../../../helpers/constants/routes' +import { returnToOnboardingInitiator } from '../onboarding-initiator-util' export default class EndOfFlowScreen extends PureComponent { static contextTypes = { @@ -14,11 +16,33 @@ export default class EndOfFlowScreen extends PureComponent { history: PropTypes.object, completeOnboarding: PropTypes.func, completionMetaMetricsName: PropTypes.string, + onboardingInitiator: PropTypes.exact({ + location: PropTypes.string, + tabId: PropTypes.number, + }), + } + + onComplete = async () => { + const { history, completeOnboarding, completionMetaMetricsName, onboardingInitiator } = this.props + + await completeOnboarding() + this.context.metricsEvent({ + eventOpts: { + category: 'Onboarding', + action: 'Onboarding Complete', + name: completionMetaMetricsName, + }, + }) + + if (onboardingInitiator) { + await returnToOnboardingInitiator(onboardingInitiator) + } + history.push(DEFAULT_ROUTE) } render () { const { t } = this.context - const { history, completeOnboarding, completionMetaMetricsName } = this.props + const { onboardingInitiator } = this.props return (
@@ -62,20 +86,17 @@ export default class EndOfFlowScreen extends PureComponent { + { + onboardingInitiator ? + : + null + }
) } diff --git a/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.container.js b/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.container.js index 38313806c..2bb9ef15a 100644 --- a/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.container.js +++ b/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.container.js @@ -1,21 +1,22 @@ import { connect } from 'react-redux' import EndOfFlow from './end-of-flow.component' import { setCompletedOnboarding } from '../../../store/actions' +import { getOnboardingInitiator } from '../first-time-flow.selectors' const firstTimeFlowTypeNameMap = { create: 'New Wallet Created', 'import': 'New Wallet Imported', } -const mapStateToProps = ({ metamask }) => { - const { firstTimeFlowType } = metamask +const mapStateToProps = (state) => { + const { metamask: { firstTimeFlowType } } = state return { completionMetaMetricsName: firstTimeFlowTypeNameMap[firstTimeFlowType], + onboardingInitiator: getOnboardingInitiator(state), } } - const mapDispatchToProps = dispatch => { return { completeOnboarding: () => dispatch(setCompletedOnboarding()), diff --git a/ui/app/pages/first-time-flow/end-of-flow/index.scss b/ui/app/pages/first-time-flow/end-of-flow/index.scss index d7eb4513b..de603fce4 100644 --- a/ui/app/pages/first-time-flow/end-of-flow/index.scss +++ b/ui/app/pages/first-time-flow/end-of-flow/index.scss @@ -50,4 +50,4 @@ font-size: 80px; margin-top: 70px; } -} \ No newline at end of file +} diff --git a/ui/app/pages/first-time-flow/first-time-flow.selectors.js b/ui/app/pages/first-time-flow/first-time-flow.selectors.js index e6cd5a84a..74cad5e12 100644 --- a/ui/app/pages/first-time-flow/first-time-flow.selectors.js +++ b/ui/app/pages/first-time-flow/first-time-flow.selectors.js @@ -4,12 +4,6 @@ import { DEFAULT_ROUTE, } from '../../helpers/constants/routes' -const selectors = { - getFirstTimeFlowTypeRoute, -} - -module.exports = selectors - function getFirstTimeFlowTypeRoute (state) { const { firstTimeFlowType } = state.metamask @@ -24,3 +18,25 @@ function getFirstTimeFlowTypeRoute (state) { return nextRoute } + +const getOnboardingInitiator = (state) => { + const { onboardingTabs } = state.metamask + + if (!onboardingTabs || Object.keys(onboardingTabs).length !== 1) { + return null + } + + const location = Object.keys(onboardingTabs)[0] + const tabId = onboardingTabs[location] + return { + location, + tabId, + } +} + +const selectors = { + getFirstTimeFlowTypeRoute, + getOnboardingInitiator, +} + +module.exports = selectors diff --git a/ui/app/pages/first-time-flow/index.scss b/ui/app/pages/first-time-flow/index.scss index c674551f4..58718c581 100644 --- a/ui/app/pages/first-time-flow/index.scss +++ b/ui/app/pages/first-time-flow/index.scss @@ -120,7 +120,7 @@ &__button { margin: 35px 0 14px; - width: 140px; + width: 170px; height: 44px; } 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 bb187d634..ad1ffbf42 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 @@ -104,7 +104,7 @@ export default class MetaMetricsOptIn extends Component { }) }) }} - cancelText={'No Thanks'} + cancelText="No Thanks" hideCancel={false} onSubmit={() => { setParticipateInMetaMetrics(true) @@ -137,8 +137,8 @@ export default class MetaMetricsOptIn extends Component { }) }) }} - submitText={'I agree'} - submitButtonType={'primary'} + submitText="I agree" + submitButtonType="primary" disabled={false} />
diff --git a/ui/app/pages/first-time-flow/onboarding-initiator-util.js b/ui/app/pages/first-time-flow/onboarding-initiator-util.js new file mode 100644 index 000000000..dd70085f6 --- /dev/null +++ b/ui/app/pages/first-time-flow/onboarding-initiator-util.js @@ -0,0 +1,48 @@ +import extension from 'extensionizer' +import log from 'loglevel' + +const returnToOnboardingInitiatorTab = async (onboardingInitiator) => { + const tab = await (new Promise((resolve) => { + extension.tabs.update(onboardingInitiator.tabId, { active: true }, (tab) => { + if (tab) { + resolve(tab) + } else { + // silence console message about unchecked error + if (extension.runtime.lastError) { + log.debug(extension.runtime.lastError) + } + resolve() + } + }) + })) + + if (!tab) { + // this case can happen if the tab was closed since being checked with `extension.tabs.get` + log.warn(`Setting current tab to onboarding initator has failed; falling back to redirect`) + window.location.assign(onboardingInitiator.location) + } else { + window.close() + } +} + +export const returnToOnboardingInitiator = async (onboardingInitiator) => { + const tab = await (new Promise((resolve) => { + extension.tabs.get(onboardingInitiator.tabId, (tab) => { + if (tab) { + resolve(tab) + } else { + // silence console message about unchecked error + if (extension.runtime.lastError) { + log.debug(extension.runtime.lastError) + } + resolve() + } + }) + })) + + if (tab) { + await returnToOnboardingInitiatorTab(onboardingInitiator) + } else { + window.location.assign(onboardingInitiator.location) + } +} 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 index cdb881921..7e0652f8f 100644 --- 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 @@ -25,7 +25,6 @@ class DraggableSeed extends Component { static defaultProps = { className: '', - onClick () {}, } componentWillReceiveProps (nextProps) { @@ -52,7 +51,7 @@ class DraggableSeed extends Component { return connectDropTarget(connectDragSource(
{ - event.preventDefault() + handleNext = () => { const { isShowingSeedPhrase } = this.state const { history } = this.props @@ -47,9 +52,8 @@ export default class RevealSeedPhrase extends PureComponent { history.push(INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE) } - handleSkip = event => { - event.preventDefault() - const { history, setSeedPhraseBackedUp, setCompletedOnboarding } = this.props + handleSkip = async () => { + const { history, setSeedPhraseBackedUp, setCompletedOnboarding, onboardingInitiator } = this.props this.context.metricsEvent({ eventOpts: { @@ -59,10 +63,12 @@ export default class RevealSeedPhrase extends PureComponent { }, }) - Promise.all([setCompletedOnboarding(), setSeedPhraseBackedUp(false)]) - .then(() => { - history.push(DEFAULT_ROUTE) - }) + await Promise.all([setCompletedOnboarding(), setSeedPhraseBackedUp(false)]) + + if (onboardingInitiator) { + await returnToOnboardingInitiator(onboardingInitiator) + } + history.push(DEFAULT_ROUTE) } renderSecretWordsContainer () { @@ -73,7 +79,7 @@ export default class RevealSeedPhrase extends PureComponent { return (
{ seedPhrase } @@ -111,6 +117,7 @@ export default class RevealSeedPhrase extends PureComponent { render () { const { t } = this.context const { isShowingSeedPhrase } = this.state + const { onboardingInitiator } = this.props return (
@@ -166,6 +173,13 @@ export default class RevealSeedPhrase extends PureComponent { { t('next') }
+ { + onboardingInitiator ? + : + null + }
) } diff --git a/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.container.js b/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.container.js index 7ada36afc..11a26fb6d 100644 --- a/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.container.js +++ b/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.container.js @@ -4,6 +4,13 @@ import { setCompletedOnboarding, setSeedPhraseBackedUp, } from '../../../../store/actions' +import { getOnboardingInitiator } from '../../first-time-flow.selectors' + +const mapStateToProps = (state) => { + return { + onboardingInitiator: getOnboardingInitiator(state), + } +} const mapDispatchToProps = dispatch => { return { @@ -12,4 +19,4 @@ const mapDispatchToProps = dispatch => { } } -export default connect(null, mapDispatchToProps)(RevealSeedPhrase) +export default connect(mapStateToProps, mapDispatchToProps)(RevealSeedPhrase) diff --git a/ui/app/pages/home/home.component.js b/ui/app/pages/home/home.component.js index 53f0ea42f..80367f9eb 100644 --- a/ui/app/pages/home/home.component.js +++ b/ui/app/pages/home/home.component.js @@ -4,6 +4,7 @@ import Media from 'react-media' import { Redirect } from 'react-router-dom' import { formatDate } from '../../helpers/utils/util' import HomeNotification from '../../components/app/home-notification' +import DaiMigrationNotification from '../../components/app/dai-migration-component' import MultipleNotifications from '../../components/app/multiple-notifications' import WalletView from '../../components/app/wallet-view' import TransactionView from '../../components/app/transaction-view' @@ -23,7 +24,7 @@ export default class Home extends PureComponent { } static defaultProps = { - activeTab: {}, + hasDaiV1Token: false, } static propTypes = { @@ -42,12 +43,12 @@ export default class Home extends PureComponent { threeBoxSynced: PropTypes.bool, setupThreeBox: PropTypes.func, turnThreeBoxSyncingOn: PropTypes.func, - restoredFromThreeBox: PropTypes.bool, + showRestorePrompt: PropTypes.bool, selectedAddress: PropTypes.string, restoreFromThreeBox: PropTypes.func, - setRestoredFromThreeBoxToFalse: PropTypes.func, - threeBoxLastUpdated: PropTypes.string, - threeBoxFeatureFlagIsTrue: PropTypes.bool, + setShowRestorePromptToFalse: PropTypes.func, + threeBoxLastUpdated: PropTypes.number, + hasDaiV1Token: PropTypes.bool, permissionsRequests: PropTypes.array, removePlugin: PropTypes.func, clearPlugins: PropTypes.func, @@ -87,10 +88,10 @@ export default class Home extends PureComponent { const { threeBoxSynced, setupThreeBox, - restoredFromThreeBox, + showRestorePrompt, threeBoxLastUpdated, } = this.props - if (threeBoxSynced && restoredFromThreeBox === null && threeBoxLastUpdated === null) { + if (threeBoxSynced && showRestorePrompt && threeBoxLastUpdated === null) { setupThreeBox() } } @@ -100,15 +101,15 @@ export default class Home extends PureComponent { const { forgottenPassword, history, + hasDaiV1Token, shouldShowSeedPhraseReminder, isPopup, selectedAddress, restoreFromThreeBox, turnThreeBoxSyncingOn, - setRestoredFromThreeBoxToFalse, - restoredFromThreeBox, + setShowRestorePromptToFalse, + showRestorePrompt, threeBoxLastUpdated, - threeBoxFeatureFlagIsTrue, permissionsRequests, hasPermissionsData, hasPlugins, @@ -120,7 +121,7 @@ export default class Home extends PureComponent { if (permissionsRequests && permissionsRequests.length > 0) { return ( - + ) } @@ -134,12 +135,10 @@ export default class Home extends PureComponent { { !history.location.pathname.match(/^\/confirm-transaction/) ? ( - + { + shouldShowSeedPhraseReminder + ? { @@ -151,12 +150,13 @@ export default class Home extends PureComponent { }} infoText={t('backupApprovalInfo')} key="home-backupApprovalNotice" - />, - }, - { - shouldBeRendered: threeBoxFeatureFlagIsTrue && threeBoxLastUpdated && restoredFromThreeBox === null, - component: + : null + } + { + threeBoxLastUpdated && showRestorePrompt + ? { - setRestoredFromThreeBoxToFalse() + setShowRestorePromptToFalse() }} key="home-privacyModeDefault" - />, - }, - ]}/> + /> + : null + } + { + hasDaiV1Token + ? + : null + } +