diff --git a/.circleci/scripts/chrome-install.sh b/.circleci/scripts/chrome-install.sh index 491e8c3ed..c301f9056 100755 --- a/.circleci/scripts/chrome-install.sh +++ b/.circleci/scripts/chrome-install.sh @@ -6,10 +6,17 @@ set -o pipefail CHROME_VERSION='79.0.3945.117-1' CHROME_BINARY="google-chrome-stable_${CHROME_VERSION}_amd64.deb" -CHROME_BINARY_URL="http://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/${CHROME_BINARY}" +CHROME_BINARY_URL="http://mirror.cs.uchicago.edu/google-chrome/pool/main/g/google-chrome-stable/${CHROME_BINARY}" + +CHROME_BINARY_SHA512SUM='2d4f76202219a40e560477d79023fa4a847187a086278924a9d916dcd5fbefafdcf7dfd8879fae907b8276b244e71a3b8a1b00a88dee87b18738ce31134a6713' wget -O "${CHROME_BINARY}" -t 5 "${CHROME_BINARY_URL}" +if [[ $(shasum -a 512 "${CHROME_BINARY}" | cut '--delimiter= ' -f1) != "${CHROME_BINARY_SHA512SUM}" ]] +then + exit 1 +fi + (sudo dpkg -i "${CHROME_BINARY}" || sudo apt-get -fy install) rm -rf "${CHROME_BINARY}" diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index a5b579a96..95077129c 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,7 +1,7 @@ blank_issues_enabled: false contact_links: - name: Request a new feature - url: https://metamask.zendesk.com/hc/en-us/community/topics/360000682552-Feature-Requests + url: https://community.metamask.io/c/feature-requests-ideas/ about: Request new features and vote on the ones that are important to you - name: Get support or ask a question url: https://metamask.zendesk.com/hc/en-us/requests/new diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index f358e58a9..b97bb8247 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -9,6 +9,9 @@ jobs: CLABot: if: github.event_name == 'pull_request_target' || contains(github.event.comment.html_url, '/pull/') runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: write steps: - name: "CLA Signature Bot" uses: MetaMask/cla-signature-bot@v3.0.2 diff --git a/.storybook/initial-states/approval-screens/token-approval.js b/.storybook/initial-states/approval-screens/token-approval.js new file mode 100644 index 000000000..f03990ea5 --- /dev/null +++ b/.storybook/initial-states/approval-screens/token-approval.js @@ -0,0 +1,56 @@ +export const currentNetworkTxListSample = { + "id": 7900715443136469, + "time": 1621395091737, + "status": "unapproved", + "metamaskNetworkId": "1337", + "chainId": "0x539", + "loadingDefaults": false, + "txParams": { + "from": "0x90f79bf6eb2c4f870365e785982e1f101e93b906", + "to": "0x057ef64e23666f000b34ae31332854acbd1c8544", + "value": "0x0", + "data": "0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170", + "gas": "0xea60", + "gasPrice": "0x4a817c800" + }, + "origin": "https://metamask.github.io", + "type": "approve", + "history": [ + { + "id": 7900715443136469, + "time": 1621395091737, + "status": "unapproved", + "metamaskNetworkId": "1337", + "chainId": "0x539", + "loadingDefaults": true, + "txParams": { + "from": "0x90f79bf6eb2c4f870365e785982e1f101e93b906", + "to": "0x057ef64e23666f000b34ae31332854acbd1c8544", + "value": "0x0", + "data": "0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170", + "gas": "0xea60", + "gasPrice": "0x4a817c800" + }, + "origin": "https://metamask.github.io", + "type": "approve" + }, + [ + { + "op": "replace", + "path": "/loadingDefaults", + "value": false, + "note": "Added new unapproved transaction.", + "timestamp": 1621395091742 + } + ] + ] +} + +export const domainMetadata = { + "https://metamask.github.io": { + "name": "E2E Test Dapp", + "icon": "https://metamask.github.io/test-dapp/metamask-fox.svg", + "lastUpdated": 1620723443380, + "host": "metamask.github.io" + } +} \ No newline at end of file diff --git a/.storybook/metametrics.js b/.storybook/metametrics.js new file mode 100644 index 000000000..387b0d467 --- /dev/null +++ b/.storybook/metametrics.js @@ -0,0 +1,24 @@ +import React from 'react'; +import { + MetaMetricsProvider, + LegacyMetaMetricsProvider, +} from '../ui/contexts/metametrics'; +import { + MetaMetricsProvider as NewMetaMetricsProvider, + LegacyMetaMetricsProvider as NewLegacyMetaMetricsProvider, +} from '../ui/contexts/metametrics.new'; + +const MetaMetricsProviderStorybook = (props) => + ( + + + + + {props.children} + + + + + ); + +export default MetaMetricsProviderStorybook \ No newline at end of file diff --git a/.storybook/preview.js b/.storybook/preview.js index 24d195c4e..525401408 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,6 +1,6 @@ import React, { useEffect } from 'react'; import { addDecorator, addParameters } from '@storybook/react'; -import { useGlobals } from '@storybook/api'; +import { action } from '@storybook/addon-actions'; import { withKnobs } from '@storybook/addon-knobs'; import { Provider } from 'react-redux'; import configureStore from '../ui/store/store'; @@ -8,7 +8,11 @@ import '../ui/css/index.scss'; import localeList from '../app/_locales/index.json'; import * as allLocales from './locales'; import { I18nProvider, LegacyI18nProvider } from './i18n'; +import MetaMetricsProviderStorybook from './metametrics' import testData from './test-data.js'; +import { Router } from "react-router-dom"; +import { createBrowserHistory } from "history"; +import { _setBackgroundConnection } from '../ui/store/actions' addParameters({ backgrounds: { @@ -41,22 +45,36 @@ const styles = { alignItems: 'center', }; -const store = configureStore(testData); +export const store = configureStore(testData); +const history = createBrowserHistory(); +const proxiedBackground = new Proxy({}, { + get(_, method) { + return function() { + action(`Background call: ${method}`)() + return new Promise(() => {}) + } + } + }) +_setBackgroundConnection(proxiedBackground) const metamaskDecorator = (story, context) => { const currentLocale = context.globals.locale; const current = allLocales[currentLocale]; return ( - - -
{story()}
-
-
+ + + + +
{story()}
+
+
+
+
); }; diff --git a/.storybook/test-data.js b/.storybook/test-data.js index ef9e15e64..32c24690f 100644 --- a/.storybook/test-data.js +++ b/.storybook/test-data.js @@ -1,217 +1,782 @@ import { TRANSACTION_STATUSES } from '../shared/constants/transaction'; const state = { - metamask: { - isInitialized: true, - isUnlocked: true, - featureFlags: { sendHexData: true }, - identities: { - '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': { - address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', - name: 'Send Account 1', - }, - '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb': { - address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', - name: 'Send Account 2', - }, - '0x2f8d4a878cfa04a6e60d46362f5644deab66572d': { - address: '0x2f8d4a878cfa04a6e60d46362f5644deab66572d', - name: 'Send Account 3', - }, - '0xd85a4b6a394794842887b8284293d69163007bbb': { - address: '0xd85a4b6a394794842887b8284293d69163007bbb', - name: 'Send Account 4', - }, - }, - cachedBalances: {}, - currentBlockGasLimit: '0x4c1878', - currentCurrency: 'USD', - conversionRate: 1200.88200327, - conversionDate: 1489013762, - nativeCurrency: 'ETH', - frequentRpcList: [], - network: '3', - provider: { - type: 'ropsten', - chainId: '0x3', - }, - accounts: { - '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': { - code: '0x', - balance: '0x47c9d71831c76efe', - nonce: '0x1b', - address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', - }, - '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb': { - code: '0x', - balance: '0x37452b1315889f80', - nonce: '0xa', - address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', - }, - '0x2f8d4a878cfa04a6e60d46362f5644deab66572d': { - code: '0x', - balance: '0x30c9d71831c76efe', - nonce: '0x1c', - address: '0x2f8d4a878cfa04a6e60d46362f5644deab66572d', - }, - '0xd85a4b6a394794842887b8284293d69163007bbb': { - code: '0x', - balance: '0x0', - nonce: '0x0', - address: '0xd85a4b6a394794842887b8284293d69163007bbb', - }, - }, - addressBook: { - '0x3': { - '0x06195827297c7a80a443b6894d3bdb8824b43896': { - address: '0x06195827297c7a80a443b6894d3bdb8824b43896', - name: 'Address Book Account 1', - chainId: '0x3', - }, - }, - }, - tokens: [ - { - address: '0x1a195821297c7a80a433b6894d3bdb8824b43896', - decimals: 18, - symbol: 'ABC', + "invalidCustomNetwork": { + "state": "CLOSED", + "networkName": "" + }, + "unconnectedAccount": { + "state": "CLOSED" + }, + "activeTab": {}, + "metamask": { + "isInitialized": true, + "isUnlocked": true, + "isAccountMenuOpen": false, + "rpcUrl": "https://rawtestrpc.metamask.io/", + "identities": { + "0x983211ce699ea5ab57cc528086154b6db1ad8e55": { + "name": "Account 1", + "address": "0x983211ce699ea5ab57cc528086154b6db1ad8e55" }, - { - address: '0x8d6b81208414189a58339873ab429b6c47ab92d3', - decimals: 4, - symbol: 'DEF', + "0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e": { + "name": "Account 2", + "address": "0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e" }, + "0x9d0ba4ddac06032527b140912ec808ab9451b788": { + "name": "Account 3", + "address": "0x9d0ba4ddac06032527b140912ec808ab9451b788" + } + }, + "unapprovedTxs": { + "7786962153682822": { + "id": 7786962153682822, + "time": 1620710815484, + "status": "unapproved", + "metamaskNetworkId": "3", + "chainId": "0x3", + "loadingDefaults": false, + "txParams": { + "from": "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4", + "to": "0xad6d458402f60fd3bd25163575031acdce07538d", + "value": "0x0", + "data": "0xa9059cbb000000000000000000000000b19ac54efa18cc3a14a5b821bfec73d284bf0c5e0000000000000000000000000000000000000000000000003782dace9d900000", + "gas": "0xcb28", + "gasPrice": "0x77359400" + }, + "type": "standard", + "origin": "metamask", + "transactionCategory": "transfer", + "history": [ + { + "id": 7786962153682822, + "time": 1620710815484, + "status": "unapproved", + "metamaskNetworkId": "3", + "chainId": "0x3", + "loadingDefaults": true, + "txParams": { + "from": "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4", + "to": "0xad6d458402f60fd3bd25163575031acdce07538d", + "value": "0x0", + "data": "0xa9059cbb000000000000000000000000b19ac54efa18cc3a14a5b821bfec73d284bf0c5e0000000000000000000000000000000000000000000000003782dace9d900000", + "gas": "0xcb28", + "gasPrice": "0x77359400" + }, + "type": "standard", + "origin": "metamask", + "transactionCategory": "transfer" + }, + [ + { + "op": "replace", + "path": "/loadingDefaults", + "value": false, + "note": "Added new unapproved transaction.", + "timestamp": 1620710815497 + } + ] + ] + } + }, + "frequentRpcList": [], + "addressBook": { + "undefined": { + "0": { + "address": "0x39a4e4Af7cCB654dB9500F258c64781c8FbD39F0", + "name": "", + "isEns": false + } + } + }, + "contractExchangeRates": { + "0xad6d458402f60fd3bd25163575031acdce07538d": 0 + }, + "tokens": [ { - address: '0xa42084c8d1d9a2198631988579bb36b48433a72b', - decimals: 18, - symbol: 'GHI', + "address": "0xad6d458402f60fd3bd25163575031acdce07538d", + "symbol": "DAI", + "decimals": 18 + } + ], + "pendingTokens": {}, + "customNonceValue": "", + "send": { + "gasLimit": "0xcb28", + "gasPrice": null, + "gasTotal": null, + "tokenBalance": "8.7a73149c048545a3fe58", + "from": "", + "to": "0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e", + "amount": "3782dace9d900000", + "memo": "", + "errors": {}, + "maxModeOn": false, + "editingTransactionId": null, + "toNickname": "Account 2", + "ensResolution": null, + "ensResolutionError": "", + "token": { + "address": "0xad6d458402f60fd3bd25163575031acdce07538d", + "symbol": "DAI", + "decimals": 18 + } + }, + "useBlockie": false, + "featureFlags": {}, + "welcomeScreenSeen": false, + "currentLocale": "en", + "preferences": { + "useNativeCurrencyAsPrimaryCurrency": true + }, + "firstTimeFlowType": "create", + "completedOnboarding": true, + "knownMethodData": { + "0x60806040": { + "name": "Approve Tokens" + }, + "0x095ea7b3": { + "name": "Approve Tokens" + } + }, + "participateInMetaMetrics": true, + "metaMetricsSendCount": 2, + "nextNonce": 71, + "connectedStatusPopoverHasBeenShown": true, + "swapsWelcomeMessageHasBeenShown": true, + "defaultHomeActiveTabName": "Assets", + "provider": { + "type": "ropsten", + "ticker": "ETH", + "nickname": "", + "rpcUrl": "", + "chainId": "0x3" + }, + "previousProviderStore": { + "type": "ropsten", + "ticker": "ETH", + "nickname": "", + "rpcUrl": "", + "chainId": "0x3" + }, + "network": "3", + "accounts": { + "0x983211ce699ea5ab57cc528086154b6db1ad8e55": { + "address": "0x983211ce699ea5ab57cc528086154b6db1ad8e55", + "balance": "0x176e5b6f173ebe66" + }, + "0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e": { + "address": "0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e", + "balance": "0x2d3142f5000" + }, + "0x9d0ba4ddac06032527b140912ec808ab9451b788": { + "address": "0x9d0ba4ddac06032527b140912ec808ab9451b788", + "balance": "0x15f6f0b9d4f8d000" + } + }, + "currentBlockGasLimit": "0x793af4", + "currentNetworkTxList": [ + + ], + "cachedBalances": { + "1": { + "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4": "0x0", + "0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e": "0xcaf5317161f400", + "0x9d0ba4ddac06032527b140912ec808ab9451b788": "0x0" + }, + "3": { + "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4": "0x18d289d450bace66", + "0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e": "0x2d3142f5000", + "0x9d0ba4ddac06032527b140912ec808ab9451b788": "0x15f6f0b9d4f8d000" }, + "0x3": { + "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4": "0x176e5b6f173ebe66", + "0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e": "0x2d3142f5000", + "0x9d0ba4ddac06032527b140912ec808ab9451b788": "0x15f6f0b9d4f8d000" + } + }, + "unapprovedMsgs": {}, + "unapprovedMsgCount": 0, + "unapprovedPersonalMsgs": {}, + "unapprovedPersonalMsgCount": 0, + "unapprovedDecryptMsgs": {}, + "unapprovedDecryptMsgCount": 0, + "unapprovedEncryptionPublicKeyMsgs": {}, + "unapprovedEncryptionPublicKeyMsgCount": 0, + "unapprovedTypedMessages": {}, + "unapprovedTypedMessagesCount": 0, + "keyringTypes": [ + "Simple Key Pair", + "HD Key Tree", + "Trezor Hardware", + "Ledger Hardware" ], - transactions: {}, - currentNetworkTxList: [ + "keyrings": [ { - id: 'mockTokenTx1', - txParams: { - to: '0x8d6b81208414189a58339873ab429b6c47ab92d3', - from: '0xd85a4b6a394794842887b8284293d69163007bbb', + "type": "HD Key Tree", + "accounts": [ + "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4", + "0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e", + "0x9d0ba4ddac06032527b140912ec808ab9451b788" + ] + } + ], + "frequentRpcListDetail": [ + { + "rpcUrl": "http://localhost:8545", + "chainId": "0x539", + "ticker": "ETH", + "nickname": "Localhost 8545", + "rpcPrefs": {} + } + ], + "accountTokens": { + "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4": { + "0x1": [ + { + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "DAI", + "decimals": 18 + }, + { + "address": "0x0d8775f648430679a709e98d2b0cb6250d2887ef", + "symbol": "BAT", + "decimals": 18 + } + ], + "0x3": [ + { + "address": "0xad6d458402f60fd3bd25163575031acdce07538d", + "symbol": "DAI", + "decimals": 18 + } + ] + }, + "0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e": {}, + "0x9d0ba4ddac06032527b140912ec808ab9451b788": {} + }, + "accountHiddenTokens": { + "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4": { + "0x3": [] + } + }, + "assetImages": { + "0xad6d458402f60fd3bd25163575031acdce07538d": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xaD6D458402F60fD3Bd25163575031ACDce07538D/logo.png" + }, + "hiddenTokens": [], + "suggestedTokens": {}, + "useNonceField": false, + "usePhishDetect": true, + "lostIdentities": {}, + "forgottenPassword": false, + "ipfsGateway": "dweb.link", + "infuraBlocked": false, + "migratedPrivacyMode": false, + "selectedAddress": "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4", + "metaMetricsId": "0xc2377d11fec1c3b7dd88c4854240ee5e3ed0d9f63b00456d98d80320337b827f", + "conversionDate": 1620710825.03, + "conversionRate": 3910.28, + "currentCurrency": "usd", + "nativeCurrency": "ETH", + "usdConversionRate": 3910.28, + "ticker": "ETH", + "alertEnabledness": { + "unconnectedAccount": true, + "web3ShimUsage": true + }, + "unconnectedAccountAlertShownOrigins": {}, + "web3ShimUsageOrigins": {}, + "seedPhraseBackedUp": null, + "onboardingTabs": {}, + "incomingTransactions": { + "0x2de9256a7c604586f7ecfd87ae9509851e217f588f9f85feed793c54ed2ce0aa": { + "blockNumber": "8888976", + "id": 4678200543090532, + "metamaskNetworkId": "1", + "status": "confirmed", + "time": 1573114896000, + "txParams": { + "from": "0x3f1b52850109023775d238c7ed5d5e7161041fd1", + "gas": "0x5208", + "gasPrice": "0x124101100", + "nonce": "0x35", + "to": "0x045c619e4d29bba3b92769508831b681b83d6a96", + "value": "0xbca9bce4d98ca3" }, - time: 1700000000000, + "hash": "0x2de9256a7c604586f7ecfd87ae9509851e217f588f9f85feed793c54ed2ce0aa", + "transactionCategory": "incoming" }, + "0x320a1fd769373578f78570e5d8f56e89bc7bce9657bb5f4c12d8fe790d471bfd": { + "blockNumber": "9453174", + "id": 4678200543090535, + "metamaskNetworkId": "1", + "status": "confirmed", + "time": 1581312411000, + "txParams": { + "from": "0xa17bd07d6d38cb9e37b29f7659a4b1047701e969", + "gas": "0xc350", + "gasPrice": "0x1a13b8600", + "nonce": "0x0", + "to": "0x045c619e4d29bba3b92769508831b681b83d6a96", + "value": "0xcdb08ab4254000" + }, + "hash": "0x320a1fd769373578f78570e5d8f56e89bc7bce9657bb5f4c12d8fe790d471bfd", + "transactionCategory": "incoming" + }, + "0x8add6c1ea089a8de9b15fa2056b1875360f17916755c88ace9e5092b7a4b1239": { + "blockNumber": "10892417", + "id": 4678200543090542, + "metamaskNetworkId": "1", + "status": "confirmed", + "time": 1600515224000, + "txParams": { + "from": "0x0681d8db095565fe8a346fa0277bffde9c0edbbf", + "gas": "0x5208", + "gasPrice": "0x1d1a94a200", + "nonce": "0x2bb8a5", + "to": "0x045c619e4d29bba3b92769508831b681b83d6a96", + "value": "0xe6ed27d6668000" + }, + "hash": "0x8add6c1ea089a8de9b15fa2056b1875360f17916755c88ace9e5092b7a4b1239", + "transactionCategory": "incoming" + }, + "0x50be62ab1cabd03ff104c602c11fdef7a50f3d73c55006d5583ba97950ab1144": { + "blockNumber": "10902987", + "id": 4678200543090545, + "metamaskNetworkId": "1", + "status": "confirmed", + "time": 1600654021000, + "txParams": { + "from": "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4", + "gas": "0x5208", + "gasPrice": "0x147d357000", + "nonce": "0xf", + "to": "0x045c619e4d29bba3b92769508831b681b83d6a96", + "value": "0x63eb89da4ed00000" + }, + "hash": "0x50be62ab1cabd03ff104c602c11fdef7a50f3d73c55006d5583ba97950ab1144", + "transactionCategory": "incoming" + } + }, + "incomingTxLastFetchedBlocksByNetwork": { + "ropsten": 8872820, + "rinkeby": null, + "kovan": null, + "goerli": null, + "mainnet": 10902989 + }, + "permissionsRequests": [], + "permissionsDescriptions": {}, + "domains": { + "https://app.uniswap.org": { + "permissions": [ + { + "@context": [ + "https://github.com/MetaMask/rpc-cap" + ], + "invoker": "https://app.uniswap.org", + "parentCapability": "eth_accounts", + "id": "a7342e4b-beae-4525-a36c-c0635fd03359", + "date": 1620710693178, + "caveats": [ + { + "type": "limitResponseLength", + "value": 1, + "name": "primaryAccountOnly" + }, + { + "type": "filterResponse", + "value": [ + "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4" + ], + "name": "exposedAccounts" + } + ] + } + ] + } + }, + "permissionsLog": [ { - id: 'mockTokenTx2', - txParams: { - to: '0xafaketokenaddress', - from: '0xd85a4b6a394794842887b8284293d69163007bbb', + "id": 522690215, + "method": "eth_accounts", + "methodType": "restricted", + "origin": "https://metamask.io", + "request": { + "method": "eth_accounts", + "params": [], + "jsonrpc": "2.0", + "id": 522690215, + "origin": "https://metamask.io", + "tabId": 5 }, - time: 1600000000000, + "requestTime": 1602643170686, + "response": { + "id": 522690215, + "jsonrpc": "2.0", + "result": [] + }, + "responseTime": 1602643170688, + "success": true }, { - id: 'mockTokenTx3', - txParams: { - to: '0x8d6b81208414189a58339873ab429b6c47ab92d3', - from: '0xd85a4b6a394794842887b8284293d69163007bbb', + "id": 1620464600, + "method": "eth_accounts", + "methodType": "restricted", + "origin": "https://widget.getacute.io", + "request": { + "method": "eth_accounts", + "params": [], + "jsonrpc": "2.0", + "id": 1620464600, + "origin": "https://widget.getacute.io", + "tabId": 5 + }, + "requestTime": 1602643172935, + "response": { + "id": 1620464600, + "jsonrpc": "2.0", + "result": [] }, - time: 1500000000000, + "responseTime": 1602643172935, + "success": true }, { - id: 'mockEthTx1', - txParams: { - to: '0xd85a4b6a394794842887b8284293d69163007bbb', - from: '0xd85a4b6a394794842887b8284293d69163007bbb', + "id": 4279100021, + "method": "eth_accounts", + "methodType": "restricted", + "origin": "https://app.uniswap.org", + "request": { + "method": "eth_accounts", + "jsonrpc": "2.0", + "id": 4279100021, + "origin": "https://app.uniswap.org", + "tabId": 5 }, - time: 1400000000000, + "requestTime": 1620710669962, + "response": { + "id": 4279100021, + "jsonrpc": "2.0", + "result": [] + }, + "responseTime": 1620710669963, + "success": true }, - ], - unapprovedMsgs: { - '0xabc': { id: 'unapprovedMessage1', time: 1650000000000 }, - '0xdef': { id: 'unapprovedMessage2', time: 1550000000000 }, - '0xghi': { id: 'unapprovedMessage3', time: 1450000000000 }, - }, - unapprovedMsgCount: 0, - unapprovedPersonalMsgs: {}, - unapprovedPersonalMsgCount: 0, - unapprovedDecryptMsgs: {}, - unapprovedDecryptMsgCount: 0, - unapprovedEncryptionPublicKeyMsgs: {}, - unapprovedEncryptionPublicKeyMsgCount: 0, - keyringTypes: ['Simple Key Pair', 'HD Key Tree'], - keyrings: [ { - type: 'HD Key Tree', - accounts: [ - 'fdea65c8e26263f6d9a1b5de9555d2931a33b825', - 'c5b8dbac4c1d3f152cdeb400e2313f309c410acb', - '2f8d4a878cfa04a6e60d46362f5644deab66572d', - ], + "id": 4279100022, + "method": "eth_requestAccounts", + "methodType": "restricted", + "origin": "https://app.uniswap.org", + "request": { + "method": "eth_requestAccounts", + "jsonrpc": "2.0", + "id": 4279100022, + "origin": "https://app.uniswap.org", + "tabId": 5 + }, + "requestTime": 1620710686872, + "response": { + "id": 4279100022, + "jsonrpc": "2.0", + "result": [ + "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4" + ] + }, + "responseTime": 1620710693187, + "success": true }, { - type: 'Simple Key Pair', - accounts: ['0xd85a4b6a394794842887b8284293d69163007bbb'], + "id": 4279100023, + "method": "eth_requestAccounts", + "methodType": "restricted", + "origin": "https://app.uniswap.org", + "request": { + "method": "eth_requestAccounts", + "jsonrpc": "2.0", + "id": 4279100023, + "origin": "https://app.uniswap.org", + "tabId": 5 + }, + "requestTime": 1620710693204, + "response": { + "id": 4279100023, + "jsonrpc": "2.0", + "result": [ + "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4" + ] + }, + "responseTime": 1620710693213, + "success": true }, + { + "id": 4279100034, + "method": "eth_accounts", + "methodType": "restricted", + "origin": "https://app.uniswap.org", + "request": { + "method": "eth_accounts", + "params": [], + "jsonrpc": "2.0", + "id": 4279100034, + "origin": "https://app.uniswap.org", + "tabId": 5 + }, + "requestTime": 1620710712072, + "response": { + "id": 4279100034, + "jsonrpc": "2.0", + "result": [ + "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4" + ] + }, + "responseTime": 1620710712075, + "success": true + } ], - selectedAddress: '0xd85a4b6a394794842887b8284293d69163007bbb', - send: { - gasLimit: '0xFFFF', - gasPrice: '0xaa', - gasTotal: '0xb451dc41b578', - tokenBalance: 3434, - from: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', - to: '0x987fedabc', - amount: '0x080', - memo: '', - errors: { - someError: null, - }, - maxModeOn: false, - editingTransactionId: 97531, - }, - unapprovedTxs: { - 4768706228115573: { - id: 4768706228115573, - time: 1487363153561, - status: TRANSACTION_STATUSES.UNAPPROVED, - gasMultiplier: 1, - metamaskNetworkId: '3', - txParams: { - from: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', - to: '0x18a3462427bcc9133bb46e88bcbe39cd7ef0e761', - value: '0xde0b6b3a7640000', - metamaskId: 4768706228115573, - metamaskNetworkId: '3', - gas: '0x5209', - }, - txFee: '17e0186e60800', - txValue: 'de0b6b3a7640000', - maxCost: 'de234b52e4a0800', - gasPrice: '4a817c800', - }, - }, - currentLocale: 'en', + "permissionsHistory": { + "https://app.uniswap.org": { + "eth_accounts": { + "lastApproved": 1620710693213, + "accounts": { + "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4": 1620710693213 + } + } + } + }, + "domainMetadata": { + "https://metamask.github.io": { + "name": "E2E Test Dapp", + "icon": "https://metamask.github.io/test-dapp/metamask-fox.svg", + "lastUpdated": 1620723443380, + "host": "metamask.github.io" + } + }, + "threeBoxSyncingAllowed": false, + "showRestorePrompt": true, + "threeBoxLastUpdated": 0, + "threeBoxAddress": null, + "threeBoxSynced": false, + "threeBoxDisabled": false, + "swapsState": { + "quotes": {}, + "fetchParams": null, + "tokens": null, + "tradeTxId": null, + "approveTxId": null, + "quotesLastFetched": null, + "customMaxGas": "", + "customGasPrice": null, + "selectedAggId": null, + "customApproveTxData": "", + "errorKey": "", + "topAggId": null, + "routeState": "", + "swapsFeatureIsLive": false, + "swapsQuoteRefreshTime": 60000 + }, + "ensResolutionsByAddress": {}, + "pendingApprovals": {}, + "pendingApprovalCount": 0 }, - appState: { - menuOpen: false, - currentView: { - name: 'accountDetail', - detailView: null, - context: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', - }, - accountDetail: { - subview: 'transactions', - }, - modal: { - modalState: {}, - previousModalState: {}, - }, - isLoading: false, - warning: null, - scrollToBottom: false, - forgottenPassword: null, + "appState": { + "shouldClose": false, + "menuOpen": false, + "modal": { + "open": false, + "modalState": { + "name": null, + "props": {} + }, + "previousModalState": { + "name": null + } + }, + "sidebar": { + "isOpen": false, + "transitionName": "", + "type": "", + "props": {} + }, + "alertOpen": false, + "alertMessage": null, + "qrCodeData": null, + "networkDropdownOpen": false, + "accountDetail": { + "subview": "transactions" + }, + "isLoading": false, + "warning": null, + "buyView": {}, + "isMouseUser": true, + "gasIsLoading": false, + "defaultHdPaths": { + "trezor": "m/44'/60'/0'/0", + "ledger": "m/44'/60'/0'/0/0" + }, + "networksTabSelectedRpcUrl": "", + "networksTabIsInAddMode": false, + "loadingMethodData": false, + "show3BoxModalAfterImport": false, + "threeBoxLastUpdated": null, + "requestAccountTabs": {}, + "openMetaMaskTabs": {}, + "currentWindowTab": {} }, - send: { - fromDropdownOpen: false, - toDropdownOpen: false, - errors: { someError: null }, + "history": { + "mostRecentOverviewPage": "/" }, -}; + "send": { + "toDropdownOpen": false, + "gasButtonGroupShown": true, + "errors": {} + }, + "confirmTransaction": { + "txData": { + "id": 3111025347726181, + "time": 1620723786838, + "status": "unapproved", + "metamaskNetworkId": "3", + "chainId": "0x3", + "loadingDefaults": false, + "txParams": { + "from": "0x983211ce699ea5ab57cc528086154b6db1ad8e55", + "to": "0xad6d458402f60fd3bd25163575031acdce07538d", + "value": "0x0", + "data": "0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170", + "gas": "0xea60", + "gasPrice": "0x4a817c800" + }, + "type": "standard", + "origin": "https://metamask.github.io", + "transactionCategory": "approve", + "history": [ + { + "id": 3111025347726181, + "time": 1620723786838, + "status": "unapproved", + "metamaskNetworkId": "3", + "chainId": "0x3", + "loadingDefaults": true, + "txParams": { + "from": "0x983211ce699ea5ab57cc528086154b6db1ad8e55", + "to": "0xad6d458402f60fd3bd25163575031acdce07538d", + "value": "0x0", + "data": "0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170", + "gas": "0xea60", + "gasPrice": "0x4a817c800" + }, + "type": "standard", + "origin": "https://metamask.github.io", + "transactionCategory": "approve" + }, + [ + { + "op": "replace", + "path": "/loadingDefaults", + "value": false, + "note": "Added new unapproved transaction.", + "timestamp": 1620723786844 + } + ] + ] + }, + "tokenData": { + "args": [ + "0x9bc5baF874d2DA8D216aE9f137804184EE5AfEF4", + { + "type": "BigNumber", + "hex": "0x011170" + } + ], + "functionFragment": { + "type": "function", + "name": "approve", + "constant": false, + "inputs": [ + { + "name": "_spender", + "type": "address", + "indexed": null, + "components": null, + "arrayLength": null, + "arrayChildren": null, + "baseType": "address", + "_isParamType": true + }, + { + "name": "_value", + "type": "uint256", + "indexed": null, + "components": null, + "arrayLength": null, + "arrayChildren": null, + "baseType": "uint256", + "_isParamType": true + } + ], + "outputs": [ + { + "name": "success", + "type": "bool", + "indexed": null, + "components": null, + "arrayLength": null, + "arrayChildren": null, + "baseType": "bool", + "_isParamType": true + } + ], + "payable": false, + "stateMutability": "nonpayable", + "gas": null, + "_isFragment": true + }, + "name": "approve", + "signature": "approve(address,uint256)", + "sighash": "0x095ea7b3", + "value": { + "type": "BigNumber", + "hex": "0x00" + } + }, + "fiatTransactionAmount": "0", + "fiatTransactionFee": "4.72", + "fiatTransactionTotal": "4.72", + "ethTransactionAmount": "0", + "ethTransactionFee": "0.0012", + "ethTransactionTotal": "0.0012", + "hexTransactionAmount": "0x0", + "hexTransactionFee": "0x44364c5bb0000", + "hexTransactionTotal": "0x44364c5bb0000", + "nonce": "" + }, + "swaps": { + "aggregatorMetadata": null, + "approveTxId": null, + "balanceError": false, + "fetchingQuotes": false, + "fromToken": null, + "quotesFetchStartTime": null, + "topAssets": {}, + "toToken": null, + "customGas": { + "price": null, + "limit": null, + "loading": "INITIAL", + "priceEstimates": {}, + "fallBackPrice": null + } + }, + "gas": { + "customData": { + "price": null, + "limit": "0xcb28" + }, + "basicEstimates": { + "average": 2 + }, + "basicEstimateIsLoading": false + } +} export default state; diff --git a/CHANGELOG.md b/CHANGELOG.md index fe938d84f..ce028e641 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [9.7.0] +### Added +- [#11021](https://github.com/MetaMask/metamask-extension/pull/11021): Add periodic reminder modal for backing up recovery phrase +- [#11179](https://github.com/MetaMask/metamask-extension/pull/11179): Add warning to the custom network form when attempting to add a custom network that already exists + +### Changed +- [#11200](https://github.com/MetaMask/metamask-extension/pull/11200): Swaps: Shows custom tokens added from main assets tab +- [#11111](https://github.com/MetaMask/metamask-extension/pull/11111): Removing low gas price warning in advanced tab on test networks +- [#11145](https://github.com/MetaMask/metamask-extension/pull/11145): Swaps: Improving price difference notifications and warnings +- [#11124](https://github.com/MetaMask/metamask-extension/pull/11124): Swaps: Allowing for continual new swap submissions without flow reset +- [#11278](https://github.com/MetaMask/metamask-extension/pull/11278): Updated contract-metadata version to 1.26.0 + +### Fixed +- [#11017](https://github.com/MetaMask/metamask-extension/pull/11017): Fixes custom RPC block explorer links +- [#11257](https://github.com/MetaMask/metamask-extension/pull/11257): Fixes incorrect network currency label in encryption public key requests + ## [9.6.1] ### Fixed - [#11309](https://github.com/MetaMask/metamask-extension/pull/11309): Fixed signTypeData parameter validation issue @@ -2301,7 +2317,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Uncategorized - Added the ability to restore accounts from seed words. -[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v9.6.1...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v9.7.0...HEAD +[9.7.0]: https://github.com/MetaMask/metamask-extension/compare/v9.6.1...v9.7.0 [9.6.1]: https://github.com/MetaMask/metamask-extension/compare/v9.6.0...v9.6.1 [9.6.0]: https://github.com/MetaMask/metamask-extension/compare/v9.5.9...v9.6.0 [9.5.9]: https://github.com/MetaMask/metamask-extension/compare/v9.5.8...v9.5.9 diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index ad2de54f3..40f8bd8ac 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -52,6 +52,10 @@ "addContact": { "message": "Add contact" }, + "addCustomTokenByContractAddress": { + "message": "Can’t find a token? You can manually add any token by pasting its address. Token contract addresses can be found on $1.", + "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" + }, "addEthereumChainConfirmationDescription": { "message": "This will allow this network to be used within MetaMask." }, @@ -285,6 +289,9 @@ "chainIdDefinition": { "message": "The chain ID used to sign transactions for this network." }, + "chainIdExistsErrorMsg": { + "message": "This Chain ID is currently used by the $1 network." + }, "chromeRequiredForHardwareWallets": { "message": "You need to use MetaMask on Google Chrome in order to connect to your Hardware Wallet." }, @@ -410,6 +417,9 @@ "continueToWyre": { "message": "Continue to Wyre" }, + "contract": { + "message": "Contract" + }, "contractAddressError": { "message": "You are sending tokens to the token's contract address. This may result in the loss of these tokens." }, @@ -893,6 +903,12 @@ "message": "or $1", "description": "$1 represents the text from `importAccountLinkText` as a link" }, + "importTokenQuestion": { + "message": "Import token?" + }, + "importTokenWarning": { + "message": "Anyone can create a token with any name, including fake versions of existing tokens. Add and trade at your own risk!" + }, "importWallet": { "message": "Import wallet" }, @@ -1042,6 +1058,9 @@ "mainnet": { "message": "Ethereum Mainnet" }, + "makeAnotherSwap": { + "message": "Create a new swap" + }, "max": { "message": "Max" }, @@ -1438,6 +1457,30 @@ "recipientAddressPlaceholder": { "message": "Search, public address (0x), or ENS" }, + "recoveryPhraseReminderBackupStart": { + "message": "Start here" + }, + "recoveryPhraseReminderConfirm": { + "message": "Got it" + }, + "recoveryPhraseReminderHasBackedUp": { + "message": "Always keep your Secret Recovery Phrase in a secure and secret place" + }, + "recoveryPhraseReminderHasNotBackedUp": { + "message": "Need to backup your Secret Recovery Phrase again?" + }, + "recoveryPhraseReminderItemOne": { + "message": "Never share your Secret Recovery Phrase with anyone" + }, + "recoveryPhraseReminderItemTwo": { + "message": "The MetaMask team will never ask for your Secret Recovery Phrase" + }, + "recoveryPhraseReminderSubText": { + "message": "Your Secret Recovery Phrase controls all of your accounts." + }, + "recoveryPhraseReminderTitle": { + "message": "Protect your funds" + }, "reject": { "message": "Reject" }, @@ -1949,18 +1992,18 @@ "message": "You are about to swap $1 $2 (~$3) for $4 $5 (~$6).", "description": "This message represents the price slippage for the swap. $1 and $4 are a number (ex: 2.89), $2 and $5 are symbols (ex: ETH), and $3 and $6 are fiat currency amounts." }, - "swapPriceDifferenceAcknowledgement": { - "message": "I'm aware" - }, "swapPriceDifferenceTitle": { "message": "Price difference of ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceDifferenceTooltip": { - "message": "The difference in market prices can be affected by fees taken by intermediaries, size of market, size of trade, or market inefficiencies." + "swapPriceImpactTooltip": { + "message": "Price impact is the difference between the current market price and the amount received during transaction execution. Price impact is a function of the size of your trade relative to the size of the liquidity pool." + }, + "swapPriceUnavailableDescription": { + "message": "Price impact could not be determined due to lack of market price data. Please confirm that you are comfortable with the amount of tokens you are about to receive before swapping." }, - "swapPriceDifferenceUnavailable": { - "message": "Market price is unavailable. Make sure you feel comfortable with the returned amount before proceeding." + "swapPriceUnavailableTitle": { + "message": "Check your rate before proceeding" }, "swapProcessing": { "message": "Processing" @@ -2063,13 +2106,13 @@ "message": "Swap $1 to $2", "description": "Used in the transaction display list to describe a swap. $1 and $2 are the symbols of tokens in involved in a swap." }, + "swapTokenVerificationAddedManually": { + "message": "This token has been added manually." + }, "swapTokenVerificationMessage": { "message": "Always confirm the token address on $1.", "description": "Points the user to Etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"Etherscan\" followed by an info icon that shows more info on hover." }, - "swapTokenVerificationNoSource": { - "message": "This token has not been verified." - }, "swapTokenVerificationOnlyOneSource": { "message": "Only verified on 1 source." }, @@ -2093,9 +2136,6 @@ "message": "Multiple tokens can use the same name and symbol. Check $1 to verify this is the token you're looking for.", "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." }, - "swapViewToken": { - "message": "View $1" - }, "swapYourTokenBalance": { "message": "$1 $2 available to swap", "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" @@ -2219,6 +2259,9 @@ "tokenSymbol": { "message": "Token Symbol" }, + "tooltipApproveButton": { + "message": "I understand" + }, "total": { "message": "Total" }, @@ -2333,7 +2376,7 @@ "message": "URLs require the appropriate HTTP/HTTPS prefix." }, "urlExistsErrorMsg": { - "message": "URL is already present in existing list of networks" + "message": "This URL is currently used by the $1 network." }, "usePhishingDetection": { "message": "Use Phishing Detection" @@ -2355,6 +2398,10 @@ "message": "Verify this token on $1", "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" }, + "verifyThisUnconfirmedTokenOn": { + "message": "Verify this token on $1 and make sure this is the token you want to trade.", + "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" + }, "viewAccount": { "message": "View Account" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 09572b549..ea6826817 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -1034,6 +1034,9 @@ "mainnet": { "message": "Red principal de Ethereum" }, + "makeAnotherSwap": { + "message": "Crear un nuevo canje" + }, "max": { "message": "Máx." }, @@ -1893,12 +1896,6 @@ "message": "Diferencia de precio de ~$1 %", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceDifferenceTooltip": { - "message": "La diferencia en los precios de mercado puede verse afectada por las tarifas cobradas por los intermediarios, el tamaño del mercado, el tamaño del comercio o las ineficiencias del mercado." - }, - "swapPriceDifferenceUnavailable": { - "message": "El precio de mercado no está disponible. Asegúrese de sentirse cómodo con el monto devuelto antes de continuar." - }, "swapProcessing": { "message": "Procesamiento" }, @@ -2027,9 +2024,6 @@ "message": "Varios tokens pueden usar el mismo nombre y símbolo. Revise $1 para comprobar que este es el token que busca.", "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." }, - "swapViewToken": { - "message": "Ver $1" - }, "swapYourTokenBalance": { "message": "$1 $2 disponible para canje", "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" @@ -2153,6 +2147,9 @@ "tokenSymbol": { "message": "Símbolo del token" }, + "tooltipApproveButton": { + "message": "Comprendo" + }, "total": { "message": "Total" }, diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index e9a8397af..413624d20 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -1042,6 +1042,9 @@ "mainnet": { "message": "Red principal de Ethereum" }, + "makeAnotherSwap": { + "message": "Crear un nuevo canje" + }, "max": { "message": "Máx." }, @@ -1937,12 +1940,6 @@ "message": "Diferencia de precio de ~$1 %", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceDifferenceTooltip": { - "message": "La diferencia en los precios de mercado puede verse afectada por las tarifas cobradas por los intermediarios, el tamaño del mercado, el tamaño del comercio o las ineficiencias del mercado." - }, - "swapPriceDifferenceUnavailable": { - "message": "El precio de mercado no está disponible. Asegúrese de sentirse cómodo con el monto devuelto antes de continuar." - }, "swapProcessing": { "message": "Procesamiento" }, @@ -2071,9 +2068,6 @@ "message": "Varios tokens pueden usar el mismo nombre y símbolo. Revise $1 para comprobar que este es el token que busca.", "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." }, - "swapViewToken": { - "message": "Ver $1" - }, "swapYourTokenBalance": { "message": "$1 $2 disponible para canje", "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" @@ -2197,6 +2191,9 @@ "tokenSymbol": { "message": "Símbolo del token" }, + "tooltipApproveButton": { + "message": "Comprendo" + }, "total": { "message": "Total" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 3c855ca53..7d57f515b 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -1034,6 +1034,9 @@ "mainnet": { "message": "Ethereum Mainnet" }, + "makeAnotherSwap": { + "message": "एक नया स्वैप बनाएँ" + }, "max": { "message": "अधिकतम" }, @@ -1893,6 +1896,15 @@ "message": "~$1% का मूल्य अंतर", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, + "swapPriceImpactTooltip": { + "message": "मूल्य प्रभाव, वर्तमान बाजार मूल्य और लेन-देन निष्पादन के दौरान प्राप्त राशि के बीच का अंतर है। मूल्य प्रभाव चलनिधि पूल के आकार के सापेक्ष आपके व्यापार के आकार का एक कार्य है।" + }, + "swapPriceUnavailableDescription": { + "message": "बाजार मूल्य डेटा की कमी के कारण मूल्य प्रभाव को निर्धारित नहीं किया जा सका। कृपया पुष्टि करें कि आप स्वैप करने से पहले प्राप्त होने वाले टोकन की राशि को लेकर सहज हैं।" + }, + "swapPriceUnavailableTitle": { + "message": "आगे बढ़ने से पहले अपने दर की जाँच करें" + }, "swapProcessing": { "message": "प्रसंस्करण" }, @@ -2021,9 +2033,6 @@ "message": "एकाधिक टोकन एक ही नाम और प्रतीक का उपयोग कर सकते हैं। यह सत्यापित करने के लिए $1 की जाँच करें कि यह वही टोकन है, जिसकी आप तलाश कर रहे हैं।", "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." }, - "swapViewToken": { - "message": "$1 देखें" - }, "swapYourTokenBalance": { "message": "$1 $2 स्वैप के लिए उपलब्ध है", "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" @@ -2147,6 +2156,9 @@ "tokenSymbol": { "message": "टोकन का प्रतीक" }, + "tooltipApproveButton": { + "message": "मैं समझता हूं" + }, "total": { "message": "कुलयोग" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index d85b0cd64..b019c25b5 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -1034,6 +1034,9 @@ "mainnet": { "message": "Ethereum Mainnet" }, + "makeAnotherSwap": { + "message": "Buat penukaran baru" + }, "max": { "message": "Maks." }, @@ -1893,6 +1896,15 @@ "message": "Perbedaan harga ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, + "swapPriceImpactTooltip": { + "message": "Dampak harga adalah selisih antara harga pasar saat ini dan jumlah yang diterima selama terjadinya transaksi. Dampak harga adalah fungsi ukuran dagang relatif terhadap ukuran pool likuiditas." + }, + "swapPriceUnavailableDescription": { + "message": "Dampak harga tidak dapat ditentukan karena kurangnya data harga pasar. Harap konfirmasi bahwa Anda setuju dengan jumlah token yang akan Anda terima sebelum penukaran." + }, + "swapPriceUnavailableTitle": { + "message": "Periksa tarif Anda sebelum melanjutkan" + }, "swapProcessing": { "message": "Memproses" }, @@ -2021,9 +2033,6 @@ "message": "Beberapa token dapat menggunakan simbol dan nama yang sama. Periksa $1 untuk memverifikasi inilah token yang Anda cari.", "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." }, - "swapViewToken": { - "message": "Lihat $1" - }, "swapYourTokenBalance": { "message": "$1 $2 tersedia untuk ditukar", "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" @@ -2147,6 +2156,9 @@ "tokenSymbol": { "message": "Simbol Token" }, + "tooltipApproveButton": { + "message": "Saya paham" + }, "total": { "message": "Total" }, diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index 36212c4f6..161086c29 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -1621,19 +1621,10 @@ "message": "Stai per scambiare $1 $2 (~$3) per $4 $5 (~$6).", "description": "This message represents the price slippage for the swap. $1 and $4 are a number (ex: 2.89), $2 and $5 are symbols (ex: ETH), and $3 and $6 are fiat currency amounts." }, - "swapPriceDifferenceAcknowledgement": { - "message": "Sono consapevole" - }, "swapPriceDifferenceTitle": { "message": "Differenza di prezzo di circa ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceDifferenceTooltip": { - "message": "La differenza tra i prezzi del mercato può essere influenzata da commissioni prelevate da intermediari, dimensione del mercato, dimensione dello scambio, o inefficienze del mercato." - }, - "swapPriceDifferenceUnavailable": { - "message": "Il prezzo di mercato non è disponibile. Assicurati di sentirti a tuo agio con l'importo restituito prima di procedere." - }, "swapProcessing": { "message": "In elaborazione" }, @@ -1749,9 +1740,6 @@ "message": "Più token possono usare lo stesso nome e simbolo. Verifica su $1 che questo sia il token che stai cercando.", "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." }, - "swapViewToken": { - "message": "Vedi $1" - }, "swapYourTokenBalance": { "message": "$1 $2 disponibili allo scambio", "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 2b83f06b9..7ac44b231 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -1034,6 +1034,9 @@ "mainnet": { "message": "イーサリアム メインネット" }, + "makeAnotherSwap": { + "message": "新しいスワップの作成" + }, "max": { "message": "最大" }, @@ -1893,12 +1896,6 @@ "message": "約 $1% の価格差", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceDifferenceTooltip": { - "message": "市場価格の違いは、仲介業者が負担する手数料、市場規模、取引量、または取引価格差の影響を受けることがあります。" - }, - "swapPriceDifferenceUnavailable": { - "message": "マーケット価格は利用できません。続行する前に、返金額に問題がないことを確認してください。" - }, "swapProcessing": { "message": "処理中" }, @@ -2027,9 +2024,6 @@ "message": "複数のトークンが同じ名前とシンボルを使用できます。$1 をチェックして、これが探しているトークンであることを確認します。", "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." }, - "swapViewToken": { - "message": "$1 を表示" - }, "swapYourTokenBalance": { "message": "$1 $2 はスワップに使用可能です", "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" @@ -2153,6 +2147,9 @@ "tokenSymbol": { "message": "トークン シンボル" }, + "tooltipApproveButton": { + "message": "理解しました" + }, "total": { "message": "合計" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 205bbe4b9..e81d77b82 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -1038,6 +1038,9 @@ "mainnet": { "message": "이더리움 메인넷" }, + "makeAnotherSwap": { + "message": "새 스왑 생성" + }, "max": { "message": "최대" }, @@ -1933,6 +1936,15 @@ "message": "~$1%의 가격 차이", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, + "swapPriceImpactTooltip": { + "message": "가격 영향은 현재 시장 가격과 거래 실행 도중 받은 금액 사이의 차이입니다. 가격 영향은 유동성 풀의 크기 대비 거래의 크기를 나타내는 함수입니다." + }, + "swapPriceUnavailableDescription": { + "message": "시장 가격 데이터가 부족하여 가격 영향을 파악할 수 없습니다. 스왑하기 전에 받게 될 토큰 수에 만족하시는지 확인하시기 바랍니다." + }, + "swapPriceUnavailableTitle": { + "message": "진행하기 전에 요율을 확인하십시오." + }, "swapProcessing": { "message": "처리 중" }, @@ -2061,9 +2073,6 @@ "message": "여러 토큰이 같은 이름과 기호를 사용할 수 있습니다. $1을(를) 확인하여 원하는 토큰인지 확인하세요.", "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." }, - "swapViewToken": { - "message": "$1 보기" - }, "swapYourTokenBalance": { "message": "$1 $2 스왑 가능", "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" @@ -2187,6 +2196,9 @@ "tokenSymbol": { "message": "토큰 기호" }, + "tooltipApproveButton": { + "message": "이해했습니다." + }, "total": { "message": "합계" }, diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index 483b62da4..3a9141757 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -1042,6 +1042,9 @@ "mainnet": { "message": "Ethereum Mainnet" }, + "makeAnotherSwap": { + "message": "Gumawa ng bagong swap" + }, "max": { "message": "Max" }, @@ -1937,6 +1940,15 @@ "message": "Kaibahan sa presyo na ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, + "swapPriceImpactTooltip": { + "message": "Ang epekto sa presyo ay ang pagkakaiba sa kasalukuyang presyo sa merkado at sa halagang natanggap sa pag-execute ng transaksyon. Ang epekto sa presyo ay isang function ng laki ng iyong trade kumpara sa laki ng liquidity pool." + }, + "swapPriceUnavailableDescription": { + "message": "Hindi natukoy ang epekto sa presyo dahil sa kakulangan ng data sa presyo sa merkado. Pakikumpirma na kumportable ka sa dami ng mga token na matatanggap mo bago makipag-swap." + }, + "swapPriceUnavailableTitle": { + "message": "Tingnan ang iyong rate bago magpatuloy" + }, "swapProcessing": { "message": "Pagproseso" }, @@ -2188,6 +2200,9 @@ "tokenSymbol": { "message": "Simbolo ng Token" }, + "tooltipApproveButton": { + "message": "Nauunawaan ko" + }, "total": { "message": "Kabuuan" }, diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index ef6d42bdc..f6d41ac24 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -1028,6 +1028,9 @@ "mainnet": { "message": "Mainnet do Ethereum" }, + "makeAnotherSwap": { + "message": "Criar novo swap" + }, "max": { "message": "Máx" }, @@ -1877,6 +1880,15 @@ "message": "Diferença de preço de aproximadamente $1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, + "swapPriceImpactTooltip": { + "message": "O impacto no preço é a diferença entre o preço de mercado atual e o valor recebido durante a execução da transação. O impacto no preço é uma função do tamanho do seu comércio em relação ao tamanho do pool de liquidez." + }, + "swapPriceUnavailableDescription": { + "message": "O impacto no preço não poderia ser determinado devido aos dados do preço de mercado. Confirme que você está satisfeito com o valor dos tokens que você está prestes a receber antes de fazer swap." + }, + "swapPriceUnavailableTitle": { + "message": "Verifique sua taxa antes de continuar" + }, "swapProcessing": { "message": "Processando" }, @@ -2128,6 +2140,9 @@ "tokenSymbol": { "message": "Símbolo do token" }, + "tooltipApproveButton": { + "message": "Eu entendo" + }, "total": { "message": "Total" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index d5f3a2381..02dfbd8bb 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -1034,6 +1034,9 @@ "mainnet": { "message": "Сеть Ethereum Mainnet" }, + "makeAnotherSwap": { + "message": "Создать новый своп" + }, "max": { "message": "Макс." }, @@ -1893,6 +1896,15 @@ "message": "Разница в цене составляет ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, + "swapPriceImpactTooltip": { + "message": "Колебание цены — это разница между текущей рыночной ценой и суммой, полученной во время выполнения транзакции. Колебание цены зависит от размера вашей сделки относительно размера пула ликвидности." + }, + "swapPriceUnavailableDescription": { + "message": "Колебание цены определить не удалось из-за отсутствия данных о рыночных ценах. Перед свопом подтвердите, что вас устраивает количество токенов, которое вы получите." + }, + "swapPriceUnavailableTitle": { + "message": "Прежде чем продолжить, проверьте курс" + }, "swapProcessing": { "message": "Обработка" }, @@ -2021,9 +2033,6 @@ "message": "Несколько токенов могут использовать одно и то же имя и символ. Убедитесь, что это именно тот токен, который вы ищете, на $1.", "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." }, - "swapViewToken": { - "message": "Просмотреть $1" - }, "swapYourTokenBalance": { "message": "$1 $2 доступны для свопа", "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" @@ -2147,6 +2156,9 @@ "tokenSymbol": { "message": "Символ токена" }, + "tooltipApproveButton": { + "message": "Я понимаю" + }, "total": { "message": "Итого" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index a6f9e1506..caf6059fd 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -1699,9 +1699,6 @@ "message": "Maaaring gamitin ng maraming token ang iisang pangalan at simbolo. Suriin ang $1 para ma-verify na ito ang token na hinahanap mo.", "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." }, - "swapViewToken": { - "message": "Tingnan ang $1" - }, "swapYourTokenBalance": { "message": "Available ang $1 $2 na i-swap", "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index ed8623056..c2955653b 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -1034,6 +1034,9 @@ "mainnet": { "message": "Mạng chính thức của Ethereum" }, + "makeAnotherSwap": { + "message": "Tạo một giao dịch hoán đổi mới" + }, "max": { "message": "Tối đa" }, @@ -1893,6 +1896,15 @@ "message": "Chênh lệch giá ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, + "swapPriceImpactTooltip": { + "message": "Tác động về giá là mức chênh lệch giữa giá thị trường hiện tại và số tiền nhận được trong quá trình thực hiện giao dịch. Tác động giá là một hàm trong quy mô giao dịch của bạn so với quy mô của nhóm thanh khoản." + }, + "swapPriceUnavailableDescription": { + "message": "Không thể xác định tác động giá do thiếu dữ liệu giá thị trường. Vui lòng xác nhận rằng bạn cảm thấy thoải mái với số lượng token bạn sắp nhận được trước khi hoán đổi." + }, + "swapPriceUnavailableTitle": { + "message": "Hãy kiểm tra tỷ giá trước khi tiếp tục" + }, "swapProcessing": { "message": "Đang xử lý" }, @@ -2021,9 +2033,6 @@ "message": "Nhiều token có thể dùng cùng một tên và ký hiệu. Hãy kiểm tra $1 để xác minh xem đây có phải là token bạn đang tìm kiếm không.", "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." }, - "swapViewToken": { - "message": "Xem $1" - }, "swapYourTokenBalance": { "message": "Có sẵn $1 $2 để hoán đổi", "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" @@ -2147,6 +2156,9 @@ "tokenSymbol": { "message": "Ký hiệu token" }, + "tooltipApproveButton": { + "message": "Tôi đã hiểu" + }, "total": { "message": "Tổng" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index d8a102716..cb1950b2c 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -1619,12 +1619,6 @@ "message": "价格差异 ~$1%", "description": "$1 is a number (ex: 1.23) that represents the price difference." }, - "swapPriceDifferenceTooltip": { - "message": "市场价格的差异可能受到中介机构收取的费用、市场规模、交易规模或市场效率低下的影响。" - }, - "swapPriceDifferenceUnavailable": { - "message": "市场价格不可用。 请确认您对退回的数额感到满意后再继续。" - }, "swapProcessing": { "message": "处理中" }, @@ -1726,9 +1720,6 @@ "message": "多个代币可以使用相同的名称和符号。检查 $1(以太坊浏览器)以确认这是您正在寻找的代币。", "description": "This appears in a tooltip next to the verifyThisTokenOn message. It gives the user more information about why they should check the token on a block explorer. $1 will be the name or url of the block explorer, which will be the translation of 'etherscan' or a block explorer url specified for a custom network." }, - "swapViewToken": { - "message": "查看 $1" - }, "swapYourTokenBalance": { "message": "$1 $2 可用", "description": "Tells the user how much of a token they have in their balance. $1 is a decimal number amount of tokens, and $2 is a token symbol" diff --git a/app/scripts/background.js b/app/scripts/background.js index ae46819c1..38f4305d6 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -19,6 +19,7 @@ import { ENVIRONMENT_TYPE_NOTIFICATION, ENVIRONMENT_TYPE_FULLSCREEN, } from '../../shared/constants/app'; +import { SECOND } from '../../shared/constants/time'; import migrations from './migrations'; import Migrator from './lib/migrator'; import ExtensionPlatform from './platforms/extension'; @@ -491,7 +492,7 @@ async function openPopup() { clearInterval(interval); resolve(); } - }, 1000); + }, SECOND); }); } diff --git a/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js index ce92798e0..9916e5d4f 100644 --- a/app/scripts/controllers/app-state.js +++ b/app/scripts/controllers/app-state.js @@ -1,6 +1,7 @@ import EventEmitter from 'events'; import { ObservableStore } from '@metamask/obs-store'; import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller'; +import { MINUTE } from '../../../shared/constants/time'; export default class AppStateController extends EventEmitter { /** @@ -24,6 +25,8 @@ export default class AppStateController extends EventEmitter { connectedStatusPopoverHasBeenShown: true, defaultHomeActiveTabName: null, browserEnvironment: {}, + recoveryPhraseReminderHasBeenShown: false, + recoveryPhraseReminderLastShown: new Date().getTime(), ...initState, }); this.timer = null; @@ -112,6 +115,27 @@ export default class AppStateController extends EventEmitter { }); } + /** + * Record that the user has been shown the recovery phrase reminder + * @returns {void} + */ + setRecoveryPhraseReminderHasBeenShown() { + this.store.updateState({ + recoveryPhraseReminderHasBeenShown: true, + }); + } + + /** + * Record the timestamp of the last time the user has seen the recovery phrase reminder + * @param {number} lastShown - timestamp when user was last shown the reminder + * @returns {void} + */ + setRecoveryPhraseReminderLastShown(lastShown) { + this.store.updateState({ + recoveryPhraseReminderLastShown: lastShown, + }); + } + /** * Sets the last active time to the current time * @returns {void} @@ -156,7 +180,7 @@ export default class AppStateController extends EventEmitter { this.timer = setTimeout( () => this.onInactiveTimeout(), - timeoutMinutes * 60 * 1000, + timeoutMinutes * MINUTE, ); } diff --git a/app/scripts/controllers/detect-tokens.js b/app/scripts/controllers/detect-tokens.js index 3115f94d3..c9ea77f2d 100644 --- a/app/scripts/controllers/detect-tokens.js +++ b/app/scripts/controllers/detect-tokens.js @@ -4,9 +4,10 @@ import { warn } from 'loglevel'; import SINGLE_CALL_BALANCES_ABI from 'single-call-balance-checker-abi'; import { MAINNET_CHAIN_ID } from '../../../shared/constants/network'; import { SINGLE_CALL_BALANCES_ADDRESS } from '../constants/contracts'; +import { MINUTE } from '../../../shared/constants/time'; // By default, poll every 3 minutes -const DEFAULT_INTERVAL = 180 * 1000; +const DEFAULT_INTERVAL = MINUTE * 3; /** * A controller that polls for token exchange diff --git a/app/scripts/controllers/incoming-transactions.js b/app/scripts/controllers/incoming-transactions.js index 7aa621898..e5fc03543 100644 --- a/app/scripts/controllers/incoming-transactions.js +++ b/app/scripts/controllers/incoming-transactions.js @@ -18,8 +18,9 @@ import { RINKEBY_CHAIN_ID, ROPSTEN_CHAIN_ID, } from '../../../shared/constants/network'; +import { SECOND } from '../../../shared/constants/time'; -const fetchWithTimeout = getFetchWithTimeout(30000); +const fetchWithTimeout = getFetchWithTimeout(SECOND * 30); /** * @typedef {import('../../../shared/constants/transaction').TransactionMeta} TransactionMeta diff --git a/app/scripts/controllers/incoming-transactions.test.js b/app/scripts/controllers/incoming-transactions.test.js index 347fa7952..f4cac0309 100644 --- a/app/scripts/controllers/incoming-transactions.test.js +++ b/app/scripts/controllers/incoming-transactions.test.js @@ -19,6 +19,7 @@ import { TRANSACTION_TYPES, TRANSACTION_STATUSES, } from '../../../shared/constants/transaction'; +import { MILLISECOND } from '../../../shared/constants/time'; const IncomingTransactionsController = proxyquire('./incoming-transactions', { '../../../shared/modules/random-id': { default: () => 54321 }, @@ -26,7 +27,7 @@ const IncomingTransactionsController = proxyquire('./incoming-transactions', { const FAKE_CHAIN_ID = '0x1338'; const MOCK_SELECTED_ADDRESS = '0x0101'; -const SET_STATE_TIMEOUT = 10; +const SET_STATE_TIMEOUT = MILLISECOND * 10; const EXISTING_INCOMING_TX = { id: 777, hash: '0x123456' }; const PREPOPULATED_INCOMING_TXS_BY_HASH = { diff --git a/app/scripts/controllers/network/createJsonRpcClient.js b/app/scripts/controllers/network/createJsonRpcClient.js index 4b505258d..050430740 100644 --- a/app/scripts/controllers/network/createJsonRpcClient.js +++ b/app/scripts/controllers/network/createJsonRpcClient.js @@ -6,9 +6,10 @@ import createInflightMiddleware from 'eth-json-rpc-middleware/inflight-cache'; import createBlockTrackerInspectorMiddleware from 'eth-json-rpc-middleware/block-tracker-inspector'; import providerFromMiddleware from 'eth-json-rpc-middleware/providerFromMiddleware'; import { PollingBlockTracker } from 'eth-block-tracker'; +import { SECOND } from '../../../../shared/constants/time'; const inTest = process.env.IN_TEST === 'true'; -const blockTrackerOpts = inTest ? { pollingInterval: 1000 } : {}; +const blockTrackerOpts = inTest ? { pollingInterval: SECOND } : {}; const getTestMiddlewares = () => { return inTest ? [createEstimateGasDelayTestMiddleware()] : []; }; @@ -51,7 +52,7 @@ function createChainIdMiddleware(chainId) { function createEstimateGasDelayTestMiddleware() { return createAsyncMiddleware(async (req, _, next) => { if (req.method === 'eth_estimateGas') { - await new Promise((resolve) => setTimeout(resolve, 2000)); + await new Promise((resolve) => setTimeout(resolve, SECOND * 2)); } return next(); }); diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js index a308a7d4b..1799f658f 100644 --- a/app/scripts/controllers/network/network.js +++ b/app/scripts/controllers/network/network.js @@ -19,6 +19,7 @@ import { RINKEBY_CHAIN_ID, INFURA_BLOCKED_KEY, } from '../../../../shared/constants/network'; +import { SECOND } from '../../../../shared/constants/time'; import { isPrefixedFormattedHexString, isSafeChainId, @@ -29,7 +30,7 @@ import createInfuraClient from './createInfuraClient'; import createJsonRpcClient from './createJsonRpcClient'; const env = process.env.METAMASK_ENV; -const fetchWithTimeout = getFetchWithTimeout(30000); +const fetchWithTimeout = getFetchWithTimeout(SECOND * 30); let defaultProviderConfigOpts; if (process.env.IN_TEST === 'true') { @@ -205,7 +206,7 @@ export default class NetworkController extends EventEmitter { }); } - async setProviderType(type, rpcUrl = '', ticker = 'ETH', nickname = '') { + async setProviderType(type) { assert.notStrictEqual( type, NETWORK_TYPE_RPC, @@ -216,7 +217,13 @@ export default class NetworkController extends EventEmitter { `Unknown Infura provider type "${type}".`, ); const { chainId } = NETWORK_TYPE_TO_ID_MAP[type]; - this.setProviderConfig({ type, rpcUrl, chainId, ticker, nickname }); + this.setProviderConfig({ + type, + rpcUrl: '', + chainId, + ticker: 'ETH', + nickname: '', + }); } resetConnection() { diff --git a/app/scripts/controllers/network/pending-middleware.test.js b/app/scripts/controllers/network/pending-middleware.test.js index 49e60aaa4..9d89e59f1 100644 --- a/app/scripts/controllers/network/pending-middleware.test.js +++ b/app/scripts/controllers/network/pending-middleware.test.js @@ -1,4 +1,5 @@ import { strict as assert } from 'assert'; +import { GAS_LIMITS } from '../../../../shared/constants/gas'; import { txMetaStub } from '../../../../test/stub/tx-meta-stub'; import { createPendingNonceMiddleware, @@ -55,7 +56,7 @@ describe('PendingNonceMiddleware', function () { blockHash: null, blockNumber: null, from: '0xf231d46dd78806e1dd93442cf33c7671f8538748', - gas: '0x5208', + gas: GAS_LIMITS.SIMPLE, gasPrice: '0x1e8480', hash: '0x2cc5a25744486f7383edebbf32003e5a66e18135799593d6b5cdd2bb43674f09', diff --git a/app/scripts/controllers/swaps.js b/app/scripts/controllers/swaps.js index 9e4247231..9af3be87c 100644 --- a/app/scripts/controllers/swaps.js +++ b/app/scripts/controllers/swaps.js @@ -14,6 +14,7 @@ import { SWAPS_FETCH_ORDER_CONFLICT, SWAPS_CHAINID_CONTRACT_ADDRESS_MAP, } from '../../../shared/constants/swaps'; + import { isSwapsDefaultTokenAddress } from '../../../shared/modules/swaps.utils'; import { @@ -21,6 +22,7 @@ import { fetchSwapsFeatureLiveness as defaultFetchSwapsFeatureLiveness, fetchSwapsQuoteRefreshTime as defaultFetchSwapsQuoteRefreshTime, } from '../../../ui/pages/swaps/swaps.util'; +import { MINUTE, SECOND } from '../../../shared/constants/time'; import { NETWORK_EVENTS } from './network'; // The MAX_GAS_LIMIT is a number that is higher than the maximum gas costs we have observed on any aggregator @@ -32,11 +34,11 @@ const POLL_COUNT_LIMIT = 3; // If for any reason the MetaSwap API fails to provide a refresh time, // provide a reasonable fallback to avoid further errors -const FALLBACK_QUOTE_REFRESH_TIME = 60000; +const FALLBACK_QUOTE_REFRESH_TIME = MINUTE; // This is the amount of time to wait, after successfully fetching quotes // and their gas estimates, before fetching for new quotes -const QUOTE_POLLING_DIFFERENCE_INTERVAL = 10 * 1000; +const QUOTE_POLLING_DIFFERENCE_INTERVAL = SECOND * 10; function calculateGasEstimateWithRefund( maxGas = MAX_GAS_LIMIT, @@ -346,7 +348,7 @@ export default class SwapsController { const gasTimeout = setTimeout(() => { gasTimedOut = true; resolve({ gasLimit: null, simulationFails: true }); - }, 5000); + }, SECOND * 5); // Remove gas from params that will be passed to the `estimateGas` call // Including it can cause the estimate to fail if the actual gas needed diff --git a/app/scripts/controllers/swaps.test.js b/app/scripts/controllers/swaps.test.js index 0fb1d80ac..d3b96a098 100644 --- a/app/scripts/controllers/swaps.test.js +++ b/app/scripts/controllers/swaps.test.js @@ -12,6 +12,7 @@ import { } from '../../../shared/constants/network'; import { ETH_SWAPS_TOKEN_OBJECT } from '../../../shared/constants/swaps'; import { createTestProviderTools } from '../../../test/stub/provider'; +import { SECOND } from '../../../shared/constants/time'; import SwapsController, { utils } from './swaps'; import { NETWORK_EVENTS } from './network'; @@ -34,6 +35,8 @@ const TEST_AGG_ID_6 = 'TEST_AGG_6'; const TEST_AGG_ID_BEST = 'TEST_AGG_BEST'; const TEST_AGG_ID_APPROVAL = 'TEST_AGG_APPROVAL'; +const POLLING_TIMEOUT = SECOND * 1000; + const MOCK_APPROVAL_NEEDED = { data: '0x095ea7b300000000000000000000000095e6f48254609a6ee006f7d493c8e5fb97094cef0000000000000000000000000000000000000000004a817c7ffffffdabf41c00', @@ -836,7 +839,7 @@ describe('SwapsController', function () { it('clears polling timeout', function () { swapsController.pollingTimeout = setTimeout( () => assert.fail(), - 1000000, + POLLING_TIMEOUT, ); swapsController.resetSwapsState(); assert.strictEqual(swapsController.pollingTimeout._idleTimeout, -1); @@ -847,7 +850,7 @@ describe('SwapsController', function () { it('clears polling timeout', function () { swapsController.pollingTimeout = setTimeout( () => assert.fail(), - 1000000, + POLLING_TIMEOUT, ); swapsController.stopPollingForQuotes(); assert.strictEqual(swapsController.pollingTimeout._idleTimeout, -1); @@ -865,7 +868,7 @@ describe('SwapsController', function () { it('clears polling timeout', function () { swapsController.pollingTimeout = setTimeout( () => assert.fail(), - 1000000, + POLLING_TIMEOUT, ); swapsController.resetPostFetchState(); assert.strictEqual(swapsController.pollingTimeout._idleTimeout, -1); diff --git a/app/scripts/controllers/token-rates.js b/app/scripts/controllers/token-rates.js index 1bba3f389..2bc7f2a19 100644 --- a/app/scripts/controllers/token-rates.js +++ b/app/scripts/controllers/token-rates.js @@ -3,11 +3,12 @@ import log from 'loglevel'; import { normalize as normalizeAddress } from 'eth-sig-util'; import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout'; import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils'; +import { MINUTE, SECOND } from '../../../shared/constants/time'; -const fetchWithTimeout = getFetchWithTimeout(30000); +const fetchWithTimeout = getFetchWithTimeout(SECOND * 30); // By default, poll every 3 minutes -const DEFAULT_INTERVAL = 180 * 1000; +const DEFAULT_INTERVAL = MINUTE * 3; /** * A controller that polls for token exchange diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 9b7011a66..fa7c46a2b 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -23,6 +23,7 @@ import { TRANSACTION_TYPES, } from '../../../../shared/constants/transaction'; import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller'; +import { GAS_LIMITS } from '../../../../shared/constants/gas'; import TransactionStateManager from './tx-state-manager'; import TxGasUtil from './tx-gas-utils'; import PendingTransactionTracker from './pending-tx-tracker'; @@ -30,7 +31,6 @@ import * as txUtils from './lib/util'; const hstInterface = new ethers.utils.Interface(abi); -const SIMPLE_GAS_COST = '0x5208'; // Hex for 21000, cost of a simple send. const MAX_MEMSTORE_TX_LIST_SIZE = 100; // Number of transactions (by unique nonces) to keep in memory /** @@ -366,7 +366,7 @@ export default class TransactionController extends EventEmitter { } // This is a standard ether simple send, gas requirement is exactly 21k - return { gasLimit: SIMPLE_GAS_COST }; + return { gasLimit: GAS_LIMITS.SIMPLE }; } const { @@ -404,7 +404,7 @@ export default class TransactionController extends EventEmitter { from, to: from, nonce, - gas: customGasLimit || '0x5208', + gas: customGasLimit || GAS_LIMITS.SIMPLE, value: '0x0', gasPrice: newGasPrice, }, diff --git a/app/scripts/controllers/transactions/index.test.js b/app/scripts/controllers/transactions/index.test.js index a249c17fc..3615c33d0 100644 --- a/app/scripts/controllers/transactions/index.test.js +++ b/app/scripts/controllers/transactions/index.test.js @@ -13,6 +13,7 @@ import { TRANSACTION_STATUSES, TRANSACTION_TYPES, } from '../../../../shared/constants/transaction'; +import { SECOND } from '../../../../shared/constants/time'; import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller'; import TransactionController from '.'; @@ -468,7 +469,7 @@ describe('Transaction Controller', function () { }, }; // eslint-disable-next-line @babel/no-invalid-this - this.timeout(15000); + this.timeout(SECOND * 15); const wrongValue = '0x05'; txController.addTransaction(txMeta); diff --git a/app/scripts/lib/ComposableObservableStore.js b/app/scripts/lib/ComposableObservableStore.js index 7e9892bea..789a12d80 100644 --- a/app/scripts/lib/ComposableObservableStore.js +++ b/app/scripts/lib/ComposableObservableStore.js @@ -1,18 +1,36 @@ import { ObservableStore } from '@metamask/obs-store'; +/** + * @typedef {import('@metamask/controllers').ControllerMessenger} ControllerMessenger + */ + /** * An ObservableStore that can composes a flat * structure of child stores based on configuration */ export default class ComposableObservableStore extends ObservableStore { + /** + * Describes which stores are being composed. The key is the name of the + * store, and the value is either an ObserableStore, or a controller that + * extends one of the two base controllers in the `@metamask/controllers` + * package. + * @type {Record} + */ + config = {}; + /** * Create a new store * - * @param {Object} [initState] - The initial store state - * @param {Object} [config] - Map of internal state keys to child stores + * @param {Object} options + * @param {Object} [options.config] - Map of internal state keys to child stores + * @param {ControllerMessenger} options.controllerMessenger - The controller + * messenger, used for subscribing to events from BaseControllerV2-based + * controllers. + * @param {Object} [options.state] - The initial store state */ - constructor(initState, config) { - super(initState); + constructor({ config, controllerMessenger, state }) { + super(state); + this.controllerMessenger = controllerMessenger; if (config) { this.updateStructure(config); } @@ -21,15 +39,31 @@ export default class ComposableObservableStore extends ObservableStore { /** * Composes a new internal store subscription structure * - * @param {Object} [config] - Map of internal state keys to child stores + * @param {Record} config - Describes which stores are being + * composed. The key is the name of the store, and the value is either an + * ObserableStore, or a controller that extends one of the two base + * controllers in the `@metamask/controllers` package. */ updateStructure(config) { this.config = config; this.removeAllListeners(); - for (const key of Object.keys(this.config)) { - config[key].subscribe((state) => { - this.updateState({ [key]: state }); - }); + for (const key of Object.keys(config)) { + if (!config[key]) { + throw new Error(`Undefined '${key}'`); + } + const store = config[key]; + if (store.subscribe) { + config[key].subscribe((state) => { + this.updateState({ [key]: state }); + }); + } else { + this.controllerMessenger.subscribe( + `${store.name}:stateChange`, + (state) => { + this.updateState({ [key]: state }); + }, + ); + } } } diff --git a/app/scripts/lib/ComposableObservableStore.test.js b/app/scripts/lib/ComposableObservableStore.test.js index 620b6df85..063f97cbf 100644 --- a/app/scripts/lib/ComposableObservableStore.test.js +++ b/app/scripts/lib/ComposableObservableStore.test.js @@ -1,40 +1,194 @@ import { strict as assert } from 'assert'; import { ObservableStore } from '@metamask/obs-store'; +import { + BaseController, + BaseControllerV2, + ControllerMessenger, +} from '@metamask/controllers'; import ComposableObservableStore from './ComposableObservableStore'; +class OldExampleController extends BaseController { + name = 'OldExampleController'; + + defaultState = { + baz: 'baz', + }; + + constructor() { + super(); + this.initialize(); + } + + updateBaz(contents) { + this.update({ baz: contents }); + } +} +class ExampleController extends BaseControllerV2 { + static defaultState = { + bar: 'bar', + }; + + static metadata = { + bar: { persist: true, anonymous: true }, + }; + + constructor({ messenger }) { + super({ + messenger, + name: 'ExampleController', + metadata: ExampleController.metadata, + state: ExampleController.defaultState, + }); + } + + updateBar(contents) { + this.update(() => { + return { bar: contents }; + }); + } +} + describe('ComposableObservableStore', function () { it('should register initial state', function () { - const store = new ComposableObservableStore('state'); + const controllerMessenger = new ControllerMessenger(); + const store = new ComposableObservableStore({ + controllerMessenger, + state: 'state', + }); assert.strictEqual(store.getState(), 'state'); }); it('should register initial structure', function () { + const controllerMessenger = new ControllerMessenger(); const testStore = new ObservableStore(); - const store = new ComposableObservableStore(null, { TestStore: testStore }); + const store = new ComposableObservableStore({ + config: { TestStore: testStore }, + controllerMessenger, + }); testStore.putState('state'); assert.deepEqual(store.getState(), { TestStore: 'state' }); }); - it('should update structure', function () { + it('should update structure with observable store', function () { + const controllerMessenger = new ControllerMessenger(); const testStore = new ObservableStore(); - const store = new ComposableObservableStore(); + const store = new ComposableObservableStore({ controllerMessenger }); store.updateStructure({ TestStore: testStore }); testStore.putState('state'); assert.deepEqual(store.getState(), { TestStore: 'state' }); }); + it('should update structure with BaseController-based controller', function () { + const controllerMessenger = new ControllerMessenger(); + const oldExampleController = new OldExampleController(); + const store = new ComposableObservableStore({ controllerMessenger }); + store.updateStructure({ OldExample: oldExampleController }); + oldExampleController.updateBaz('state'); + assert.deepEqual(store.getState(), { OldExample: { baz: 'state' } }); + }); + + it('should update structure with BaseControllerV2-based controller', function () { + const controllerMessenger = new ControllerMessenger(); + const exampleController = new ExampleController({ + messenger: controllerMessenger, + }); + const store = new ComposableObservableStore({ controllerMessenger }); + store.updateStructure({ Example: exampleController }); + exampleController.updateBar('state'); + console.log(exampleController.state); + assert.deepEqual(store.getState(), { Example: { bar: 'state' } }); + }); + + it('should update structure with all three types of stores', function () { + const controllerMessenger = new ControllerMessenger(); + const exampleStore = new ObservableStore(); + const exampleController = new ExampleController({ + messenger: controllerMessenger, + }); + const oldExampleController = new OldExampleController(); + const store = new ComposableObservableStore({ controllerMessenger }); + store.updateStructure({ + Example: exampleController, + OldExample: oldExampleController, + Store: exampleStore, + }); + exampleStore.putState('state'); + exampleController.updateBar('state'); + oldExampleController.updateBaz('state'); + assert.deepEqual(store.getState(), { + Example: { bar: 'state' }, + OldExample: { baz: 'state' }, + Store: 'state', + }); + }); + it('should return flattened state', function () { + const controllerMessenger = new ControllerMessenger(); const fooStore = new ObservableStore({ foo: 'foo' }); - const barStore = new ObservableStore({ bar: 'bar' }); - const store = new ComposableObservableStore(null, { - FooStore: fooStore, - BarStore: barStore, + const barController = new ExampleController({ + messenger: controllerMessenger, + }); + const bazController = new OldExampleController(); + const store = new ComposableObservableStore({ + config: { + FooStore: fooStore, + BarStore: barController, + BazStore: bazController, + }, + controllerMessenger, + state: { + FooStore: fooStore.getState(), + BarStore: barController.state, + BazStore: bazController.state, + }, + }); + assert.deepEqual(store.getFlatState(), { + foo: 'foo', + bar: 'bar', + baz: 'baz', }); - assert.deepEqual(store.getFlatState(), { foo: 'foo', bar: 'bar' }); }); it('should return empty flattened state when not configured', function () { - const store = new ComposableObservableStore(); + const controllerMessenger = new ControllerMessenger(); + const store = new ComposableObservableStore({ controllerMessenger }); assert.deepEqual(store.getFlatState(), {}); }); + + it('should throw if the controller messenger is omitted and the config includes a BaseControllerV2 controller', function () { + const controllerMessenger = new ControllerMessenger(); + const exampleController = new ExampleController({ + messenger: controllerMessenger, + }); + assert.throws( + () => + new ComposableObservableStore({ + config: { + Example: exampleController, + }, + }), + ); + }); + + it('should throw if the controller messenger is omitted and updateStructure called with a BaseControllerV2 controller', function () { + const controllerMessenger = new ControllerMessenger(); + const exampleController = new ExampleController({ + messenger: controllerMessenger, + }); + const store = new ComposableObservableStore({}); + assert.throws(() => store.updateStructure({ Example: exampleController })); + }); + + it('should throw if initialized with undefined config entry', function () { + const controllerMessenger = new ControllerMessenger(); + assert.throws( + () => + new ComposableObservableStore({ + config: { + Example: undefined, + }, + controllerMessenger, + }), + ); + }); }); diff --git a/app/scripts/lib/ens-ipfs/setup.js b/app/scripts/lib/ens-ipfs/setup.js index 8c30de975..353bd32f3 100644 --- a/app/scripts/lib/ens-ipfs/setup.js +++ b/app/scripts/lib/ens-ipfs/setup.js @@ -1,8 +1,9 @@ import extension from 'extensionizer'; import getFetchWithTimeout from '../../../../shared/modules/fetch-with-timeout'; +import { SECOND } from '../../../../shared/constants/time'; import resolveEnsToIpfsContentId from './resolver'; -const fetchWithTimeout = getFetchWithTimeout(30000); +const fetchWithTimeout = getFetchWithTimeout(SECOND * 30); const supportedTopLevelDomains = ['eth']; diff --git a/app/scripts/lib/network-store.js b/app/scripts/lib/network-store.js index eddba5fa2..95561e00e 100644 --- a/app/scripts/lib/network-store.js +++ b/app/scripts/lib/network-store.js @@ -1,7 +1,8 @@ import log from 'loglevel'; +import { SECOND } from '../../../shared/constants/time'; import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout'; -const fetchWithTimeout = getFetchWithTimeout(30000); +const fetchWithTimeout = getFetchWithTimeout(SECOND * 30); const FIXTURE_SERVER_HOST = 'localhost'; const FIXTURE_SERVER_PORT = 12345; diff --git a/app/scripts/lib/segment.js b/app/scripts/lib/segment.js index a75cf29bd..5c5ab5bac 100644 --- a/app/scripts/lib/segment.js +++ b/app/scripts/lib/segment.js @@ -1,4 +1,5 @@ import Analytics from 'analytics-node'; +import { SECOND } from '../../../shared/constants/time'; const isDevOrTestEnvironment = Boolean( process.env.METAMASK_DEBUG || process.env.IN_TEST, @@ -21,7 +22,7 @@ const SEGMENT_FLUSH_AT = // deal with short lived sessions that happen faster than the interval // e.g confirmations. This is set to 5,000ms (5 seconds) arbitrarily with the // intent of having a value less than 10 seconds. -const SEGMENT_FLUSH_INTERVAL = 5000; +const SEGMENT_FLUSH_INTERVAL = SECOND * 5; /** * Creates a mock segment module for usage in test environments. This is used diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index ecee4dd0b..e2546a91f 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -20,6 +20,7 @@ import contractMap from '@metamask/contract-metadata'; import { AddressBookController, ApprovalController, + ControllerMessenger, CurrencyRateController, PhishingController, NotificationController, @@ -28,6 +29,7 @@ import { TRANSACTION_STATUSES } from '../../shared/constants/transaction'; import { MAINNET_CHAIN_ID } from '../../shared/constants/network'; import { UI_NOTIFICATIONS } from '../../shared/notifications'; import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; +import { MILLISECOND } from '../../shared/constants/time'; import ComposableObservableStore from './lib/ComposableObservableStore'; import AccountTracker from './lib/account-tracker'; @@ -81,7 +83,10 @@ export default class MetamaskController extends EventEmitter { this.defaultMaxListeners = 20; - this.sendUpdate = debounce(this.privateSendUpdate.bind(this), 200); + this.sendUpdate = debounce( + this.privateSendUpdate.bind(this), + MILLISECOND * 200, + ); this.opts = opts; this.extension = opts.extension; this.platform = opts.platform; @@ -96,8 +101,13 @@ export default class MetamaskController extends EventEmitter { this.getRequestAccountTabIds = opts.getRequestAccountTabIds; this.getOpenMetamaskTabsIds = opts.getOpenMetamaskTabsIds; + const controllerMessenger = new ControllerMessenger(); + // observable state store - this.store = new ComposableObservableStore(initState); + this.store = new ComposableObservableStore({ + state: initState, + controllerMessenger, + }); // external connections by origin // Do not modify directly. Use the associated methods. @@ -157,10 +167,14 @@ export default class MetamaskController extends EventEmitter { preferencesStore: this.preferencesController.store, }); - this.currencyRateController = new CurrencyRateController( - { includeUSDRate: true }, - initState.CurrencyController, - ); + const currencyRateMessenger = controllerMessenger.getRestricted({ + name: 'CurrencyRateController', + }); + this.currencyRateController = new CurrencyRateController({ + includeUSDRate: true, + messenger: currencyRateMessenger, + state: initState.CurrencyController, + }); this.phishingController = new PhishingController(); @@ -222,10 +236,12 @@ export default class MetamaskController extends EventEmitter { this.accountTracker.start(); this.incomingTransactionsController.start(); this.tokenRatesController.start(); + this.currencyRateController.start(); } else { this.accountTracker.stop(); this.incomingTransactionsController.stop(); this.tokenRatesController.stop(); + this.currencyRateController.stop(); } }); @@ -364,18 +380,15 @@ export default class MetamaskController extends EventEmitter { } }); - this.networkController.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => { - this.setCurrentCurrency( - this.currencyRateController.state.currentCurrency, - (error) => { - if (error) { - throw error; - } - }, - ); + this.networkController.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, async () => { + const { ticker } = this.networkController.getProviderConfig(); + try { + await this.currencyRateController.setNativeCurrency(ticker); + } catch (error) { + // TODO: Handle failure to get conversion rate more gracefully + console.error(error); + } }); - const { ticker } = this.networkController.getProviderConfig(); - this.currencyRateController.configure({ nativeCurrency: ticker ?? 'ETH' }); this.networkController.lookupNetwork(); this.messageManager = new MessageManager(); this.personalMessageManager = new PersonalMessageManager(); @@ -439,33 +452,37 @@ export default class MetamaskController extends EventEmitter { NotificationController: this.notificationController, }); - this.memStore = new ComposableObservableStore(null, { - AppStateController: this.appStateController.store, - NetworkController: this.networkController.store, - AccountTracker: this.accountTracker.store, - TxController: this.txController.memStore, - CachedBalancesController: this.cachedBalancesController.store, - TokenRatesController: this.tokenRatesController.store, - MessageManager: this.messageManager.memStore, - PersonalMessageManager: this.personalMessageManager.memStore, - DecryptMessageManager: this.decryptMessageManager.memStore, - EncryptionPublicKeyManager: this.encryptionPublicKeyManager.memStore, - TypesMessageManager: this.typedMessageManager.memStore, - KeyringController: this.keyringController.memStore, - PreferencesController: this.preferencesController.store, - MetaMetricsController: this.metaMetricsController.store, - AddressBookController: this.addressBookController, - CurrencyController: this.currencyRateController, - AlertController: this.alertController.store, - OnboardingController: this.onboardingController.store, - IncomingTransactionsController: this.incomingTransactionsController.store, - PermissionsController: this.permissionsController.permissions, - PermissionsMetadata: this.permissionsController.store, - ThreeBoxController: this.threeBoxController.store, - SwapsController: this.swapsController.store, - EnsController: this.ensController.store, - ApprovalController: this.approvalController, - NotificationController: this.notificationController, + this.memStore = new ComposableObservableStore({ + config: { + AppStateController: this.appStateController.store, + NetworkController: this.networkController.store, + AccountTracker: this.accountTracker.store, + TxController: this.txController.memStore, + CachedBalancesController: this.cachedBalancesController.store, + TokenRatesController: this.tokenRatesController.store, + MessageManager: this.messageManager.memStore, + PersonalMessageManager: this.personalMessageManager.memStore, + DecryptMessageManager: this.decryptMessageManager.memStore, + EncryptionPublicKeyManager: this.encryptionPublicKeyManager.memStore, + TypesMessageManager: this.typedMessageManager.memStore, + KeyringController: this.keyringController.memStore, + PreferencesController: this.preferencesController.store, + MetaMetricsController: this.metaMetricsController.store, + AddressBookController: this.addressBookController, + CurrencyController: this.currencyRateController, + AlertController: this.alertController.store, + OnboardingController: this.onboardingController.store, + IncomingTransactionsController: this.incomingTransactionsController + .store, + PermissionsController: this.permissionsController.permissions, + PermissionsMetadata: this.permissionsController.store, + ThreeBoxController: this.threeBoxController.store, + SwapsController: this.swapsController.store, + EnsController: this.ensController.store, + ApprovalController: this.approvalController, + NotificationController: this.notificationController, + }, + controllerMessenger, }); this.memStore.subscribe(this.sendUpdate.bind(this)); @@ -649,7 +666,11 @@ export default class MetamaskController extends EventEmitter { return { // etc getState: (cb) => cb(null, this.getState()), - setCurrentCurrency: this.setCurrentCurrency.bind(this), + setCurrentCurrency: nodeify( + this.currencyRateController.setCurrentCurrency.bind( + this.currencyRateController, + ), + ), setUseBlockie: this.setUseBlockie.bind(this), setUseNonceField: this.setUseNonceField.bind(this), setUsePhishDetect: this.setUsePhishDetect.bind(this), @@ -763,6 +784,14 @@ export default class MetamaskController extends EventEmitter { this.appStateController.setConnectedStatusPopoverHasBeenShown, this.appStateController, ), + setRecoveryPhraseReminderHasBeenShown: nodeify( + this.appStateController.setRecoveryPhraseReminderHasBeenShown, + this.appStateController, + ), + setRecoveryPhraseReminderLastShown: nodeify( + this.appStateController.setRecoveryPhraseReminderLastShown, + this.appStateController, + ), // EnsController tryReverseResolveAddress: nodeify( @@ -2511,29 +2540,6 @@ export default class MetamaskController extends EventEmitter { // Log blocks - /** - * A method for setting the user's preferred display currency. - * @param {string} currencyCode - The code of the preferred currency. - * @param {Function} cb - A callback function returning currency info. - */ - setCurrentCurrency(currencyCode, cb) { - const { ticker } = this.networkController.getProviderConfig(); - try { - const currencyState = { - nativeCurrency: ticker, - currentCurrency: currencyCode, - }; - this.currencyRateController.update(currencyState); - this.currencyRateController.configure(currencyState); - cb(null); - return; - } catch (err) { - cb(err); - // eslint-disable-next-line no-useless-return - return; - } - } - /** * A method for selecting a custom URL for an ethereum RPC provider and updating it * @param {string} rpcUrl - A URL for a valid Ethereum RPC API. diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index 3d8cb57cf..87fed2aef 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -654,46 +654,24 @@ describe('MetaMaskController', function () { }); describe('#setCustomRpc', function () { - let rpcUrl; - - beforeEach(function () { - rpcUrl = metamaskController.setCustomRpc( + it('returns custom RPC that when called', async function () { + const rpcUrl = await metamaskController.setCustomRpc( CUSTOM_RPC_URL, CUSTOM_RPC_CHAIN_ID, ); + assert.equal(rpcUrl, CUSTOM_RPC_URL); }); - it('returns custom RPC that when called', async function () { - assert.equal(await rpcUrl, CUSTOM_RPC_URL); - }); - - it('changes the network controller rpc', function () { + it('changes the network controller rpc', async function () { + await metamaskController.setCustomRpc( + CUSTOM_RPC_URL, + CUSTOM_RPC_CHAIN_ID, + ); const networkControllerState = metamaskController.networkController.store.getState(); assert.equal(networkControllerState.provider.rpcUrl, CUSTOM_RPC_URL); }); }); - describe('#setCurrentCurrency', function () { - let defaultMetaMaskCurrency; - - beforeEach(function () { - defaultMetaMaskCurrency = - metamaskController.currencyRateController.state.currentCurrency; - }); - - it('defaults to usd', function () { - assert.equal(defaultMetaMaskCurrency, 'usd'); - }); - - it('sets currency to JPY', function () { - metamaskController.setCurrentCurrency('JPY', noop); - assert.equal( - metamaskController.currencyRateController.state.currentCurrency, - 'JPY', - ); - }); - }); - describe('#addNewAccount', function () { it('errors when an primary keyring is does not exist', async function () { const addNewAccount = metamaskController.addNewAccount(); diff --git a/app/scripts/migrations/061.js b/app/scripts/migrations/061.js new file mode 100644 index 000000000..2937c6ed0 --- /dev/null +++ b/app/scripts/migrations/061.js @@ -0,0 +1,32 @@ +import { cloneDeep } from 'lodash'; + +const version = 61; + +/** + * Initialize attributes related to recovery seed phrase reminder + */ +export default { + version, + async migrate(originalVersionedData) { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + const state = versionedData.data; + const newState = transformState(state); + versionedData.data = newState; + return versionedData; + }, +}; + +function transformState(state) { + const currentTime = new Date().getTime(); + if (state.AppStateController) { + state.AppStateController.recoveryPhraseReminderHasBeenShown = false; + state.AppStateController.recoveryPhraseReminderLastShown = currentTime; + } else { + state.AppStateController = { + recoveryPhraseReminderHasBeenShown: false, + recoveryPhraseReminderLastShown: currentTime, + }; + } + return state; +} diff --git a/app/scripts/migrations/061.test.js b/app/scripts/migrations/061.test.js new file mode 100644 index 000000000..2c0c45510 --- /dev/null +++ b/app/scripts/migrations/061.test.js @@ -0,0 +1,67 @@ +import { strict as assert } from 'assert'; +import sinon from 'sinon'; +import migration61 from './061'; + +describe('migration #61', function () { + let dateStub; + + beforeEach(function () { + dateStub = sinon.stub(Date.prototype, 'getTime').returns(1621580400000); + }); + + afterEach(function () { + dateStub.restore(); + }); + + it('should update the version metadata', async function () { + const oldStorage = { + meta: { + version: 60, + }, + data: {}, + }; + + const newStorage = await migration61.migrate(oldStorage); + assert.deepEqual(newStorage.meta, { + version: 61, + }); + }); + + it('should set recoveryPhraseReminderHasBeenShown to false and recoveryPhraseReminderLastShown to the current time', async function () { + const oldStorage = { + meta: {}, + data: { + AppStateController: { + existingProperty: 'foo', + }, + }, + }; + + const newStorage = await migration61.migrate(oldStorage); + assert.deepEqual(newStorage.data, { + AppStateController: { + recoveryPhraseReminderHasBeenShown: false, + recoveryPhraseReminderLastShown: 1621580400000, + existingProperty: 'foo', + }, + }); + }); + + it('should initialize AppStateController if it does not exist', async function () { + const oldStorage = { + meta: {}, + data: { + existingProperty: 'foo', + }, + }; + + const newStorage = await migration61.migrate(oldStorage); + assert.deepEqual(newStorage.data, { + existingProperty: 'foo', + AppStateController: { + recoveryPhraseReminderHasBeenShown: false, + recoveryPhraseReminderLastShown: 1621580400000, + }, + }); + }); +}); diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 682ba6d08..175ee0044 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -65,6 +65,7 @@ const migrations = [ require('./058').default, require('./059').default, require('./060').default, + require('./061').default, ]; export default migrations; diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index 0eaeefaa8..e8820d602 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -1,8 +1,8 @@ import extension from 'extensionizer'; +import { getBlockExplorerLink } from '@metamask/etherscan-link'; import { getEnvironmentType, checkForError } from '../lib/util'; import { ENVIRONMENT_TYPE_BACKGROUND } from '../../../shared/constants/app'; import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction'; -import { getBlockExplorerUrlForTx } from '../../../shared/modules/transaction.utils'; export default class ExtensionPlatform { // @@ -192,7 +192,7 @@ export default class ExtensionPlatform { _showConfirmedTransaction(txMeta, rpcPrefs) { this._subscribeToNotificationClicked(); - const url = getBlockExplorerUrlForTx(txMeta, rpcPrefs); + const url = getBlockExplorerLink(txMeta, rpcPrefs); const nonce = parseInt(txMeta.txParams.nonce, 16); const title = 'Confirmed transaction'; diff --git a/development/lib/run-command.js b/development/lib/run-command.js new file mode 100644 index 000000000..08bffcc1b --- /dev/null +++ b/development/lib/run-command.js @@ -0,0 +1,134 @@ +const spawn = require('cross-spawn'); + +/** + * Run a command to completion using the system shell. + * + * This will run a command with the specified arguments, and resolve when the + * process has exited. The STDOUT stream is monitored for output, which is + * returned after being split into lines. All output is expected to be UTF-8 + * encoded, and empty lines are removed from the output. + * + * Anything received on STDERR is assumed to indicate a problem, and is tracked + * as an error. + * + * @param {string} command - The command to run + * @param {Array} [args] - The arguments to pass to the command + * @returns {Array} Lines of output received via STDOUT + */ +async function runCommand(command, args) { + const output = []; + let mostRecentError; + let errorSignal; + let errorCode; + const internalError = new Error('Internal'); + try { + await new Promise((resolve, reject) => { + const childProcess = spawn(command, args, { encoding: 'utf8' }); + childProcess.stdout.setEncoding('utf8'); + childProcess.stderr.setEncoding('utf8'); + + childProcess.on('error', (error) => { + mostRecentError = error; + }); + + childProcess.stdout.on('data', (message) => { + const nonEmptyLines = message.split('\n').filter((line) => line !== ''); + output.push(...nonEmptyLines); + }); + + childProcess.stderr.on('data', (message) => { + mostRecentError = new Error(message.trim()); + }); + + childProcess.once('exit', (code, signal) => { + if (code === 0) { + return resolve(); + } + errorCode = code; + errorSignal = signal; + return reject(internalError); + }); + }); + } catch (error) { + /** + * The error is re-thrown here in an `async` context to preserve the stack trace. If this was + * was thrown inside the Promise constructor, the stack trace would show a few frames of + * Node.js internals then end, without indicating where `runCommand` was called. + */ + if (error === internalError) { + let errorMessage; + if (errorCode !== null && errorSignal !== null) { + errorMessage = `Terminated by signal '${errorSignal}'; exited with code '${errorCode}'`; + } else if (errorSignal !== null) { + errorMessage = `Terminaled by signal '${errorSignal}'`; + } else if (errorCode === null) { + errorMessage = 'Exited with no code or signal'; + } else { + errorMessage = `Exited with code '${errorCode}'`; + } + const improvedError = new Error(errorMessage); + if (mostRecentError) { + improvedError.cause = mostRecentError; + } + throw improvedError; + } + } + return output; +} + +/** + * Run a command to using the system shell. + * + * This will run a command with the specified arguments, and resolve when the + * process has exited. The STDIN, STDOUT and STDERR streams are inherited, + * letting the command take over completely until it completes. The success or + * failure of the process is determined entirely by the exit code; STDERR + * output is not used to indicate failure. + * + * @param {string} command - The command to run + * @param {Array} [args] - The arguments to pass to the command + */ +async function runInShell(command, args) { + let errorSignal; + let errorCode; + const internalError = new Error('Internal'); + try { + await new Promise((resolve, reject) => { + const childProcess = spawn(command, args, { + encoding: 'utf8', + stdio: 'inherit', + }); + + childProcess.once('exit', (code, signal) => { + if (code === 0) { + return resolve(); + } + errorCode = code; + errorSignal = signal; + return reject(internalError); + }); + }); + } catch (error) { + /** + * The error is re-thrown here in an `async` context to preserve the stack trace. If this was + * was thrown inside the Promise constructor, the stack trace would show a few frames of + * Node.js internals then end, without indicating where `runInShell` was called. + */ + if (error === internalError) { + let errorMessage; + if (errorCode !== null && errorSignal !== null) { + errorMessage = `Terminated by signal '${errorSignal}'; exited with code '${errorCode}'`; + } else if (errorSignal !== null) { + errorMessage = `Terminaled by signal '${errorSignal}'`; + } else if (errorCode === null) { + errorMessage = 'Exited with no code or signal'; + } else { + errorMessage = `Exited with code '${errorCode}'`; + } + const improvedError = new Error(errorMessage); + throw improvedError; + } + } +} + +module.exports = { runCommand, runInShell }; diff --git a/development/sentry-publish.js b/development/sentry-publish.js index 1ff7b0eb6..81fa58790 100644 --- a/development/sentry-publish.js +++ b/development/sentry-publish.js @@ -1,9 +1,6 @@ #!/usr/bin/env node -const childProcess = require('child_process'); -const pify = require('pify'); - -const exec = pify(childProcess.exec, { multiArgs: true }); const VERSION = require('../dist/chrome/manifest.json').version; // eslint-disable-line import/no-unresolved +const { runCommand, runInShell } = require('./lib/run-command'); start().catch((error) => { console.error(error); @@ -31,11 +28,17 @@ async function start() { } else { // create sentry release console.log(`creating Sentry release for "${VERSION}"...`); - await exec(`sentry-cli releases new ${VERSION}`); + await runCommand('sentry-cli', ['releases', 'new', VERSION]); console.log( `removing any existing files from Sentry release "${VERSION}"...`, ); - await exec(`sentry-cli releases files ${VERSION} delete --all`); + await runCommand('sentry-cli', [ + 'releases', + 'files', + VERSION, + 'delete', + '--all', + ]); } // check if version has artifacts or not @@ -49,34 +52,43 @@ async function start() { } // upload sentry source and sourcemaps - await exec(`./development/sentry-upload-artifacts.sh --release ${VERSION}`); + await runInShell('./development/sentry-upload-artifacts.sh', [ + '--release', + VERSION, + ]); } async function checkIfAuthWorks() { - const itWorked = await doesNotFail(async () => { - await exec(`sentry-cli releases list`); - }); - return itWorked; + return await doesNotFail(() => + runCommand('sentry-cli', ['releases', 'list']), + ); } async function checkIfVersionExists() { - const versionAlreadyExists = await doesNotFail(async () => { - await exec(`sentry-cli releases info ${VERSION}`); - }); - return versionAlreadyExists; + return await doesNotFail(() => + runCommand('sentry-cli', ['releases', 'info', VERSION]), + ); } async function checkIfVersionHasArtifacts() { - const artifacts = await exec(`sentry-cli releases files ${VERSION} list`); + const [artifact] = await runCommand('sentry-cli', [ + 'releases', + 'files', + VERSION, + 'list', + ]); // When there's no artifacts, we get a response from the shell like this ['', ''] - return artifacts[0] && artifacts[0].length > 0; + return artifact?.length > 0; } async function doesNotFail(asyncFn) { try { await asyncFn(); return true; - } catch (err) { - return false; + } catch (error) { + if (error.message === `Exited with code '1'`) { + return false; + } + throw error; } } diff --git a/development/sentry-upload-artifacts.sh b/development/sentry-upload-artifacts.sh index 71f88c750..673e1dd73 100755 --- a/development/sentry-upload-artifacts.sh +++ b/development/sentry-upload-artifacts.sh @@ -1,6 +1,5 @@ #!/usr/bin/env bash -set -x set -e set -u set -o pipefail diff --git a/jest.config.js b/jest.config.js index e186bc814..6fce2ce4d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -6,7 +6,7 @@ module.exports = { coverageThreshold: { global: { branches: 32.75, - functions: 43.31, + functions: 42.9, lines: 43.12, statements: 43.67, }, diff --git a/jsconfig.json b/jsconfig.json index b0cc155d5..28d588ea9 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,14 +1,7 @@ { - "exclude": [ - "*.log", - "builds", - "coverage", - "dist", - "docs", - "lavamoat", - "node:console", - "node_modules", - "patches", - "test-artifacts" - ] + "compilerOptions": { + "target": "ES6", + "module": "commonjs" + }, + "include": ["ui/**/*.js", "app/**/*.js", "shared/**/*.js"] } diff --git a/package.json b/package.json index 3a2c01b76..aea30bbba 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "metamask-crx", - "version": "9.6.1", + "version": "9.7.0", "private": true, "repository": { "type": "git", - "url": "https://github.com/MetaMask/metamask-extension" + "url": "https://github.com/MetaMask/metamask-extension.git" }, "scripts": { "setup": "yarn install && yarn setup:postinstall", @@ -96,8 +96,8 @@ "@fortawesome/fontawesome-free": "^5.13.0", "@lavamoat/preinstall-always-fail": "^1.0.0", "@material-ui/core": "^4.11.0", - "@metamask/contract-metadata": "^1.22.0", - "@metamask/controllers": "^8.0.0", + "@metamask/contract-metadata": "^1.26.0", + "@metamask/controllers": "^9.0.0", "@metamask/eth-ledger-bridge-keyring": "^0.5.0", "@metamask/eth-token-tracker": "^3.0.1", "@metamask/etherscan-link": "^2.1.0", @@ -138,7 +138,7 @@ "ethereum-ens-network-map": "^1.0.2", "ethereumjs-abi": "^0.6.4", "ethereumjs-tx": "1.3.7", - "ethereumjs-util": "^7.0.9", + "ethereumjs-util": "^7.0.10", "ethereumjs-wallet": "^0.6.4", "ethers": "^5.0.8", "ethjs": "^0.4.0", @@ -212,7 +212,7 @@ "@babel/preset-react": "^7.0.0", "@babel/register": "^7.5.5", "@lavamoat/allow-scripts": "^1.0.6", - "@metamask/auto-changelog": "^1.0.0", + "@metamask/auto-changelog": "^2.1.0", "@metamask/eslint-config": "^6.0.0", "@metamask/eslint-config-jest": "^6.0.0", "@metamask/eslint-config-mocha": "^6.0.0", @@ -272,6 +272,7 @@ "gulp-terser-js": "^5.2.2", "gulp-watch": "^5.0.1", "gulp-zip": "^4.0.0", + "history": "^5.0.0", "jest": "^26.6.3", "jsdom": "^11.2.0", "koa": "^2.7.0", @@ -336,7 +337,8 @@ "gc-stats": false, "github:assemblyscript/assemblyscript": false, "tiny-secp256k1": false, - "@lavamoat/preinstall-always-fail": false + "@lavamoat/preinstall-always-fail": false, + "fsevents": false } } } diff --git a/shared/constants/gas.js b/shared/constants/gas.js new file mode 100644 index 000000000..6f53aaeae --- /dev/null +++ b/shared/constants/gas.js @@ -0,0 +1,11 @@ +import { addHexPrefix } from 'ethereumjs-util'; + +const TWENTY_ONE_THOUSAND = 21000; +const ONE_HUNDRED_THOUSAND = 100000; + +export const GAS_LIMITS = { + // maximum gasLimit of a simple send + SIMPLE: addHexPrefix(TWENTY_ONE_THOUSAND.toString(16)), + // a base estimate for token transfers. + BASE_TOKEN_ESTIMATE: addHexPrefix(ONE_HUNDRED_THOUSAND.toString(16)), +}; diff --git a/shared/constants/swaps.js b/shared/constants/swaps.js index 2843821e5..42a2167f3 100644 --- a/shared/constants/swaps.js +++ b/shared/constants/swaps.js @@ -63,6 +63,7 @@ const SWAPS_TESTNET_CHAIN_ID = '0x539'; const SWAPS_TESTNET_HOST = 'https://metaswap-api.airswap-dev.codefi.network'; const BSC_DEFAULT_BLOCK_EXPLORER_URL = 'https://bscscan.com/'; +const MAINNET_DEFAULT_BLOCK_EXPLORER_URL = 'https://etherscan.io/'; export const ALLOWED_SWAPS_CHAIN_IDS = { [MAINNET_CHAIN_ID]: true, @@ -90,4 +91,5 @@ export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = { export const SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP = { [BSC_CHAIN_ID]: BSC_DEFAULT_BLOCK_EXPLORER_URL, + [MAINNET_CHAIN_ID]: MAINNET_DEFAULT_BLOCK_EXPLORER_URL, }; diff --git a/shared/constants/time.js b/shared/constants/time.js new file mode 100644 index 000000000..71cd6f0ed --- /dev/null +++ b/shared/constants/time.js @@ -0,0 +1,5 @@ +export const MILLISECOND = 1; +export const SECOND = MILLISECOND * 1000; +export const MINUTE = SECOND * 60; +export const HOUR = MINUTE * 60; +export const DAY = HOUR * 24; diff --git a/shared/modules/fetch-with-timeout.test.js b/shared/modules/fetch-with-timeout.test.js index 3b9ce1552..2061c2b92 100644 --- a/shared/modules/fetch-with-timeout.test.js +++ b/shared/modules/fetch-with-timeout.test.js @@ -1,13 +1,14 @@ import { strict as assert } from 'assert'; import nock from 'nock'; +import { MILLISECOND, SECOND } from '../constants/time'; import getFetchWithTimeout from './fetch-with-timeout'; describe('getFetchWithTimeout', function () { it('fetches a url', async function () { nock('https://api.infura.io').get('/money').reply(200, '{"hodl": false}'); - const fetchWithTimeout = getFetchWithTimeout(30000); + const fetchWithTimeout = getFetchWithTimeout(SECOND * 30); const response = await ( await fetchWithTimeout('https://api.infura.io/money') ).json(); @@ -19,10 +20,10 @@ describe('getFetchWithTimeout', function () { it('throws when the request hits a custom timeout', async function () { nock('https://api.infura.io') .get('/moon') - .delay(2000) + .delay(SECOND * 2) .reply(200, '{"moon": "2012-12-21T11:11:11Z"}'); - const fetchWithTimeout = getFetchWithTimeout(123); + const fetchWithTimeout = getFetchWithTimeout(MILLISECOND * 123); try { await fetchWithTimeout('https://api.infura.io/moon').then((r) => @@ -37,10 +38,10 @@ describe('getFetchWithTimeout', function () { it('should abort the request when the custom timeout is hit', async function () { nock('https://api.infura.io') .get('/moon') - .delay(2000) + .delay(SECOND * 2) .reply(200, '{"moon": "2012-12-21T11:11:11Z"}'); - const fetchWithTimeout = getFetchWithTimeout(123); + const fetchWithTimeout = getFetchWithTimeout(MILLISECOND * 123); try { await fetchWithTimeout('https://api.infura.io/moon').then((r) => diff --git a/shared/modules/hexstring-utils.js b/shared/modules/hexstring-utils.js index 1f895a3c7..68b0d73fe 100644 --- a/shared/modules/hexstring-utils.js +++ b/shared/modules/hexstring-utils.js @@ -4,9 +4,10 @@ import { isValidChecksumAddress, addHexPrefix, toChecksumAddress, + zeroAddress, } from 'ethereumjs-util'; -export const BURN_ADDRESS = '0x0000000000000000000000000000000000000000'; +export const BURN_ADDRESS = zeroAddress(); export function isBurnAddress(address) { return address === BURN_ADDRESS; diff --git a/shared/modules/rpc.utils.js b/shared/modules/rpc.utils.js index 7d4d51318..ab0c006b6 100644 --- a/shared/modules/rpc.utils.js +++ b/shared/modules/rpc.utils.js @@ -1,6 +1,7 @@ +import { SECOND } from '../constants/time'; import getFetchWithTimeout from './fetch-with-timeout'; -const fetchWithTimeout = getFetchWithTimeout(30000); +const fetchWithTimeout = getFetchWithTimeout(SECOND * 30); /** * Makes a JSON RPC request to the given URL, with the given RPC method and params. diff --git a/shared/modules/tests/transaction.utils.test.js b/shared/modules/tests/transaction.utils.test.js deleted file mode 100644 index 0da399f34..000000000 --- a/shared/modules/tests/transaction.utils.test.js +++ /dev/null @@ -1,96 +0,0 @@ -import { strict as assert } from 'assert'; -import { - MAINNET_CHAIN_ID, - MAINNET_NETWORK_ID, - ROPSTEN_CHAIN_ID, - ROPSTEN_NETWORK_ID, -} from '../../constants/network'; -import { getBlockExplorerUrlForTx } from '../transaction.utils'; - -const tests = [ - { - expected: 'https://etherscan.io/tx/0xabcd', - transaction: { - metamaskNetworkId: MAINNET_NETWORK_ID, - hash: '0xabcd', - }, - }, - { - expected: 'https://ropsten.etherscan.io/tx/0xdef0', - transaction: { - metamaskNetworkId: ROPSTEN_NETWORK_ID, - hash: '0xdef0', - }, - rpcPrefs: {}, - }, - { - // test handling of `blockExplorerUrl` for a custom RPC - expected: 'https://block.explorer/tx/0xabcd', - transaction: { - metamaskNetworkId: '31', - hash: '0xabcd', - }, - rpcPrefs: { - blockExplorerUrl: 'https://block.explorer', - }, - }, - { - // test handling of trailing `/` in `blockExplorerUrl` for a custom RPC - expected: 'https://another.block.explorer/tx/0xdef0', - transaction: { - networkId: '33', - hash: '0xdef0', - }, - rpcPrefs: { - blockExplorerUrl: 'https://another.block.explorer/', - }, - }, - { - expected: 'https://etherscan.io/tx/0xabcd', - transaction: { - chainId: MAINNET_CHAIN_ID, - hash: '0xabcd', - }, - }, - { - expected: 'https://ropsten.etherscan.io/tx/0xdef0', - transaction: { - chainId: ROPSTEN_CHAIN_ID, - hash: '0xdef0', - }, - rpcPrefs: {}, - }, - { - // test handling of `blockExplorerUrl` for a custom RPC - expected: 'https://block.explorer/tx/0xabcd', - transaction: { - chainId: '0x1f', - hash: '0xabcd', - }, - rpcPrefs: { - blockExplorerUrl: 'https://block.explorer', - }, - }, - { - // test handling of trailing `/` in `blockExplorerUrl` for a custom RPC - expected: 'https://another.block.explorer/tx/0xdef0', - transaction: { - chainId: '0x21', - hash: '0xdef0', - }, - rpcPrefs: { - blockExplorerUrl: 'https://another.block.explorer/', - }, - }, -]; - -describe('getBlockExplorerUrlForTx', function () { - tests.forEach((test) => { - it(`should return '${test.expected}' for transaction with hash: '${test.transaction.hash}'`, function () { - assert.strictEqual( - getBlockExplorerUrlForTx(test.transaction, test.rpcPrefs), - test.expected, - ); - }); - }); -}); diff --git a/shared/modules/transaction.utils.js b/shared/modules/transaction.utils.js index 47a0a4334..9e89679f8 100644 --- a/shared/modules/transaction.utils.js +++ b/shared/modules/transaction.utils.js @@ -1,37 +1,6 @@ -import { - createExplorerLink, - createExplorerLinkForChain, -} from '@metamask/etherscan-link'; - export function transactionMatchesNetwork(transaction, chainId, networkId) { if (typeof transaction.chainId !== 'undefined') { return transaction.chainId === chainId; } return transaction.metamaskNetworkId === networkId; } - -/** - * build the etherscan link for a transaction by either chainId, if available - * or metamaskNetworkId as a fallback. If rpcPrefs is provided will build the - * url for the provided blockExplorerUrl. - * - * @param {Object} transaction - a transaction object from state - * @param {string} [transaction.metamaskNetworkId] - network id tx occurred on - * @param {string} [transaction.chainId] - chain id tx occurred on - * @param {string} [transaction.hash] - hash of the transaction - * @param {Object} [rpcPrefs] - the rpc preferences for the current RPC network - * @param {string} [rpcPrefs.blockExplorerUrl] - the block explorer url for RPC - * networks - * @returns {string} - */ -export function getBlockExplorerUrlForTx(transaction, rpcPrefs = {}) { - if (rpcPrefs.blockExplorerUrl) { - return `${rpcPrefs.blockExplorerUrl.replace(/\/+$/u, '')}/tx/${ - transaction.hash - }`; - } - if (transaction.chainId) { - return createExplorerLinkForChain(transaction.hash, transaction.chainId); - } - return createExplorerLink(transaction.hash, transaction.metamaskNetworkId); -} diff --git a/test/e2e/fixtures/custom-token/state.json b/test/e2e/fixtures/custom-token/state.json new file mode 100644 index 000000000..1d3a24437 --- /dev/null +++ b/test/e2e/fixtures/custom-token/state.json @@ -0,0 +1,151 @@ +{ + "data": { + "AppStateController": { + "mkrMigrationReminderTimestamp": null + }, + "CachedBalancesController": { + "cachedBalances": { + "4": {} + } + }, + "CurrencyController": { + "conversionDate": 1575697244.188, + "conversionRate": 149.61, + "currentCurrency": "usd", + "nativeCurrency": "ETH" + }, + "IncomingTransactionsController": { + "incomingTransactions": {}, + "incomingTxLastFetchedBlocksByNetwork": { + "goerli": null, + "kovan": null, + "mainnet": null, + "rinkeby": 5570536 + } + }, + "KeyringController": { + "vault": "{\"data\":\"s6TpYjlUNsn7ifhEFTkuDGBUM1GyOlPrim7JSjtfIxgTt8/6MiXgiR/CtFfR4dWW2xhq85/NGIBYEeWrZThGdKGarBzeIqBfLFhw9n509jprzJ0zc2Rf+9HVFGLw+xxC4xPxgCS0IIWeAJQ+XtGcHmn0UZXriXm8Ja4kdlow6SWinB7sr/WM3R0+frYs4WgllkwggDf2/Tv6VHygvLnhtzp6hIJFyTjh+l/KnyJTyZW1TkZhDaNDzX3SCOHT\",\"iv\":\"FbeHDAW5afeWNORfNJBR0Q==\",\"salt\":\"TxZ+WbCW6891C9LK/hbMAoUsSEW1E8pyGLVBU6x5KR8=\"}" + }, + "NetworkController": { + "network": "1337", + "provider": { + "nickname": "Localhost 8545", + "rpcUrl": "http://localhost:8545", + "chainId": "0x539", + "ticker": "ETH", + "type": "rpc" + } + }, + "NotificationController": { + "notifications": { + "1": { + "isShown": true + }, + "3": { + "isShown": true + }, + "5": { + "isShown": true + }, + "6": { + "isShown": true + } + } + }, + "OnboardingController": { + "onboardingTabs": {}, + "seedPhraseBackedUp": false + }, + "PermissionsMetadata": { + "domainMetadata": { + "metamask.github.io": { + "icon": null, + "name": "M E T A M A S K M E S H T E S T" + } + }, + "permissionsHistory": {}, + "permissionsLog": [ + { + "id": 746677923, + "method": "eth_accounts", + "methodType": "restricted", + "origin": "metamask.github.io", + "request": { + "id": 746677923, + "jsonrpc": "2.0", + "method": "eth_accounts", + "origin": "metamask.github.io", + "params": [] + }, + "requestTime": 1575697241368, + "response": { + "id": 746677923, + "jsonrpc": "2.0", + "result": [] + }, + "responseTime": 1575697241370, + "success": true + } + ] + }, + "PreferencesController": { + "accountTokens": { + "0x5cfe73b6021e818b776b421b1c4db2474086a7e1": { + "0x539": [ + { + "address": "0x86002be4cdd922de1ccb831582bf99284b99ac12", + "symbol": "TST", + "decimals": 4 + } + ], + "rinkeby": [], + "ropsten": [] + } + }, + "assetImages": {}, + "completedOnboarding": true, + "currentLocale": "en", + "featureFlags": { + "showIncomingTransactions": true, + "transactionTime": false + }, + "firstTimeFlowType": "create", + "forgottenPassword": false, + "frequentRpcListDetail": [], + "identities": { + "0x5cfe73b6021e818b776b421b1c4db2474086a7e1": { + "address": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1", + "name": "Account 1" + } + }, + "knownMethodData": {}, + "lostIdentities": {}, + "metaMetricsId": null, + "metaMetricsSendCount": 0, + "participateInMetaMetrics": false, + "preferences": { + "useNativeCurrencyAsPrimaryCurrency": true + }, + "selectedAddress": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1", + "suggestedTokens": {}, + "tokens": [ + { + "address": "0x86002be4cdd922de1ccb831582bf99284b99ac12", + "symbol": "TST", + "decimals": 4 + } + ], + "useBlockie": false, + "useNonceField": false, + "usePhishDetect": true + }, + "config": {}, + "firstTimeInfo": { + "date": 1575697234195, + "version": "7.7.0" + } + }, + "meta": { + "version": 40 + } +} diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index 38ab4d500..ae8b4d6c5 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -27,6 +27,7 @@ async function withFixtures(options, testSuite) { } = options; const fixtureServer = new FixtureServer(); const ganacheServer = new Ganache(); + let secondaryGanacheServer; let dappServer; let segmentServer; let segmentStub; @@ -34,6 +35,16 @@ async function withFixtures(options, testSuite) { let webDriver; try { await ganacheServer.start(ganacheOptions); + if (ganacheOptions?.concurrent) { + const { port, chainId } = ganacheOptions.concurrent; + secondaryGanacheServer = new Ganache(); + await secondaryGanacheServer.start({ + blockTime: 2, + _chainIdRpc: chainId, + port, + vmErrorsOnRPCResponse: false, + }); + } await fixtureServer.start(); await fixtureServer.loadState(path.join(__dirname, 'fixtures', fixtures)); if (dapp) { @@ -103,6 +114,9 @@ async function withFixtures(options, testSuite) { } finally { await fixtureServer.stop(); await ganacheServer.quit(); + if (ganacheOptions?.concurrent) { + await secondaryGanacheServer.quit(); + } if (webDriver) { await webDriver.quit(); } diff --git a/test/e2e/metamask-ui.spec.js b/test/e2e/metamask-ui.spec.js index 8a8fcbf21..b813a597d 100644 --- a/test/e2e/metamask-ui.spec.js +++ b/test/e2e/metamask-ui.spec.js @@ -1506,55 +1506,4 @@ describe('MetaMask', function () { }); }); }); - - describe('Hide token', function () { - it('hides the token when clicked', async function () { - await driver.clickElement({ text: 'Assets', tag: 'button' }); - - await driver.clickElement({ text: 'TST', tag: 'span' }); - - await driver.clickElement('[data-testid="asset-options__button"]'); - - await driver.clickElement('[data-testid="asset-options__hide"]'); - - // wait for confirm hide modal to be visible - const confirmHideModal = await driver.findVisibleElement('span .modal'); - - await driver.clickElement( - '[data-testid="hide-token-confirmation__hide"]', - ); - - // wait for confirm hide modal to be removed from DOM. - await confirmHideModal.waitForElementState('hidden'); - }); - }); - - describe('Add existing token using search', function () { - it('clicks on the Add Token button', async function () { - await driver.clickElement({ text: 'Add Token', tag: 'button' }); - await driver.delay(regularDelayMs); - }); - - it('can pick a token from the existing options', async function () { - await driver.fill('#search-tokens', 'BAT'); - await driver.delay(regularDelayMs); - - await driver.clickElement({ text: 'BAT', tag: 'span' }); - await driver.delay(regularDelayMs); - - await driver.clickElement({ text: 'Next', tag: 'button' }); - await driver.delay(regularDelayMs); - - await driver.clickElement({ text: 'Add Tokens', tag: 'button' }); - await driver.delay(largeDelayMs); - }); - - it('renders the balance for the chosen token', async function () { - await driver.waitForSelector({ - css: '.token-overview__primary-balance', - text: '0 BAT', - }); - await driver.delay(regularDelayMs); - }); - }); }); diff --git a/test/e2e/tests/add-hide-token.spec.js b/test/e2e/tests/add-hide-token.spec.js new file mode 100644 index 000000000..8b98378cd --- /dev/null +++ b/test/e2e/tests/add-hide-token.spec.js @@ -0,0 +1,94 @@ +const { strict: assert } = require('assert'); +const { withFixtures } = require('../helpers'); + +describe('Hide token', function () { + const ganacheOptions = { + accounts: [ + { + secretKey: + '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', + balance: 25000000000000000000, + }, + ], + }; + it('hides the token when clicked', async function () { + await withFixtures( + { + fixtures: 'custom-token', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + await driver.waitForSelector({ + css: '.asset-list-item__token-button', + text: '0 TST', + }); + + let assets = await driver.findElements('.asset-list-item'); + assert.equal(assets.length, 2); + + await driver.clickElement({ text: 'Assets', tag: 'button' }); + + await driver.clickElement({ text: 'TST', tag: 'span' }); + + await driver.clickElement('[data-testid="asset-options__button"]'); + + await driver.clickElement('[data-testid="asset-options__hide"]'); + + // wait for confirm hide modal to be visible + const confirmHideModal = await driver.findVisibleElement('span .modal'); + + await driver.clickElement( + '[data-testid="hide-token-confirmation__hide"]', + ); + + // wait for confirm hide modal to be removed from DOM. + await confirmHideModal.waitForElementState('hidden'); + + assets = await driver.findElements('.asset-list-item'); + assert.equal(assets.length, 1); + }, + ); + }); +}); + +describe('Add existing token using search', function () { + const ganacheOptions = { + accounts: [ + { + secretKey: + '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', + balance: 25000000000000000000, + }, + ], + }; + it('renders the balance for the chosen token', async function () { + await withFixtures( + { + fixtures: 'imported-account', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + await driver.clickElement({ text: 'Add Token', tag: 'button' }); + await driver.fill('#search-tokens', 'BAT'); + await driver.clickElement({ text: 'BAT', tag: 'span' }); + await driver.clickElement({ text: 'Next', tag: 'button' }); + await driver.clickElement({ text: 'Add Tokens', tag: 'button' }); + + await driver.waitForSelector({ + css: '.token-overview__primary-balance', + text: '0 BAT', + }); + }, + ); + }); +}); diff --git a/test/e2e/tests/custom-rpc-history.spec.js b/test/e2e/tests/custom-rpc-history.spec.js index a827ada2d..792bfa980 100644 --- a/test/e2e/tests/custom-rpc-history.spec.js +++ b/test/e2e/tests/custom-rpc-history.spec.js @@ -12,10 +12,12 @@ describe('Stores custom RPC history', function () { ], }; it(`creates first custom RPC entry`, async function () { + const port = 8546; + const chainId = 1338; await withFixtures( { fixtures: 'imported-account', - ganacheOptions, + ganacheOptions: { ...ganacheOptions, concurrent: { port, chainId } }, title: this.test.title, }, async ({ driver }) => { @@ -23,8 +25,8 @@ describe('Stores custom RPC history', function () { await driver.fill('#password', 'correct horse battery staple'); await driver.press('#password', driver.Key.ENTER); - const rpcUrl = 'http://127.0.0.1:8545/1'; - const chainId = '0x539'; // Ganache default, decimal 1337 + const rpcUrl = `http://127.0.0.1:${port}`; + const networkName = 'Secondary Ganache Testnet'; await driver.clickElement('.network-display'); @@ -33,17 +35,95 @@ describe('Stores custom RPC history', function () { await driver.findElement('.settings-page__sub-header-text'); const customRpcInputs = await driver.findElements('input[type="text"]'); + const networkNameInput = customRpcInputs[0]; const rpcUrlInput = customRpcInputs[1]; const chainIdInput = customRpcInputs[2]; + await networkNameInput.clear(); + await networkNameInput.sendKeys(networkName); + await rpcUrlInput.clear(); await rpcUrlInput.sendKeys(rpcUrl); await chainIdInput.clear(); - await chainIdInput.sendKeys(chainId); + await chainIdInput.sendKeys(chainId.toString()); await driver.clickElement('.network-form__footer .btn-secondary'); - await driver.findElement({ text: rpcUrl, tag: 'div' }); + await driver.findElement({ text: networkName, tag: 'div' }); + }, + ); + }); + + it('warns user when they enter url for an already configured network', async function () { + await withFixtures( + { + fixtures: 'imported-account', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + // duplicate network + const duplicateRpcUrl = 'http://localhost:8545'; + + await driver.clickElement('.network-display'); + + await driver.clickElement({ text: 'Custom RPC', tag: 'span' }); + + await driver.findElement('.settings-page__sub-header-text'); + + const customRpcInputs = await driver.findElements('input[type="text"]'); + const rpcUrlInput = customRpcInputs[1]; + + await rpcUrlInput.clear(); + await rpcUrlInput.sendKeys(duplicateRpcUrl); + await driver.findElement({ + text: 'This URL is currently used by the Localhost 8545 network.', + tag: 'p', + }); + }, + ); + }); + + it('warns user when they enter chainId for an already configured network', async function () { + await withFixtures( + { + fixtures: 'imported-account', + ganacheOptions, + title: this.test.title, + }, + async ({ driver }) => { + await driver.navigate(); + await driver.fill('#password', 'correct horse battery staple'); + await driver.press('#password', driver.Key.ENTER); + + // duplicate network + const newRpcUrl = 'http://localhost:8544'; + const duplicateChainId = '0x539'; + + await driver.clickElement('.network-display'); + + await driver.clickElement({ text: 'Custom RPC', tag: 'span' }); + + await driver.findElement('.settings-page__sub-header-text'); + + const customRpcInputs = await driver.findElements('input[type="text"]'); + const rpcUrlInput = customRpcInputs[1]; + const chainIdInput = customRpcInputs[2]; + + await rpcUrlInput.clear(); + await rpcUrlInput.sendKeys(newRpcUrl); + + await chainIdInput.clear(); + await chainIdInput.sendKeys(duplicateChainId); + await driver.findElement({ + text: + 'This Chain ID is currently used by the Localhost 8545 network.', + tag: 'p', + }); }, ); }); diff --git a/test/stub/tx-meta-stub.js b/test/stub/tx-meta-stub.js index 0af67dd20..bb1fc4f99 100644 --- a/test/stub/tx-meta-stub.js +++ b/test/stub/tx-meta-stub.js @@ -1,3 +1,4 @@ +import { GAS_LIMITS } from '../../shared/constants/gas'; import { TRANSACTION_STATUSES, TRANSACTION_TYPES, @@ -16,7 +17,7 @@ export const txMetaStub = { type: TRANSACTION_TYPES.SENT_ETHER, txParams: { from: '0xf231d46dd78806e1dd93442cf33c7671f8538748', - gas: '0x5208', + gas: GAS_LIMITS.SIMPLE, gasPrice: '0x1e8480', to: '0xf231d46dd78806e1dd93442cf33c7671f8538748', value: '0x0', @@ -197,7 +198,7 @@ export const txMetaStub = { type: TRANSACTION_TYPES.SENT_ETHER, txParams: { from: '0xf231d46dd78806e1dd93442cf33c7671f8538748', - gas: '0x5208', + gas: GAS_LIMITS.SIMPLE, gasPrice: '0x1e8480', nonce: '0x4', to: '0xf231d46dd78806e1dd93442cf33c7671f8538748', diff --git a/ui/components/app/app-components.scss b/ui/components/app/app-components.scss index 8110d8341..89f2b9760 100644 --- a/ui/components/app/app-components.scss +++ b/ui/components/app/app-components.scss @@ -23,6 +23,7 @@ @import 'permission-page-container/index'; @import 'permissions-connect-footer/index'; @import 'permissions-connect-header/index'; +@import 'recovery-phrase-reminder/index'; @import 'selected-account/index'; @import 'sidebars/index'; @import 'signature-request/index'; diff --git a/ui/components/app/asset-list-item/asset-list-item.js b/ui/components/app/asset-list-item/asset-list-item.js index 7096664d4..c147297b5 100644 --- a/ui/components/app/asset-list-item/asset-list-item.js +++ b/ui/components/app/asset-list-item/asset-list-item.js @@ -10,7 +10,7 @@ import InfoIcon from '../../ui/icon/info-icon.component'; import Button from '../../ui/button'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { useMetricEvent } from '../../../hooks/useMetricEvent'; -import { updateSendToken } from '../../../store/actions'; +import { updateSendToken } from '../../../ducks/send/send.duck'; import { SEND_ROUTE } from '../../../helpers/constants/routes'; import { SEVERITIES } from '../../../helpers/constants/design-system'; diff --git a/ui/components/app/asset-list/asset-list.js b/ui/components/app/asset-list/asset-list.js index e4b9efea4..98ce9f042 100644 --- a/ui/components/app/asset-list/asset-list.js +++ b/ui/components/app/asset-list/asset-list.js @@ -11,10 +11,10 @@ import { useMetricEvent } from '../../../hooks/useMetricEvent'; import { useUserPreferencedCurrency } from '../../../hooks/useUserPreferencedCurrency'; import { getCurrentAccountWithSendEtherInfo, - getNativeCurrency, getShouldShowFiat, getNativeCurrencyImage, } from '../../../selectors'; +import { getNativeCurrency } from '../../../ducks/metamask/metamask'; import { useCurrencyDisplay } from '../../../hooks/useCurrencyDisplay'; const AssetList = ({ onClickAsset }) => { diff --git a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js index eef1ed09e..29f7b9edd 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js @@ -6,7 +6,7 @@ const ConfirmPageContainerWarning = (props) => {
diff --git a/ui/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js b/ui/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js index 05d40a8b1..9c84a720a 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js @@ -47,7 +47,7 @@ export default function ConfirmPageContainerHeader({ visibility: showEdit ? 'initial' : 'hidden', }} > - + onEdit()} diff --git a/ui/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.test.js b/ui/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.test.js index 864979f8d..98c1169b2 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.test.js +++ b/ui/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.test.js @@ -8,6 +8,11 @@ import ConfirmPageContainerHeader from './confirm-page-container-header.componen const util = require('../../../../../app/scripts/lib/util'); +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useLayoutEffect: jest.requireActual('react').useEffect, +})); + describe('Confirm Detail Row Component', () => { describe('render', () => { it('should render a div with a confirm-page-container-header class', () => { diff --git a/ui/components/app/confirm-page-container/confirm-page-container-navigation/confirm-page-container-navigation.component.js b/ui/components/app/confirm-page-container/confirm-page-container-navigation/confirm-page-container-navigation.component.js index 20e9c0ce5..8cc0985fb 100755 --- a/ui/components/app/confirm-page-container/confirm-page-container-navigation/confirm-page-container-navigation.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container-navigation/confirm-page-container-navigation.component.js @@ -33,14 +33,14 @@ const ConfirmPageContainerNavigation = (props) => { data-testid="first-page" onClick={() => onNextTx(firstTx)} > - +
onNextTx(prevTxId)} > - +
@@ -64,7 +64,7 @@ const ConfirmPageContainerNavigation = (props) => { >
@@ -75,7 +75,7 @@ const ConfirmPageContainerNavigation = (props) => { > diff --git a/ui/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content-component.test.js b/ui/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content-component.test.js index de8ec3b80..4a7f86284 100644 --- a/ui/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content-component.test.js +++ b/ui/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content-component.test.js @@ -23,6 +23,7 @@ describe('AdvancedTabContent Component', () => { insufficientBalance={false} customPriceIsSafe isSpeedUp={false} + customPriceIsExcessive={false} />, ); }); diff --git a/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container-component.test.js b/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container-component.test.js index 2094d4b54..d88a3ab98 100644 --- a/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container-component.test.js +++ b/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container-component.test.js @@ -76,6 +76,7 @@ describe('GasModalPageContainer Component', () => { customGasLimitInHex="mockCustomGasLimitInHex" insufficientBalance={false} disableSave={false} + customPriceIsExcessive={false} />, ); }); @@ -124,6 +125,7 @@ describe('GasModalPageContainer Component', () => { , { context: { t: (str1, str2) => (str2 ? str1 + str2 : str1) } }, ); @@ -202,6 +204,7 @@ describe('GasModalPageContainer Component', () => { customGasLimitInHex="mockCustomGasLimitInHex" insufficientBalance={false} disableSave={false} + customPriceIsExcessive={false} hideBasic />, ); diff --git a/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container-container.test.js b/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container-container.test.js index bcbbaa9d1..6b36af651 100644 --- a/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container-container.test.js +++ b/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container-container.test.js @@ -1,6 +1,6 @@ import sinon from 'sinon'; -import { hideModal, setGasLimit, setGasPrice } from '../../../../store/actions'; +import { hideModal } from '../../../../store/actions'; import { setCustomGasPrice, @@ -8,7 +8,11 @@ import { resetCustomData, } from '../../../../ducks/gas/gas.duck'; -import { hideGasButtonGroup } from '../../../../ducks/send/send.duck'; +import { + hideGasButtonGroup, + setGasLimit, + setGasPrice, +} from '../../../../ducks/send/send.duck'; let mapDispatchToProps; let mergeProps; @@ -29,7 +33,7 @@ jest.mock('../../../../selectors', () => ({ getDefaultActiveButtonIndex: (a, b) => a + b, getCurrentEthBalance: (state) => state.metamask.balance || '0x0', getSendToken: () => null, - getTokenBalance: (state) => state.metamask.send.tokenBalance || '0x0', + getTokenBalance: (state) => state.send.tokenBalance || '0x0', getCustomGasPrice: (state) => state.gas.customData.price || '0x0', getCustomGasLimit: (state) => state.gas.customData.limit || '0x0', getCurrentCurrency: jest.fn().mockReturnValue('usd'), @@ -44,8 +48,6 @@ jest.mock('../../../../selectors', () => ({ jest.mock('../../../../store/actions', () => ({ hideModal: jest.fn(), - setGasLimit: jest.fn(), - setGasPrice: jest.fn(), updateTransaction: jest.fn(), })); @@ -57,6 +59,8 @@ jest.mock('../../../../ducks/gas/gas.duck', () => ({ jest.mock('../../../../ducks/send/send.duck', () => ({ hideGasButtonGroup: jest.fn(), + setGasLimit: jest.fn(), + setGasPrice: jest.fn(), })); require('./gas-modal-page-container.container'); diff --git a/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js b/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js index d85d07c72..9e70e3284 100644 --- a/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js +++ b/ui/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js @@ -2,13 +2,9 @@ import { connect } from 'react-redux'; import { addHexPrefix } from '../../../../../app/scripts/lib/util'; import { hideModal, - setGasLimit, - setGasPrice, createRetryTransaction, createSpeedUpTransaction, hideSidebar, - updateSendAmount, - setGasTotal, updateTransaction, } from '../../../../store/actions'; import { @@ -19,6 +15,10 @@ import { } from '../../../../ducks/gas/gas.duck'; import { hideGasButtonGroup, + setGasLimit, + setGasPrice, + setGasTotal, + updateSendAmount, updateSendErrors, } from '../../../../ducks/send/send.duck'; import { @@ -28,6 +28,7 @@ import { getIsMainnet, getSendToken, getPreferences, + getIsTestnet, getBasicGasEstimateLoadingStatus, getCustomGasLimit, getCustomGasPrice, @@ -36,8 +37,10 @@ import { isCustomPriceSafe, getTokenBalance, getSendMaxModeState, + isCustomPriceSafeForCustomNetwork, getAveragePriceEstimateInHexWEI, isCustomPriceExcessive, + getIsGasEstimatesFetched, } from '../../../../selectors'; import { @@ -55,10 +58,14 @@ import { import { MIN_GAS_LIMIT_DEC } from '../../../../pages/send/send.constants'; import { calcMaxAmount } from '../../../../pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils'; import { TRANSACTION_STATUSES } from '../../../../../shared/constants/transaction'; +import { GAS_LIMITS } from '../../../../../shared/constants/gas'; import GasModalPageContainer from './gas-modal-page-container.component'; const mapStateToProps = (state, ownProps) => { - const { currentNetworkTxList, send } = state.metamask; + const { + metamask: { currentNetworkTxList }, + send, + } = state; const { modalState: { props: modalProps } = {} } = state.appState.modal || {}; const { txData = {} } = modalProps || {}; const { transaction = {}, onSubmit } = ownProps; @@ -72,7 +79,7 @@ const mapStateToProps = (state, ownProps) => { const txParams = selectedTransaction?.txParams ? selectedTransaction.txParams : { - gas: send.gasLimit || '0x5208', + gas: send.gasLimit || GAS_LIMITS.SIMPLE, gasPrice: send.gasPrice || getAveragePriceEstimateInHexWEI(state, true), value: sendToken ? '0x0' : send.amount, }; @@ -81,7 +88,7 @@ const mapStateToProps = (state, ownProps) => { const value = ownProps.transaction?.txParams?.value || txParams.value; const customModalGasPriceInHex = getCustomGasPrice(state) || currentGasPrice; const customModalGasLimitInHex = - getCustomGasLimit(state) || currentGasLimit || '0x5208'; + getCustomGasLimit(state) || currentGasLimit || GAS_LIMITS.SIMPLE; const customGasTotal = calcGasTotal( customModalGasLimitInHex, customModalGasPriceInHex, @@ -113,6 +120,7 @@ const mapStateToProps = (state, ownProps) => { const showFiat = Boolean(isMainnet || showFiatInTestnets); const isSendTokenSet = Boolean(sendToken); + const isTestnet = getIsTestnet(state); const newTotalEth = maxModeOn && !isSendTokenSet @@ -132,6 +140,16 @@ const mapStateToProps = (state, ownProps) => { balance, conversionRate, }); + const isGasEstimate = getIsGasEstimatesFetched(state); + + let customPriceIsSafe; + if ((isMainnet || process.env.IN_TEST) && isGasEstimate) { + customPriceIsSafe = isCustomPriceSafe(state); + } else if (isTestnet) { + customPriceIsSafe = true; + } else { + customPriceIsSafe = isCustomPriceSafeForCustomNetwork(state); + } return { hideBasic, @@ -142,7 +160,7 @@ const mapStateToProps = (state, ownProps) => { customGasLimit: calcCustomGasLimit(customModalGasLimitInHex), customGasTotal, newTotalFiat, - customPriceIsSafe: isCustomPriceSafe(state), + customPriceIsSafe, customPriceIsExcessive: isCustomPriceExcessive(state), maxModeOn, gasPriceButtonGroupProps: { diff --git a/ui/components/app/loading-network-screen/loading-network-screen.component.js b/ui/components/app/loading-network-screen/loading-network-screen.component.js index 9097ca4d8..7f2f2df48 100644 --- a/ui/components/app/loading-network-screen/loading-network-screen.component.js +++ b/ui/components/app/loading-network-screen/loading-network-screen.component.js @@ -2,6 +2,7 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import Button from '../../ui/button'; import LoadingScreen from '../../ui/loading-screen'; +import { SECOND } from '../../../../shared/constants/time'; export default class LoadingNetworkScreen extends PureComponent { state = { @@ -27,7 +28,7 @@ export default class LoadingNetworkScreen extends PureComponent { componentDidMount = () => { this.cancelCallTimeout = setTimeout( this.cancelCall, - this.props.cancelTime || 15000, + this.props.cancelTime || SECOND * 15, ); }; @@ -87,7 +88,7 @@ export default class LoadingNetworkScreen extends PureComponent { window.clearTimeout(this.cancelCallTimeout); this.cancelCallTimeout = setTimeout( this.cancelCall, - this.props.cancelTime || 15000, + this.props.cancelTime || SECOND * 15, ); }} > @@ -114,7 +115,7 @@ export default class LoadingNetworkScreen extends PureComponent { this.setState({ showErrorScreen: false }); this.cancelCallTimeout = setTimeout( this.cancelCall, - this.props.cancelTime || 15000, + this.props.cancelTime || SECOND * 15, ); } }; diff --git a/ui/components/app/menu-bar/account-options-menu.js b/ui/components/app/menu-bar/account-options-menu.js index f71908aae..3ae13c385 100644 --- a/ui/components/app/menu-bar/account-options-menu.js +++ b/ui/components/app/menu-bar/account-options-menu.js @@ -2,11 +2,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useHistory } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; +import { getAccountLink } from '@metamask/etherscan-link'; import { showModal } from '../../../store/actions'; import { CONNECTED_ROUTE } from '../../../helpers/constants/routes'; import { Menu, MenuItem } from '../../ui/menu'; -import getAccountLink from '../../../helpers/utils/account-link'; import { getCurrentChainId, getCurrentKeyring, @@ -14,7 +14,10 @@ import { getSelectedIdentity, } from '../../../selectors'; import { useI18nContext } from '../../../hooks/useI18nContext'; -import { useMetricEvent } from '../../../hooks/useMetricEvent'; +import { + useMetricEvent, + useNewMetricEvent, +} from '../../../hooks/useMetricEvent'; import { getEnvironmentType } from '../../../../app/scripts/lib/util'; import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app'; @@ -22,6 +25,14 @@ export default function AccountOptionsMenu({ anchorElement, onClose }) { const t = useI18nContext(); const dispatch = useDispatch(); const history = useHistory(); + + const keyring = useSelector(getCurrentKeyring); + const chainId = useSelector(getCurrentChainId); + const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); + const selectedIdentity = useSelector(getSelectedIdentity); + const { address } = selectedIdentity; + const addressLink = getAccountLink(address, chainId, rpcPrefs); + const openFullscreenEvent = useMetricEvent({ eventOpts: { category: 'Navigation', @@ -36,13 +47,7 @@ export default function AccountOptionsMenu({ anchorElement, onClose }) { name: 'Viewed Account Details', }, }); - const viewOnEtherscanEvent = useMetricEvent({ - eventOpts: { - category: 'Navigation', - action: 'Account Options', - name: 'Clicked View on Etherscan', - }, - }); + const openConnectedSitesEvent = useMetricEvent({ eventOpts: { category: 'Navigation', @@ -51,12 +56,16 @@ export default function AccountOptionsMenu({ anchorElement, onClose }) { }, }); - const keyring = useSelector(getCurrentKeyring); - const chainId = useSelector(getCurrentChainId); - const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); - const selectedIdentity = useSelector(getSelectedIdentity); + const blockExplorerLinkClickedEvent = useNewMetricEvent({ + category: 'Navigation', + event: 'Clicked Block Explorer Link', + properties: { + link_type: 'Account Tracker', + action: 'Account Options', + block_explorer_domain: addressLink ? new URL(addressLink)?.hostname : '', + }, + }); - const { address } = selectedIdentity; const isRemovable = keyring.type !== 'HD Key Tree'; return ( @@ -90,9 +99,9 @@ export default function AccountOptionsMenu({ anchorElement, onClose }) { { - viewOnEtherscanEvent(); + blockExplorerLinkClickedEvent(); global.platform.openTab({ - url: getAccountLink(address, chainId, rpcPrefs), + url: addressLink, }); onClose(); }} diff --git a/ui/components/app/menu-bar/menu-bar.test.js b/ui/components/app/menu-bar/menu-bar.test.js index 4a625f717..89f933717 100644 --- a/ui/components/app/menu-bar/menu-bar.test.js +++ b/ui/components/app/menu-bar/menu-bar.test.js @@ -1,6 +1,7 @@ import React from 'react'; import { Provider } from 'react-redux'; import configureStore from 'redux-mock-store'; +import { waitFor } from '@testing-library/react'; import { mountWithRouter } from '../../../../test/lib/render-helpers'; import { ROPSTEN_CHAIN_ID } from '../../../../shared/constants/network'; import MenuBar from './menu-bar'; @@ -30,21 +31,25 @@ const initState = { const mockStore = configureStore(); describe('MenuBar', () => { - it('opens account detail menu when account options is clicked', () => { + it('opens account detail menu when account options is clicked', async () => { const store = mockStore(initState); const wrapper = mountWithRouter( , ); - expect(!wrapper.exists('AccountOptionsMenu')).toStrictEqual(true); + await waitFor(() => + expect(!wrapper.exists('AccountOptionsMenu')).toStrictEqual(true), + ); const accountOptions = wrapper.find('.menu-bar__account-options'); accountOptions.simulate('click'); wrapper.update(); - expect(wrapper.exists('AccountOptionsMenu')).toStrictEqual(true); + await waitFor(() => + expect(wrapper.exists('AccountOptionsMenu')).toStrictEqual(true), + ); }); - it('sets accountDetailsMenuOpen to false when closed', () => { + it('sets accountDetailsMenuOpen to false when closed', async () => { const store = mockStore(initState); const wrapper = mountWithRouter( @@ -54,10 +59,14 @@ describe('MenuBar', () => { const accountOptions = wrapper.find('.menu-bar__account-options'); accountOptions.simulate('click'); wrapper.update(); - expect(wrapper.exists('AccountOptionsMenu')).toStrictEqual(true); + await waitFor(() => + expect(wrapper.exists('AccountOptionsMenu')).toStrictEqual(true), + ); const accountDetailsMenu = wrapper.find('AccountOptionsMenu'); - accountDetailsMenu.prop('onClose')(); - wrapper.update(); - expect(!wrapper.exists('AccountOptionsMenu')).toStrictEqual(true); + await waitFor(() => { + accountDetailsMenu.prop('onClose')(); + wrapper.update(); + expect(!wrapper.exists('AccountOptionsMenu')).toStrictEqual(true); + }); }); }); diff --git a/ui/components/app/modals/account-details-modal/account-details-modal.component.js b/ui/components/app/modals/account-details-modal/account-details-modal.component.js index c314d6979..6dc47f3d2 100644 --- a/ui/components/app/modals/account-details-modal/account-details-modal.component.js +++ b/ui/components/app/modals/account-details-modal/account-details-modal.component.js @@ -1,7 +1,8 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import { getAccountLink } from '@metamask/etherscan-link'; + import AccountModalContainer from '../account-modal-container'; -import getAccountLink from '../../../../helpers/utils/account-link'; import QrView from '../../../ui/qr-code'; import EditableLabel from '../../../ui/editable-label'; import Button from '../../../ui/button'; @@ -18,6 +19,7 @@ export default class AccountDetailsModal extends Component { static contextTypes = { t: PropTypes.func, + trackEvent: PropTypes.func, }; render() { @@ -61,8 +63,20 @@ export default class AccountDetailsModal extends Component { type="secondary" className="account-details-modal__button" onClick={() => { + const accountLink = getAccountLink(address, chainId, rpcPrefs); + this.context.trackEvent({ + category: 'Navigation', + event: 'Clicked Block Explorer Link', + properties: { + link_type: 'Account Tracker', + action: 'Account Details Modal', + block_explorer_domain: accountLink + ? new URL(accountLink)?.hostname + : '', + }, + }); global.platform.openTab({ - url: getAccountLink(address, chainId, rpcPrefs), + url: accountLink, }); }} > diff --git a/ui/components/app/modals/account-details-modal/account-details-modal.test.js b/ui/components/app/modals/account-details-modal/account-details-modal.test.js index a0e7a93ed..2f0fdb788 100644 --- a/ui/components/app/modals/account-details-modal/account-details-modal.test.js +++ b/ui/components/app/modals/account-details-modal/account-details-modal.test.js @@ -36,6 +36,7 @@ describe('Account Details Modal', () => { wrapper = shallow(, { context: { t: (str) => str, + trackEvent: (e) => e, }, }); }); diff --git a/ui/components/app/modals/confirm-remove-account/confirm-remove-account.component.js b/ui/components/app/modals/confirm-remove-account/confirm-remove-account.component.js index 4a7785d4d..5b90f0359 100644 --- a/ui/components/app/modals/confirm-remove-account/confirm-remove-account.component.js +++ b/ui/components/app/modals/confirm-remove-account/confirm-remove-account.component.js @@ -1,9 +1,9 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import { getAccountLink } from '@metamask/etherscan-link'; import Modal from '../../modal'; import { addressSummary } from '../../../../helpers/utils/util'; import Identicon from '../../../ui/identicon'; -import getAccountLink from '../../../../helpers/utils/account-link'; export default class ConfirmRemoveAccount extends Component { static propTypes = { @@ -16,6 +16,7 @@ export default class ConfirmRemoveAccount extends Component { static contextTypes = { t: PropTypes.func, + trackEvent: PropTypes.func, }; handleRemove = () => { @@ -30,7 +31,7 @@ export default class ConfirmRemoveAccount extends Component { renderSelectedAccount() { const { t } = this.context; - const { identity } = this.props; + const { identity, rpcPrefs, chainId } = this.props; return (
@@ -53,11 +54,27 @@ export default class ConfirmRemoveAccount extends Component {
{ + const accountLink = getAccountLink( + identity.address, + chainId, + rpcPrefs, + ); + this.context.trackEvent({ + category: 'Accounts', + event: 'Clicked Block Explorer Link', + properties: { + link_type: 'Account Tracker', + action: 'Remove Account', + block_explorer_domain: accountLink + ? new URL(accountLink)?.hostname + : '', + }, + }); + global.platform.openTab({ + url: accountLink, + }); + }} target="_blank" rel="noopener noreferrer" title={t('etherscanView')} diff --git a/ui/components/app/modals/confirm-remove-account/confirm-remove-account.test.js b/ui/components/app/modals/confirm-remove-account/confirm-remove-account.test.js index fd89e96f8..da59aeffa 100644 --- a/ui/components/app/modals/confirm-remove-account/confirm-remove-account.test.js +++ b/ui/components/app/modals/confirm-remove-account/confirm-remove-account.test.js @@ -21,6 +21,8 @@ describe('Confirm Remove Account', () => { address: '0x0', name: 'Account 1', }, + chainId: '0x0', + rpcPrefs: {}, }; const mockStore = configureStore(); diff --git a/ui/components/app/modals/qr-scanner/qr-scanner.component.js b/ui/components/app/modals/qr-scanner/qr-scanner.component.js index 6cad31f6e..b8b834de6 100644 --- a/ui/components/app/modals/qr-scanner/qr-scanner.component.js +++ b/ui/components/app/modals/qr-scanner/qr-scanner.component.js @@ -4,6 +4,7 @@ import log from 'loglevel'; import { BrowserQRCodeReader } from '@zxing/library'; import { getEnvironmentType } from '../../../../../app/scripts/lib/util'; import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../../shared/constants/app'; +import { SECOND } from '../../../../../shared/constants/time'; import Spinner from '../../../ui/spinner'; import WebcamUtils from '../../../../helpers/utils/webcam-utils'; import PageContainerFooter from '../../../ui/page-container/page-container-footer/page-container-footer.component'; @@ -86,14 +87,14 @@ export default class QrScanner extends Component { const { permissions } = await WebcamUtils.checkStatus(); if (permissions) { // Let the video stream load first... - await new Promise((resolve) => setTimeout(resolve, 2000)); + await new Promise((resolve) => setTimeout(resolve, SECOND * 2)); if (!this.mounted) { return; } this.setState({ ready: READY_STATE.READY }); } else if (this.mounted) { // Keep checking for permissions - this.permissionChecker = setTimeout(this.checkPermissions, 1000); + this.permissionChecker = setTimeout(this.checkPermissions, SECOND); } } catch (error) { if (this.mounted) { diff --git a/ui/components/app/recovery-phrase-reminder/index.js b/ui/components/app/recovery-phrase-reminder/index.js new file mode 100644 index 000000000..35b8d2da3 --- /dev/null +++ b/ui/components/app/recovery-phrase-reminder/index.js @@ -0,0 +1 @@ +export { default } from './recovery-phrase-reminder'; diff --git a/ui/components/app/recovery-phrase-reminder/index.scss b/ui/components/app/recovery-phrase-reminder/index.scss new file mode 100644 index 000000000..2f50b8da6 --- /dev/null +++ b/ui/components/app/recovery-phrase-reminder/index.scss @@ -0,0 +1,10 @@ +.recovery-phrase-reminder { + &__list { + list-style: disc; + padding-left: 20px; + + li { + margin-bottom: 5px; + } + } +} diff --git a/ui/components/app/recovery-phrase-reminder/recovery-phrase-reminder.js b/ui/components/app/recovery-phrase-reminder/recovery-phrase-reminder.js new file mode 100644 index 000000000..1b8b66ba3 --- /dev/null +++ b/ui/components/app/recovery-phrase-reminder/recovery-phrase-reminder.js @@ -0,0 +1,91 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useHistory } from 'react-router-dom'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +// Components +import Box from '../../ui/box'; +import Button from '../../ui/button'; +import Popover from '../../ui/popover'; +import Typography from '../../ui/typography'; +// Helpers +import { + COLORS, + DISPLAY, + TEXT_ALIGN, + TYPOGRAPHY, + BLOCK_SIZES, + FONT_WEIGHT, + JUSTIFY_CONTENT, +} from '../../../helpers/constants/design-system'; +import { INITIALIZE_BACKUP_SEED_PHRASE_ROUTE } from '../../../helpers/constants/routes'; + +export default function RecoveryPhraseReminder({ onConfirm, hasBackedUp }) { + const t = useI18nContext(); + const history = useHistory(); + + const handleBackUp = () => { + history.push(INITIALIZE_BACKUP_SEED_PHRASE_ROUTE); + }; + + return ( + + + + {t('recoveryPhraseReminderSubText')} + + +
    +
  • + + {t('recoveryPhraseReminderItemOne')} + +
  • +
  • {t('recoveryPhraseReminderItemTwo')}
  • +
  • + {hasBackedUp ? ( + t('recoveryPhraseReminderHasBackedUp') + ) : ( + <> + {t('recoveryPhraseReminderHasNotBackedUp')} + + + + + )} +
  • +
+
+ + + + + +
+
+ ); +} + +RecoveryPhraseReminder.propTypes = { + hasBackedUp: PropTypes.bool.isRequired, + onConfirm: PropTypes.func.isRequired, +}; diff --git a/ui/components/app/selected-account/selected-account.component.js b/ui/components/app/selected-account/selected-account.component.js index b52dd033f..2c4df0ce0 100644 --- a/ui/components/app/selected-account/selected-account.component.js +++ b/ui/components/app/selected-account/selected-account.component.js @@ -5,6 +5,7 @@ import { shortenAddress } from '../../../helpers/utils/util'; import Tooltip from '../../ui/tooltip'; import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; +import { SECOND } from '../../../../shared/constants/time'; class SelectedAccount extends Component { state = { @@ -50,7 +51,7 @@ class SelectedAccount extends Component { this.setState({ copied: true }); this.copyTimeout = setTimeout( () => this.setState({ copied: false }), - 3000, + SECOND * 3, ); copyToClipboard(checksummedAddress); }} diff --git a/ui/components/app/sidebars/sidebar.component.js b/ui/components/app/sidebars/sidebar.component.js index b9fff51cd..45be23b50 100644 --- a/ui/components/app/sidebars/sidebar.component.js +++ b/ui/components/app/sidebars/sidebar.component.js @@ -2,6 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import ReactCSSTransitionGroup from 'react-transition-group/CSSTransitionGroup'; import CustomizeGas from '../gas-customization/gas-modal-page-container'; +import { MILLISECOND } from '../../../../shared/constants/time'; export default class Sidebar extends Component { static propTypes = { @@ -60,8 +61,8 @@ export default class Sidebar extends Component {
{sidebarOpen && !sidebarShouldClose ? this.renderSidebarContent() diff --git a/ui/components/app/transaction-activity-log/transaction-activity-log.component.js b/ui/components/app/transaction-activity-log/transaction-activity-log.component.js index 641976046..2e7cb77d3 100644 --- a/ui/components/app/transaction-activity-log/transaction-activity-log.component.js +++ b/ui/components/app/transaction-activity-log/transaction-activity-log.component.js @@ -2,18 +2,19 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; +import { getBlockExplorerLink } from '@metamask/etherscan-link'; import { getEthConversionFromWeiHex, getValueFromWeiHex, } from '../../../helpers/utils/conversions.util'; import { formatDate } from '../../../helpers/utils/util'; -import { getBlockExplorerUrlForTx } from '../../../../shared/modules/transaction.utils'; import TransactionActivityLogIcon from './transaction-activity-log-icon'; import { CONFIRMED_STATUS } from './transaction-activity-log.constants'; export default class TransactionActivityLog extends PureComponent { static contextTypes = { t: PropTypes.func, + trackEvent: PropTypes.func, }; static propTypes = { @@ -31,10 +32,21 @@ export default class TransactionActivityLog extends PureComponent { }; handleActivityClick = (activity) => { - const etherscanUrl = getBlockExplorerUrlForTx( - activity, - this.props.rpcPrefs, - ); + const { rpcPrefs } = this.props; + const etherscanUrl = getBlockExplorerLink(activity, rpcPrefs); + + this.context.trackEvent({ + category: 'Transactions', + event: 'Clicked Block Explorer Link', + properties: { + link_type: 'Transaction Block Explorer', + action: 'Activity Details', + block_explorer_domain: etherscanUrl + ? new URL(etherscanUrl)?.hostname + : '', + }, + }); + global.platform.openTab({ url: etherscanUrl }); }; diff --git a/ui/components/app/transaction-activity-log/transaction-activity-log.container.js b/ui/components/app/transaction-activity-log/transaction-activity-log.container.js index 99f96baca..dad4e1ee7 100644 --- a/ui/components/app/transaction-activity-log/transaction-activity-log.container.js +++ b/ui/components/app/transaction-activity-log/transaction-activity-log.container.js @@ -2,9 +2,9 @@ import { connect } from 'react-redux'; import { findLastIndex } from 'lodash'; import { conversionRateSelector, - getNativeCurrency, getRpcPrefsForCurrentProvider, } from '../../../selectors'; +import { getNativeCurrency } from '../../../ducks/metamask/metamask'; import TransactionActivityLog from './transaction-activity-log.component'; import { combineTransactionHistories } from './transaction-activity-log.util'; import { diff --git a/ui/components/app/transaction-activity-log/transaction-activity-log.util.test.js b/ui/components/app/transaction-activity-log/transaction-activity-log.util.test.js index af7adee79..50a221a9e 100644 --- a/ui/components/app/transaction-activity-log/transaction-activity-log.util.test.js +++ b/ui/components/app/transaction-activity-log/transaction-activity-log.util.test.js @@ -1,3 +1,4 @@ +import { GAS_LIMITS } from '../../../../shared/constants/gas'; import { ROPSTEN_CHAIN_ID, ROPSTEN_NETWORK_ID, @@ -34,7 +35,7 @@ describe('TransactionActivityLog utils', () => { from: '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706', to: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6', value: '0x2386f26fc10000', - gas: '0x5208', + gas: GAS_LIMITS.SIMPLE, gasPrice: '0x3b9aca00', }, type: TRANSACTION_TYPES.STANDARD, @@ -82,7 +83,7 @@ describe('TransactionActivityLog utils', () => { time: 1543958845581, txParams: { from: '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706', - gas: '0x5208', + gas: GAS_LIMITS.SIMPLE, gasPrice: '0x3b9aca00', nonce: '0x32', to: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6', @@ -105,7 +106,7 @@ describe('TransactionActivityLog utils', () => { from: '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706', to: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6', value: '0x2386f26fc10000', - gas: '0x5208', + gas: GAS_LIMITS.SIMPLE, gasPrice: '0x3b9aca00', nonce: '0x32', }, @@ -176,7 +177,7 @@ describe('TransactionActivityLog utils', () => { time: 1543958857697, txParams: { from: '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706', - gas: '0x5208', + gas: GAS_LIMITS.SIMPLE, gasPrice: '0x481f2280', nonce: '0x32', to: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6', @@ -244,7 +245,7 @@ describe('TransactionActivityLog utils', () => { status: TRANSACTION_STATUSES.CONFIRMED, txParams: { from: '0x1', - gas: '0x5208', + gas: GAS_LIMITS.SIMPLE, gasPrice: '0x3b9aca00', nonce: '0xa4', to: '0x2', @@ -267,7 +268,7 @@ describe('TransactionActivityLog utils', () => { time: 1535507561452, txParams: { from: '0x1', - gas: '0x5208', + gas: GAS_LIMITS.SIMPLE, gasPrice: '0x3b9aca00', nonce: '0xa4', to: '0x2', @@ -395,7 +396,7 @@ describe('TransactionActivityLog utils', () => { status: TRANSACTION_STATUSES.CONFIRMED, txParams: { from: '0x1', - gas: '0x5208', + gas: GAS_LIMITS.SIMPLE, gasPrice: '0x3b9aca00', nonce: '0xa4', to: '0x2', diff --git a/ui/components/app/transaction-breakdown/transaction-breakdown.component.test.js b/ui/components/app/transaction-breakdown/transaction-breakdown.component.test.js index fb7916c46..3ac6aa268 100644 --- a/ui/components/app/transaction-breakdown/transaction-breakdown.component.test.js +++ b/ui/components/app/transaction-breakdown/transaction-breakdown.component.test.js @@ -1,6 +1,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { TRANSACTION_STATUSES } from '../../../../shared/constants/transaction'; +import { GAS_LIMITS } from '../../../../shared/constants/gas'; import TransactionBreakdown from './transaction-breakdown.component'; describe('TransactionBreakdown Component', () => { @@ -11,7 +12,7 @@ describe('TransactionBreakdown Component', () => { status: TRANSACTION_STATUSES.CONFIRMED, txParams: { from: '0x1', - gas: '0x5208', + gas: GAS_LIMITS.SIMPLE, gasPrice: '0x3b9aca00', nonce: '0xa4', to: '0x2', diff --git a/ui/components/app/transaction-breakdown/transaction-breakdown.container.js b/ui/components/app/transaction-breakdown/transaction-breakdown.container.js index d8f39cdd7..01cdc036e 100644 --- a/ui/components/app/transaction-breakdown/transaction-breakdown.container.js +++ b/ui/components/app/transaction-breakdown/transaction-breakdown.container.js @@ -1,9 +1,6 @@ import { connect } from 'react-redux'; -import { - getIsMainnet, - getNativeCurrency, - getPreferences, -} from '../../../selectors'; +import { getIsMainnet, getPreferences } from '../../../selectors'; +import { getNativeCurrency } from '../../../ducks/metamask/metamask'; import { getHexGasTotal } from '../../../helpers/utils/confirm-tx.util'; import { sumHexes } from '../../../helpers/utils/transactions.util'; import TransactionBreakdown from './transaction-breakdown.component'; diff --git a/ui/components/app/transaction-icon/transaction-icon.js b/ui/components/app/transaction-icon/transaction-icon.js index ec1b2a605..6980a89cd 100644 --- a/ui/components/app/transaction-icon/transaction-icon.js +++ b/ui/components/app/transaction-icon/transaction-icon.js @@ -56,14 +56,6 @@ export default function TransactionIcon({ status, category }) { TransactionIcon.propTypes = { status: PropTypes.oneOf([ - TRANSACTION_GROUP_CATEGORIES.APPROVAL, - TRANSACTION_GROUP_CATEGORIES.INTERACTION, - TRANSACTION_GROUP_CATEGORIES.SEND, - TRANSACTION_GROUP_CATEGORIES.SIGNATURE_REQUEST, - TRANSACTION_GROUP_CATEGORIES.RECEIVE, - TRANSACTION_GROUP_CATEGORIES.SWAP, - ]).isRequired, - category: PropTypes.oneOf([ TRANSACTION_GROUP_STATUSES.PENDING, TRANSACTION_STATUSES.UNAPPROVED, TRANSACTION_STATUSES.APPROVED, @@ -72,4 +64,12 @@ TransactionIcon.propTypes = { TRANSACTION_GROUP_STATUSES.CANCELLED, TRANSACTION_STATUSES.DROPPED, ]).isRequired, + category: PropTypes.oneOf([ + TRANSACTION_GROUP_CATEGORIES.APPROVAL, + TRANSACTION_GROUP_CATEGORIES.INTERACTION, + TRANSACTION_GROUP_CATEGORIES.SEND, + TRANSACTION_GROUP_CATEGORIES.SIGNATURE_REQUEST, + TRANSACTION_GROUP_CATEGORIES.RECEIVE, + TRANSACTION_GROUP_CATEGORIES.SWAP, + ]).isRequired, }; diff --git a/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js b/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js index c5f03eeb4..d822e2f76 100644 --- a/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js +++ b/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js @@ -1,6 +1,7 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import copyToClipboard from 'copy-to-clipboard'; +import { getBlockExplorerLink } from '@metamask/etherscan-link'; import SenderToRecipient from '../../ui/sender-to-recipient'; import { FLAT_VARIANT } from '../../ui/sender-to-recipient/sender-to-recipient.constants'; import TransactionActivityLog from '../transaction-activity-log'; @@ -9,13 +10,14 @@ import Button from '../../ui/button'; import Tooltip from '../../ui/tooltip'; import Copy from '../../ui/icon/copy-icon.component'; import Popover from '../../ui/popover'; -import { getBlockExplorerUrlForTx } from '../../../../shared/modules/transaction.utils'; +import { SECOND } from '../../../../shared/constants/time'; import { TRANSACTION_TYPES } from '../../../../shared/constants/transaction'; export default class TransactionListItemDetails extends PureComponent { static contextTypes = { t: PropTypes.func, metricsEvent: PropTypes.func, + trackEvent: PropTypes.func, }; static defaultProps = { @@ -47,22 +49,30 @@ export default class TransactionListItemDetails extends PureComponent { justCopied: false, }; - handleEtherscanClick = () => { + handleBlockExplorerClick = () => { const { transactionGroup: { primaryTransaction }, rpcPrefs, } = this.props; + const blockExplorerLink = getBlockExplorerLink( + primaryTransaction, + rpcPrefs, + ); - this.context.metricsEvent({ - eventOpts: { - category: 'Navigation', - action: 'Activity Log', - name: 'Clicked "View on Etherscan"', + this.context.trackEvent({ + category: 'Transactions', + event: 'Clicked Block Explorer Link', + properties: { + link_type: 'Transaction Block Explorer', + action: 'Transaction Details', + block_explorer_domain: blockExplorerLink + ? new URL(blockExplorerLink)?.hostname + : '', }, }); global.platform.openTab({ - url: getBlockExplorerUrlForTx(primaryTransaction, rpcPrefs), + url: blockExplorerLink, }); }; @@ -93,7 +103,7 @@ export default class TransactionListItemDetails extends PureComponent { this.setState({ justCopied: true }, () => { copyToClipboard(hash); - setTimeout(() => this.setState({ justCopied: false }), 1000); + setTimeout(() => this.setState({ justCopied: false }), SECOND); }); }; @@ -203,10 +213,10 @@ export default class TransactionListItemDetails extends PureComponent { > {showRetry && ( diff --git a/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.test.js b/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.test.js index 409ca1092..4164de3a3 100644 --- a/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.test.js +++ b/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.test.js @@ -5,6 +5,7 @@ import SenderToRecipient from '../../ui/sender-to-recipient'; import TransactionBreakdown from '../transaction-breakdown'; import TransactionActivityLog from '../transaction-activity-log'; import { TRANSACTION_STATUSES } from '../../../../shared/constants/transaction'; +import { GAS_LIMITS } from '../../../../shared/constants/gas'; import TransactionListItemDetails from './transaction-list-item-details.component'; describe('TransactionListItemDetails Component', () => { @@ -15,7 +16,7 @@ describe('TransactionListItemDetails Component', () => { status: TRANSACTION_STATUSES.CONFIRMED, txParams: { from: '0x1', - gas: '0x5208', + gas: GAS_LIMITS.SIMPLE, gasPrice: '0x3b9aca00', nonce: '0xa4', to: '0x2', @@ -57,7 +58,7 @@ describe('TransactionListItemDetails Component', () => { status: TRANSACTION_STATUSES.CONFIRMED, txParams: { from: '0x1', - gas: '0x5208', + gas: GAS_LIMITS.SIMPLE, gasPrice: '0x3b9aca00', nonce: '0xa4', to: '0x2', @@ -102,7 +103,7 @@ describe('TransactionListItemDetails Component', () => { status: 'confirmed', txParams: { from: '0x1', - gas: '0x5208', + gas: GAS_LIMITS.SIMPLE, gasPrice: '0x3b9aca00', nonce: '0xa4', to: '0x2', @@ -146,7 +147,7 @@ describe('TransactionListItemDetails Component', () => { hash: '0xaa', txParams: { from: '0x1', - gas: '0x5208', + gas: GAS_LIMITS.SIMPLE, gasPrice: '0x3b9aca00', nonce: '0xa4', to: '0x2', diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js index aa1aba7e7..e572d8adb 100644 --- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js +++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js @@ -22,7 +22,9 @@ export default function UserPreferencedCurrencyDisplay({ const prefixComponent = useMemo(() => { return ( currency === ETH && - showEthLogo && + showEthLogo && ( + + ) ); }, [currency, showEthLogo, ethLogoHeight]); diff --git a/ui/components/app/wallet-overview/token-overview.js b/ui/components/app/wallet-overview/token-overview.js index 4b71e3dba..9bdb87533 100644 --- a/ui/components/app/wallet-overview/token-overview.js +++ b/ui/components/app/wallet-overview/token-overview.js @@ -17,7 +17,7 @@ import { } from '../../../hooks/useMetricEvent'; import { useTokenTracker } from '../../../hooks/useTokenTracker'; import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount'; -import { updateSendToken } from '../../../store/actions'; +import { updateSendToken } from '../../../ducks/send/send.duck'; import { setSwapsFromToken } from '../../../ducks/swaps/swaps'; import { getAssetImages, diff --git a/ui/components/app/whats-new-popup/whats-new-popup.js b/ui/components/app/whats-new-popup/whats-new-popup.js index c4fc2abe5..9b944135a 100644 --- a/ui/components/app/whats-new-popup/whats-new-popup.js +++ b/ui/components/app/whats-new-popup/whats-new-popup.js @@ -12,8 +12,8 @@ import Typography from '../../ui/typography'; import { updateViewedNotifications } from '../../../store/actions'; import { getTranslatedUINoficiations } from '../../../../shared/notifications'; import { getSortedNotificationsToShow } from '../../../selectors'; -import { TYPOGRAPHY } from '../../../helpers/constants/design-system'; import { BUILD_QUOTE_ROUTE } from '../../../helpers/constants/routes'; +import { TYPOGRAPHY } from '../../../helpers/constants/design-system'; function getActionFunctionById(id, history) { const actionFunctions = { diff --git a/ui/components/ui/alert/index.js b/ui/components/ui/alert/index.js index 44677cf0b..19a18126e 100644 --- a/ui/components/ui/alert/index.js +++ b/ui/components/ui/alert/index.js @@ -1,6 +1,7 @@ import classnames from 'classnames'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { MILLISECOND } from '../../../../shared/constants/time'; class Alert extends Component { state = { @@ -33,7 +34,7 @@ class Alert extends Component { setTimeout((_) => { this.setState({ visible: false }); - }, 500); + }, MILLISECOND * 500); } render() { diff --git a/ui/components/ui/callout/callout.js b/ui/components/ui/callout/callout.js index 7da13c2b9..bba3efdf6 100644 --- a/ui/components/ui/callout/callout.js +++ b/ui/components/ui/callout/callout.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; import InfoIconInverted from '../icon/info-icon-inverted.component'; import { SEVERITIES } from '../../../helpers/constants/design-system'; +import { MILLISECOND } from '../../../../shared/constants/time'; export default function Callout({ severity, @@ -29,7 +30,7 @@ export default function Callout({ if (removed) { setTimeout(() => { dismiss(); - }, 500); + }, MILLISECOND * 500); } }, [removed, dismiss]); return ( diff --git a/ui/components/ui/error-message/error-message.component.js b/ui/components/ui/error-message/error-message.component.js index 399c7c24c..542ecc47b 100644 --- a/ui/components/ui/error-message/error-message.component.js +++ b/ui/components/ui/error-message/error-message.component.js @@ -7,7 +7,11 @@ const ErrorMessage = (props, context) => { return (
- +
{`ALERT: ${error}`}
); diff --git a/ui/components/ui/metafox-logo/metafox-logo.component.js b/ui/components/ui/metafox-logo/metafox-logo.component.js index 363bf422c..2b0b36c87 100644 --- a/ui/components/ui/metafox-logo/metafox-logo.component.js +++ b/ui/components/ui/metafox-logo/metafox-logo.component.js @@ -25,7 +25,7 @@ export default class MetaFoxLogo extends PureComponent { > { const t = useI18nContext(); return ( @@ -32,7 +33,12 @@ const Popover = ({ > {showArrow ?
: null}
-
+

{onBack ? (

{subtitle ? (

{subtitle}

@@ -76,7 +84,7 @@ Popover.propTypes = { footer: PropTypes.node, footerClassName: PropTypes.string, onBack: PropTypes.func, - onClose: PropTypes.func.isRequired, + onClose: PropTypes.func, CustomBackground: PropTypes.func, contentClassName: PropTypes.string, className: PropTypes.string, @@ -84,6 +92,7 @@ Popover.propTypes = { popoverRef: PropTypes.shape({ current: PropTypes.instanceOf(window.Element), }), + centerTitle: PropTypes.bool, }; export default class PopoverPortal extends PureComponent { diff --git a/ui/ducks/app/app.js b/ui/ducks/app/app.js index 20bab88f7..b19072d18 100644 --- a/ui/ducks/app/app.js +++ b/ui/ducks/app/app.js @@ -37,7 +37,6 @@ export default function reduceApp(state = {}, action) { warning: null, buyView: {}, isMouseUser: false, - gasIsLoading: false, defaultHdPaths: { trezor: `m/44'/60'/0'/0`, ledger: `m/44'/60'/0'/0/0`, @@ -293,18 +292,6 @@ export default function reduceApp(state = {}, action) { isMouseUser: action.value, }; - case actionConstants.GAS_LOADING_STARTED: - return { - ...appState, - gasIsLoading: true, - }; - - case actionConstants.GAS_LOADING_FINISHED: - return { - ...appState, - gasIsLoading: false, - }; - case actionConstants.SET_SELECTED_SETTINGS_RPC_URL: return { ...appState, @@ -377,3 +364,8 @@ export function hideWhatsNewPopup() { type: actionConstants.HIDE_WHATS_NEW_POPUP, }; } + +// Selectors +export function getQrCodeData(state) { + return state.appState.qrCodeData; +} diff --git a/ui/ducks/app/app.test.js b/ui/ducks/app/app.test.js index 7a25bfe2b..6f536c903 100644 --- a/ui/ducks/app/app.test.js +++ b/ui/ducks/app/app.test.js @@ -352,22 +352,4 @@ describe('App State', () => { expect(state.isMouseUser).toStrictEqual(true); }); - - it('sets gas loading', () => { - const state = reduceApp(metamaskState, { - type: actions.GAS_LOADING_STARTED, - }); - - expect(state.gasIsLoading).toStrictEqual(true); - }); - - it('unsets gas loading', () => { - const gasLoadingState = { gasIsLoading: true }; - const oldState = { ...metamaskState, ...gasLoadingState }; - const state = reduceApp(oldState, { - type: actions.GAS_LOADING_FINISHED, - }); - - expect(state.gasIsLoading).toStrictEqual(false); - }); }); diff --git a/ui/ducks/confirm-transaction/confirm-transaction.duck.js b/ui/ducks/confirm-transaction/confirm-transaction.duck.js index a59785c7a..09e74a2e8 100644 --- a/ui/ducks/confirm-transaction/confirm-transaction.duck.js +++ b/ui/ducks/confirm-transaction/confirm-transaction.duck.js @@ -2,8 +2,8 @@ import { conversionRateSelector, currentCurrencySelector, unconfirmedTransactionsHashSelector, - getNativeCurrency, } from '../../selectors'; +import { getNativeCurrency } from '../metamask/metamask'; import { getValueFromWeiHex, diff --git a/ui/ducks/gas/gas.duck.js b/ui/ducks/gas/gas.duck.js index 863d726fa..e991e5e73 100644 --- a/ui/ducks/gas/gas.duck.js +++ b/ui/ducks/gas/gas.duck.js @@ -11,13 +11,13 @@ import { import { getIsMainnet, getCurrentChainId } from '../../selectors'; import fetchWithCache from '../../helpers/utils/fetch-with-cache'; -const BASIC_ESTIMATE_STATES = { +export const BASIC_ESTIMATE_STATES = { LOADING: 'LOADING', FAILED: 'FAILED', READY: 'READY', }; -const GAS_SOURCE = { +export const GAS_SOURCE = { METASWAPS: 'MetaSwaps', ETHGASPRICE: 'eth_gasprice', }; diff --git a/ui/ducks/metamask/metamask.js b/ui/ducks/metamask/metamask.js index 093f3e925..21a0476ea 100644 --- a/ui/ducks/metamask/metamask.js +++ b/ui/ducks/metamask/metamask.js @@ -1,6 +1,10 @@ import * as actionConstants from '../../store/actionConstants'; import { ALERT_TYPES } from '../../../shared/constants/alerts'; import { NETWORK_TYPE_RPC } from '../../../shared/constants/network'; +import { + accountsWithSendEtherInfoSelector, + getAddressBook, +} from '../../selectors'; export default function reduceMetamask(state = {}, action) { const metamaskState = { @@ -15,26 +19,11 @@ export default function reduceMetamask(state = {}, action) { tokens: [], pendingTokens: {}, customNonceValue: '', - send: { - gasLimit: null, - gasPrice: null, - gasTotal: null, - tokenBalance: '0x0', - from: '', - to: '', - amount: '0', - memo: '', - errors: {}, - maxModeOn: false, - editingTransactionId: null, - toNickname: '', - ensResolution: null, - ensResolutionError: '', - }, useBlockie: false, featureFlags: {}, welcomeScreenSeen: false, currentLocale: '', + currentBlockGasLimit: '', preferences: { autoLockTimeLimit: undefined, showFiatInTestnets: false, @@ -46,6 +35,8 @@ export default function reduceMetamask(state = {}, action) { participateInMetaMetrics: null, metaMetricsSendCount: 0, nextNonce: null, + conversionRate: null, + nativeCurrency: 'ETH', ...state, }; @@ -99,28 +90,11 @@ export default function reduceMetamask(state = {}, action) { tokens: action.newTokens, }; - // metamask.send - case actionConstants.UPDATE_GAS_LIMIT: - return { - ...metamaskState, - send: { - ...metamaskState.send, - gasLimit: action.value, - }, - }; case actionConstants.UPDATE_CUSTOM_NONCE: return { ...metamaskState, customNonceValue: action.value, }; - case actionConstants.UPDATE_GAS_PRICE: - return { - ...metamaskState, - send: { - ...metamaskState.send, - gasPrice: action.value, - }, - }; case actionConstants.TOGGLE_ACCOUNT_MENU: return { @@ -128,139 +102,6 @@ export default function reduceMetamask(state = {}, action) { isAccountMenuOpen: !metamaskState.isAccountMenuOpen, }; - case actionConstants.UPDATE_GAS_TOTAL: - return { - ...metamaskState, - send: { - ...metamaskState.send, - gasTotal: action.value, - }, - }; - - case actionConstants.UPDATE_SEND_TOKEN_BALANCE: - return { - ...metamaskState, - send: { - ...metamaskState.send, - tokenBalance: action.value, - }, - }; - - case actionConstants.UPDATE_SEND_HEX_DATA: - return { - ...metamaskState, - send: { - ...metamaskState.send, - data: action.value, - }, - }; - - case actionConstants.UPDATE_SEND_TO: - return { - ...metamaskState, - send: { - ...metamaskState.send, - to: action.value.to, - toNickname: action.value.nickname, - }, - }; - - case actionConstants.UPDATE_SEND_AMOUNT: - return { - ...metamaskState, - send: { - ...metamaskState.send, - amount: action.value, - }, - }; - - case actionConstants.UPDATE_MAX_MODE: - return { - ...metamaskState, - send: { - ...metamaskState.send, - maxModeOn: action.value, - }, - }; - - case actionConstants.UPDATE_SEND: - return Object.assign(metamaskState, { - send: { - ...metamaskState.send, - ...action.value, - }, - }); - - case actionConstants.UPDATE_SEND_TOKEN: { - const newSend = { - ...metamaskState.send, - token: action.value, - }; - // erase token-related state when switching back to native currency - if (newSend.editingTransactionId && !newSend.token) { - const unapprovedTx = - newSend?.unapprovedTxs?.[newSend.editingTransactionId] || {}; - const txParams = unapprovedTx.txParams || {}; - Object.assign(newSend, { - tokenBalance: null, - balance: '0', - from: unapprovedTx.from || '', - unapprovedTxs: { - ...newSend.unapprovedTxs, - [newSend.editingTransactionId]: { - ...unapprovedTx, - txParams: { - ...txParams, - data: '', - }, - }, - }, - }); - } - return Object.assign(metamaskState, { - send: newSend, - }); - } - - case actionConstants.UPDATE_SEND_ENS_RESOLUTION: - return { - ...metamaskState, - send: { - ...metamaskState.send, - ensResolution: action.payload, - ensResolutionError: '', - }, - }; - - case actionConstants.UPDATE_SEND_ENS_RESOLUTION_ERROR: - return { - ...metamaskState, - send: { - ...metamaskState.send, - ensResolution: null, - ensResolutionError: action.payload, - }, - }; - - case actionConstants.CLEAR_SEND: - return { - ...metamaskState, - send: { - gasLimit: null, - gasPrice: null, - gasTotal: null, - tokenBalance: null, - from: '', - to: '', - amount: '0x0', - memo: '', - errors: {}, - maxModeOn: false, - editingTransactionId: null, - toNickname: '', - }, - }; - case actionConstants.UPDATE_TRANSACTION_PARAMS: { const { id: txId, value } = action; let { currentNetworkTxList } = metamaskState; @@ -378,3 +219,29 @@ export const getUnconnectedAccountAlertShown = (state) => state.metamask.unconnectedAccountAlertShownOrigins; export const getTokens = (state) => state.metamask.tokens; + +export function getBlockGasLimit(state) { + return state.metamask.currentBlockGasLimit; +} + +export function getConversionRate(state) { + return state.metamask.conversionRate; +} + +export function getNativeCurrency(state) { + return state.metamask.nativeCurrency; +} + +export function getSendHexDataFeatureFlagState(state) { + return state.metamask.featureFlags.sendHexData; +} + +export function getSendToAccounts(state) { + const fromAccounts = accountsWithSendEtherInfoSelector(state); + const addressBookAccounts = getAddressBook(state); + return [...fromAccounts, ...addressBookAccounts]; +} + +export function getUnapprovedTxs(state) { + return state.metamask.unapprovedTxs; +} diff --git a/ui/ducks/metamask/metamask.test.js b/ui/ducks/metamask/metamask.test.js index c830cd4a2..702c7c319 100644 --- a/ui/ducks/metamask/metamask.test.js +++ b/ui/ducks/metamask/metamask.test.js @@ -1,9 +1,111 @@ +import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction'; import * as actionConstants from '../../store/actionConstants'; -import reduceMetamask from './metamask'; +import reduceMetamask, { + getBlockGasLimit, + getConversionRate, + getNativeCurrency, + getSendHexDataFeatureFlagState, + getSendToAccounts, + getUnapprovedTxs, +} from './metamask'; describe('MetaMask Reducers', () => { + const mockState = { + metamask: reduceMetamask( + { + isInitialized: true, + isUnlocked: true, + featureFlags: { sendHexData: true }, + identities: { + '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': { + address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', + name: 'Send Account 1', + }, + '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb': { + address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', + name: 'Send Account 2', + }, + '0x2f8d4a878cfa04a6e60d46362f5644deab66572d': { + address: '0x2f8d4a878cfa04a6e60d46362f5644deab66572d', + name: 'Send Account 3', + }, + '0xd85a4b6a394794842887b8284293d69163007bbb': { + address: '0xd85a4b6a394794842887b8284293d69163007bbb', + name: 'Send Account 4', + }, + }, + cachedBalances: {}, + currentBlockGasLimit: '0x4c1878', + conversionRate: 1200.88200327, + nativeCurrency: 'ETH', + network: '3', + provider: { + type: 'testnet', + chainId: '0x3', + }, + accounts: { + '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': { + code: '0x', + balance: '0x47c9d71831c76efe', + nonce: '0x1b', + address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', + }, + '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb': { + code: '0x', + balance: '0x37452b1315889f80', + nonce: '0xa', + address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', + }, + '0x2f8d4a878cfa04a6e60d46362f5644deab66572d': { + code: '0x', + balance: '0x30c9d71831c76efe', + nonce: '0x1c', + address: '0x2f8d4a878cfa04a6e60d46362f5644deab66572d', + }, + '0xd85a4b6a394794842887b8284293d69163007bbb': { + code: '0x', + balance: '0x0', + nonce: '0x0', + address: '0xd85a4b6a394794842887b8284293d69163007bbb', + }, + }, + addressBook: { + '0x3': { + '0x06195827297c7a80a443b6894d3bdb8824b43896': { + address: '0x06195827297c7a80a443b6894d3bdb8824b43896', + name: 'Address Book Account 1', + chainId: '0x3', + }, + }, + }, + unapprovedTxs: { + 4768706228115573: { + id: 4768706228115573, + time: 1487363153561, + status: TRANSACTION_STATUSES.UNAPPROVED, + gasMultiplier: 1, + metamaskNetworkId: '3', + txParams: { + from: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', + to: '0x18a3462427bcc9133bb46e88bcbe39cd7ef0e761', + value: '0xde0b6b3a7640000', + metamaskId: 4768706228115573, + metamaskNetworkId: '3', + gas: '0x5209', + }, + txFee: '17e0186e60800', + txValue: 'de0b6b3a7640000', + maxCost: 'de234b52e4a0800', + gasPrice: '4a817c800', + }, + }, + }, + {}, + ), + }; it('init state', () => { const initState = reduceMetamask(undefined, {}); + expect.anything(initState); }); @@ -48,13 +150,11 @@ describe('MetaMask Reducers', () => { {}, { type: actionConstants.SHOW_ACCOUNT_DETAIL, - value: 'test address', }, ); expect(state.isUnlocked).toStrictEqual(true); expect(state.isInitialized).toStrictEqual(true); - expect(state.selectedAddress).toStrictEqual('test address'); }); it('sets account label', () => { @@ -92,30 +192,6 @@ describe('MetaMask Reducers', () => { expect(state.tokens).toStrictEqual(newTokens); }); - it('updates send gas limit', () => { - const state = reduceMetamask( - {}, - { - type: actionConstants.UPDATE_GAS_LIMIT, - value: '0xGasLimit', - }, - ); - - expect(state.send.gasLimit).toStrictEqual('0xGasLimit'); - }); - - it('updates send gas price', () => { - const state = reduceMetamask( - {}, - { - type: actionConstants.UPDATE_GAS_PRICE, - value: '0xGasPrice', - }, - ); - - expect(state.send.gasPrice).toStrictEqual('0xGasPrice'); - }); - it('toggles account menu', () => { const state = reduceMetamask( {}, @@ -127,153 +203,6 @@ describe('MetaMask Reducers', () => { expect(state.isAccountMenuOpen).toStrictEqual(true); }); - it('updates gas total', () => { - const state = reduceMetamask( - {}, - { - type: actionConstants.UPDATE_GAS_TOTAL, - value: '0xGasTotal', - }, - ); - - expect(state.send.gasTotal).toStrictEqual('0xGasTotal'); - }); - - it('updates send token balance', () => { - const state = reduceMetamask( - {}, - { - type: actionConstants.UPDATE_SEND_TOKEN_BALANCE, - value: '0xTokenBalance', - }, - ); - - expect(state.send.tokenBalance).toStrictEqual('0xTokenBalance'); - }); - - it('updates data', () => { - const state = reduceMetamask( - {}, - { - type: actionConstants.UPDATE_SEND_HEX_DATA, - value: '0xData', - }, - ); - - expect(state.send.data).toStrictEqual('0xData'); - }); - - it('updates send to', () => { - const state = reduceMetamask( - {}, - { - type: actionConstants.UPDATE_SEND_TO, - value: { - to: '0xAddress', - nickname: 'nickname', - }, - }, - ); - - expect(state.send.to).toStrictEqual('0xAddress'); - expect(state.send.toNickname).toStrictEqual('nickname'); - }); - - it('update send amount', () => { - const state = reduceMetamask( - {}, - { - type: actionConstants.UPDATE_SEND_AMOUNT, - value: '0xAmount', - }, - ); - - expect(state.send.amount).toStrictEqual('0xAmount'); - }); - - it('updates max mode', () => { - const state = reduceMetamask( - {}, - { - type: actionConstants.UPDATE_MAX_MODE, - value: true, - }, - ); - - expect(state.send.maxModeOn).toStrictEqual(true); - }); - - it('update send', () => { - const value = { - gasLimit: '0xGasLimit', - gasPrice: '0xGasPrice', - gasTotal: '0xGasTotal', - tokenBalance: '0xBalance', - from: '0xAddress', - to: '0xAddress', - toNickname: '', - maxModeOn: false, - amount: '0xAmount', - memo: '0xMemo', - errors: {}, - editingTransactionId: 22, - ensResolution: null, - ensResolutionError: '', - }; - - const sendState = reduceMetamask( - {}, - { - type: actionConstants.UPDATE_SEND, - value, - }, - ); - - expect(sendState.send).toStrictEqual(value); - }); - - it('clears send', () => { - const initStateSend = { - send: { - gasLimit: null, - gasPrice: null, - gasTotal: null, - tokenBalance: null, - from: '', - to: '', - amount: '0x0', - memo: '', - errors: {}, - maxModeOn: false, - editingTransactionId: null, - toNickname: '', - }, - }; - - const sendState = { - send: { - gasLimit: '0xGasLimit', - gasPrice: '0xGasPrice', - gasTotal: '0xGasTotal', - tokenBalance: '0xBalance', - from: '0xAddress', - to: '0xAddress', - toNickname: '', - maxModeOn: false, - amount: '0xAmount', - memo: '0xMemo', - errors: {}, - editingTransactionId: 22, - }, - }; - - const state = reduceMetamask(sendState, { - type: actionConstants.CLEAR_SEND, - }); - - expect(state.send).toStrictEqual(initStateSend.send); - }); - it('updates value of tx by id', () => { const oldState = { currentNetworkTxList: [ @@ -378,29 +307,93 @@ describe('MetaMask Reducers', () => { expect(state.pendingTokens).toStrictEqual({}); }); - it('update ensResolution', () => { - const state = reduceMetamask( - {}, - { - type: actionConstants.UPDATE_SEND_ENS_RESOLUTION, - payload: '0x1337', - }, - ); + describe('metamask state selectors', () => { + describe('getBlockGasLimit', () => { + it('should return the current block gas limit', () => { + expect(getBlockGasLimit(mockState)).toStrictEqual('0x4c1878'); + }); + }); - expect(state.send.ensResolution).toStrictEqual('0x1337'); - expect(state.send.ensResolutionError).toStrictEqual(''); - }); + describe('getConversionRate()', () => { + it('should return the eth conversion rate', () => { + expect(getConversionRate(mockState)).toStrictEqual(1200.88200327); + }); + }); - it('update ensResolutionError', () => { - const state = reduceMetamask( - {}, - { - type: actionConstants.UPDATE_SEND_ENS_RESOLUTION_ERROR, - payload: 'ens name not found', - }, - ); + describe('getNativeCurrency()', () => { + it('should return the ticker symbol of the selected network', () => { + expect(getNativeCurrency(mockState)).toStrictEqual('ETH'); + }); + }); + + describe('getSendHexDataFeatureFlagState()', () => { + it('should return the sendHexData feature flag state', () => { + expect(getSendHexDataFeatureFlagState(mockState)).toStrictEqual(true); + }); + }); - expect(state.send.ensResolutionError).toStrictEqual('ens name not found'); - expect(state.send.ensResolution).toBeNull(); + describe('getSendToAccounts()', () => { + it('should return an array including all the users accounts and the address book', () => { + expect(getSendToAccounts(mockState)).toStrictEqual([ + { + code: '0x', + balance: '0x47c9d71831c76efe', + nonce: '0x1b', + address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', + name: 'Send Account 1', + }, + { + code: '0x', + balance: '0x37452b1315889f80', + nonce: '0xa', + address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', + name: 'Send Account 2', + }, + { + code: '0x', + balance: '0x30c9d71831c76efe', + nonce: '0x1c', + address: '0x2f8d4a878cfa04a6e60d46362f5644deab66572d', + name: 'Send Account 3', + }, + { + code: '0x', + balance: '0x0', + nonce: '0x0', + address: '0xd85a4b6a394794842887b8284293d69163007bbb', + name: 'Send Account 4', + }, + { + address: '0x06195827297c7a80a443b6894d3bdb8824b43896', + name: 'Address Book Account 1', + chainId: '0x3', + }, + ]); + }); + }); + + it('should return the unapproved txs', () => { + expect(getUnapprovedTxs(mockState)).toStrictEqual({ + 4768706228115573: { + id: 4768706228115573, + time: 1487363153561, + status: TRANSACTION_STATUSES.UNAPPROVED, + gasMultiplier: 1, + metamaskNetworkId: '3', + txParams: { + from: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', + to: '0x18a3462427bcc9133bb46e88bcbe39cd7ef0e761', + value: '0xde0b6b3a7640000', + metamaskId: 4768706228115573, + metamaskNetworkId: '3', + gas: '0x5209', + }, + txFee: '17e0186e60800', + txValue: 'de0b6b3a7640000', + maxCost: 'de234b52e4a0800', + gasPrice: '4a817c800', + }, + }); + }); }); }); diff --git a/ui/ducks/send/send-duck.test.js b/ui/ducks/send/send-duck.test.js index 12ef5bbb3..7c05e8689 100644 --- a/ui/ducks/send/send-duck.test.js +++ b/ui/ducks/send/send-duck.test.js @@ -12,8 +12,22 @@ describe('Send Duck', () => { }; const initState = { toDropdownOpen: false, - errors: {}, gasButtonGroupShown: true, + errors: {}, + gasLimit: null, + gasPrice: null, + gasTotal: null, + tokenBalance: '0x0', + from: '', + to: '', + amount: '0', + memo: '', + maxModeOn: false, + editingTransactionId: null, + toNickname: '', + ensResolution: null, + ensResolutionError: '', + gasIsLoading: false, }; const OPEN_TO_DROPDOWN = 'metamask/send/OPEN_TO_DROPDOWN'; const CLOSE_TO_DROPDOWN = 'metamask/send/CLOSE_TO_DROPDOWN'; diff --git a/ui/ducks/send/send.duck.js b/ui/ducks/send/send.duck.js index b8d974457..82d9b9d82 100644 --- a/ui/ducks/send/send.duck.js +++ b/ui/ducks/send/send.duck.js @@ -1,3 +1,11 @@ +import log from 'loglevel'; +import { estimateGas } from '../../store/actions'; +import { setCustomGasLimit } from '../gas/gas.duck'; +import { + estimateGasForSend, + calcTokenBalance, +} from '../../pages/send/send.utils'; + // Actions const OPEN_TO_DROPDOWN = 'metamask/send/OPEN_TO_DROPDOWN'; const CLOSE_TO_DROPDOWN = 'metamask/send/CLOSE_TO_DROPDOWN'; @@ -5,11 +13,40 @@ const UPDATE_SEND_ERRORS = 'metamask/send/UPDATE_SEND_ERRORS'; const RESET_SEND_STATE = 'metamask/send/RESET_SEND_STATE'; const SHOW_GAS_BUTTON_GROUP = 'metamask/send/SHOW_GAS_BUTTON_GROUP'; const HIDE_GAS_BUTTON_GROUP = 'metamask/send/HIDE_GAS_BUTTON_GROUP'; +const UPDATE_GAS_LIMIT = 'UPDATE_GAS_LIMIT'; +const UPDATE_GAS_PRICE = 'UPDATE_GAS_PRICE'; +const UPDATE_GAS_TOTAL = 'UPDATE_GAS_TOTAL'; +const UPDATE_SEND_HEX_DATA = 'UPDATE_SEND_HEX_DATA'; +const UPDATE_SEND_TOKEN_BALANCE = 'UPDATE_SEND_TOKEN_BALANCE'; +const UPDATE_SEND_TO = 'UPDATE_SEND_TO'; +const UPDATE_SEND_AMOUNT = 'UPDATE_SEND_AMOUNT'; +const UPDATE_MAX_MODE = 'UPDATE_MAX_MODE'; +const UPDATE_SEND = 'UPDATE_SEND'; +const UPDATE_SEND_TOKEN = 'UPDATE_SEND_TOKEN'; +const CLEAR_SEND = 'CLEAR_SEND'; +const GAS_LOADING_STARTED = 'GAS_LOADING_STARTED'; +const GAS_LOADING_FINISHED = 'GAS_LOADING_FINISHED'; +const UPDATE_SEND_ENS_RESOLUTION = 'UPDATE_SEND_ENS_RESOLUTION'; +const UPDATE_SEND_ENS_RESOLUTION_ERROR = 'UPDATE_SEND_ENS_RESOLUTION_ERROR'; const initState = { toDropdownOpen: false, gasButtonGroupShown: true, errors: {}, + gasLimit: null, + gasPrice: null, + gasTotal: null, + tokenBalance: '0x0', + from: '', + to: '', + amount: '0', + memo: '', + maxModeOn: false, + editingTransactionId: null, + toNickname: '', + ensResolution: null, + ensResolutionError: '', + gasIsLoading: false, }; // Reducer @@ -43,8 +80,118 @@ export default function reducer(state = initState, action) { ...state, gasButtonGroupShown: false, }; + case UPDATE_GAS_LIMIT: + return { + ...state, + gasLimit: action.value, + }; + case UPDATE_GAS_PRICE: + return { + ...state, + gasPrice: action.value, + }; case RESET_SEND_STATE: return { ...initState }; + case UPDATE_GAS_TOTAL: + return { + ...state, + gasTotal: action.value, + }; + case UPDATE_SEND_TOKEN_BALANCE: + return { + ...state, + tokenBalance: action.value, + }; + case UPDATE_SEND_HEX_DATA: + return { + ...state, + data: action.value, + }; + case UPDATE_SEND_TO: + return { + ...state, + to: action.value.to, + toNickname: action.value.nickname, + }; + case UPDATE_SEND_AMOUNT: + return { + ...state, + amount: action.value, + }; + case UPDATE_MAX_MODE: + return { + ...state, + maxModeOn: action.value, + }; + case UPDATE_SEND: + return Object.assign(state, action.value); + case UPDATE_SEND_TOKEN: { + const newSend = { + ...state, + token: action.value, + }; + // erase token-related state when switching back to native currency + if (newSend.editingTransactionId && !newSend.token) { + const unapprovedTx = + newSend?.unapprovedTxs?.[newSend.editingTransactionId] || {}; + const txParams = unapprovedTx.txParams || {}; + Object.assign(newSend, { + tokenBalance: null, + balance: '0', + from: unapprovedTx.from || '', + unapprovedTxs: { + ...newSend.unapprovedTxs, + [newSend.editingTransactionId]: { + ...unapprovedTx, + txParams: { + ...txParams, + data: '', + }, + }, + }, + }); + } + return Object.assign(state, newSend); + } + case UPDATE_SEND_ENS_RESOLUTION: + return { + ...state, + ensResolution: action.payload, + ensResolutionError: '', + }; + case UPDATE_SEND_ENS_RESOLUTION_ERROR: + return { + ...state, + ensResolution: null, + ensResolutionError: action.payload, + }; + case CLEAR_SEND: + return { + ...state, + gasLimit: null, + gasPrice: null, + gasTotal: null, + tokenBalance: null, + from: '', + to: '', + amount: '0x0', + memo: '', + errors: {}, + maxModeOn: false, + editingTransactionId: null, + toNickname: '', + }; + case GAS_LOADING_STARTED: + return { + ...state, + gasIsLoading: true, + }; + + case GAS_LOADING_FINISHED: + return { + ...state, + gasIsLoading: false, + }; default: return state; } @@ -77,3 +224,159 @@ export function updateSendErrors(errorObject) { export function resetSendState() { return { type: RESET_SEND_STATE }; } + +export function setGasLimit(gasLimit) { + return { + type: UPDATE_GAS_LIMIT, + value: gasLimit, + }; +} + +export function setGasPrice(gasPrice) { + return { + type: UPDATE_GAS_PRICE, + value: gasPrice, + }; +} + +export function setGasTotal(gasTotal) { + return { + type: UPDATE_GAS_TOTAL, + value: gasTotal, + }; +} + +export function updateGasData({ + gasPrice, + blockGasLimit, + selectedAddress, + sendToken, + to, + value, + data, +}) { + return (dispatch) => { + dispatch(gasLoadingStarted()); + return estimateGasForSend({ + estimateGasMethod: estimateGas, + blockGasLimit, + selectedAddress, + sendToken, + to, + value, + estimateGasPrice: gasPrice, + data, + }) + .then((gas) => { + dispatch(setGasLimit(gas)); + dispatch(setCustomGasLimit(gas)); + dispatch(updateSendErrors({ gasLoadingError: null })); + dispatch(gasLoadingFinished()); + }) + .catch((err) => { + log.error(err); + dispatch(updateSendErrors({ gasLoadingError: 'gasLoadingError' })); + dispatch(gasLoadingFinished()); + }); + }; +} + +export function gasLoadingStarted() { + return { + type: GAS_LOADING_STARTED, + }; +} + +export function gasLoadingFinished() { + return { + type: GAS_LOADING_FINISHED, + }; +} + +export function updateSendTokenBalance({ sendToken, tokenContract, address }) { + return (dispatch) => { + const tokenBalancePromise = tokenContract + ? tokenContract.balanceOf(address) + : Promise.resolve(); + return tokenBalancePromise + .then((usersToken) => { + if (usersToken) { + const newTokenBalance = calcTokenBalance({ sendToken, usersToken }); + dispatch(setSendTokenBalance(newTokenBalance)); + } + }) + .catch((err) => { + log.error(err); + updateSendErrors({ tokenBalance: 'tokenBalanceError' }); + }); + }; +} + +export function setSendTokenBalance(tokenBalance) { + return { + type: UPDATE_SEND_TOKEN_BALANCE, + value: tokenBalance, + }; +} + +export function updateSendHexData(value) { + return { + type: UPDATE_SEND_HEX_DATA, + value, + }; +} + +export function updateSendTo(to, nickname = '') { + return { + type: UPDATE_SEND_TO, + value: { to, nickname }, + }; +} + +export function updateSendAmount(amount) { + return { + type: UPDATE_SEND_AMOUNT, + value: amount, + }; +} + +export function setMaxModeTo(bool) { + return { + type: UPDATE_MAX_MODE, + value: bool, + }; +} + +export function updateSend(newSend) { + return { + type: UPDATE_SEND, + value: newSend, + }; +} + +export function updateSendToken(token) { + return { + type: UPDATE_SEND_TOKEN, + value: token, + }; +} + +export function clearSend() { + return { + type: CLEAR_SEND, + }; +} + +export function updateSendEnsResolution(ensResolution) { + return { + type: UPDATE_SEND_ENS_RESOLUTION, + payload: ensResolution, + }; +} + +export function updateSendEnsResolutionError(errorMessage) { + return { + type: UPDATE_SEND_ENS_RESOLUTION_ERROR, + payload: errorMessage, + }; +} diff --git a/ui/helpers/utils/account-link.js b/ui/helpers/utils/account-link.js deleted file mode 100644 index 47d36908a..000000000 --- a/ui/helpers/utils/account-link.js +++ /dev/null @@ -1,12 +0,0 @@ -import { createAccountLinkForChain } from '@metamask/etherscan-link'; - -export default function getAccountLink(address, chainId, rpcPrefs) { - if (rpcPrefs && rpcPrefs.blockExplorerUrl) { - return `${rpcPrefs.blockExplorerUrl.replace( - /\/+$/u, - '', - )}/address/${address}`; - } - - return createAccountLinkForChain(address, chainId); -} diff --git a/ui/helpers/utils/account-link.test.js b/ui/helpers/utils/account-link.test.js deleted file mode 100644 index b5c430b87..000000000 --- a/ui/helpers/utils/account-link.test.js +++ /dev/null @@ -1,49 +0,0 @@ -import { - MAINNET_CHAIN_ID, - ROPSTEN_CHAIN_ID, -} from '../../../shared/constants/network'; -import getAccountLink from './account-link'; - -describe('Account link', () => { - describe('getAccountLink', () => { - it('should return the correct block explorer url for an account', () => { - const tests = [ - { - expected: 'https://etherscan.io/address/0xabcd', - chainId: MAINNET_CHAIN_ID, - address: '0xabcd', - }, - { - expected: 'https://ropsten.etherscan.io/address/0xdef0', - chainId: ROPSTEN_CHAIN_ID, - address: '0xdef0', - rpcPrefs: {}, - }, - { - // test handling of `blockExplorerUrl` for a custom RPC - expected: 'https://block.explorer/address/0xabcd', - chainId: '0x21', - address: '0xabcd', - rpcPrefs: { - blockExplorerUrl: 'https://block.explorer', - }, - }, - { - // test handling of trailing `/` in `blockExplorerUrl` for a custom RPC - expected: 'https://another.block.explorer/address/0xdef0', - chainId: '0x1f', - address: '0xdef0', - rpcPrefs: { - blockExplorerUrl: 'https://another.block.explorer/', - }, - }, - ]; - - tests.forEach(({ expected, address, chainId, rpcPrefs }) => { - expect(getAccountLink(address, chainId, rpcPrefs)).toStrictEqual( - expected, - ); - }); - }); - }); -}); diff --git a/ui/helpers/utils/confirm-tx.util.test.js b/ui/helpers/utils/confirm-tx.util.test.js index 2dc8af0ce..71cd49a5a 100644 --- a/ui/helpers/utils/confirm-tx.util.test.js +++ b/ui/helpers/utils/confirm-tx.util.test.js @@ -1,3 +1,4 @@ +import { GAS_LIMITS } from '../../../shared/constants/gas'; import * as utils from './confirm-tx.util'; describe('Confirm Transaction utils', () => { @@ -34,7 +35,10 @@ describe('Confirm Transaction utils', () => { describe('getHexGasTotal', () => { it('should multiply the hex gasLimit and hex gasPrice values together', () => { expect( - utils.getHexGasTotal({ gasLimit: '0x5208', gasPrice: '0x3b9aca00' }), + utils.getHexGasTotal({ + gasLimit: GAS_LIMITS.SIMPLE, + gasPrice: '0x3b9aca00', + }), ).toStrictEqual('0x1319718a5000'); }); diff --git a/ui/helpers/utils/fetch-with-cache.js b/ui/helpers/utils/fetch-with-cache.js index f810864cc..377c3d51f 100644 --- a/ui/helpers/utils/fetch-with-cache.js +++ b/ui/helpers/utils/fetch-with-cache.js @@ -1,10 +1,11 @@ +import { MINUTE, SECOND } from '../../../shared/constants/time'; import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout'; import { getStorageItem, setStorageItem } from './storage-helpers'; const fetchWithCache = async ( url, fetchOptions = {}, - { cacheRefreshTime = 360000, timeout = 30000 } = {}, + { cacheRefreshTime = MINUTE * 6, timeout = SECOND * 30 } = {}, ) => { if ( fetchOptions.body || diff --git a/ui/helpers/utils/i18n-helper.js b/ui/helpers/utils/i18n-helper.js index 611d1f665..d9d7a35a8 100644 --- a/ui/helpers/utils/i18n-helper.js +++ b/ui/helpers/utils/i18n-helper.js @@ -3,9 +3,10 @@ import React from 'react'; import log from 'loglevel'; import * as Sentry from '@sentry/browser'; +import { SECOND } from '../../../shared/constants/time'; import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout'; -const fetchWithTimeout = getFetchWithTimeout(30000); +const fetchWithTimeout = getFetchWithTimeout(SECOND * 30); const warned = {}; const missingMessageErrors = {}; diff --git a/ui/hooks/useCancelTransaction.js b/ui/hooks/useCancelTransaction.js index 03bad3c54..62cf65d72 100644 --- a/ui/hooks/useCancelTransaction.js +++ b/ui/hooks/useCancelTransaction.js @@ -7,16 +7,15 @@ import { getHexGasTotal, increaseLastGasPrice, } from '../helpers/utils/confirm-tx.util'; -import { - getConversionRate, - getSelectedAccount, - getIsMainnet, -} from '../selectors'; +import { getSelectedAccount, getIsMainnet } from '../selectors'; +import { getConversionRate } from '../ducks/metamask/metamask'; + import { setCustomGasLimit, setCustomGasPriceForRetry, } from '../ducks/gas/gas.duck'; import { multiplyCurrencies } from '../helpers/utils/conversion-util'; +import { GAS_LIMITS } from '../../shared/constants/gas'; /** * Determine whether a transaction can be cancelled and provide a method to @@ -52,13 +51,13 @@ export function useCancelTransaction(transactionGroup) { const cancelTransaction = useCallback( (event) => { event.stopPropagation(); - dispatch(setCustomGasLimit('0x5208')); + dispatch(setCustomGasLimit(GAS_LIMITS.SIMPLE)); dispatch(setCustomGasPriceForRetry(defaultNewGasPrice)); const tx = { ...transaction, txParams: { ...transaction.txParams, - gas: '0x5208', + gas: GAS_LIMITS.SIMPLE, value: '0x0', }, }; diff --git a/ui/hooks/useCancelTransaction.test.js b/ui/hooks/useCancelTransaction.test.js index 28453bc6c..6bc391475 100644 --- a/ui/hooks/useCancelTransaction.test.js +++ b/ui/hooks/useCancelTransaction.test.js @@ -6,6 +6,7 @@ import { getConversionRate, getSelectedAccount } from '../selectors'; import { showModal } from '../store/actions'; import { increaseLastGasPrice } from '../helpers/utils/confirm-tx.util'; import * as actionConstants from '../store/actionConstants'; +import { GAS_LIMITS } from '../../shared/constants/gas'; import { useCancelTransaction } from './useCancelTransaction'; describe('useCancelTransaction', function () { @@ -77,7 +78,7 @@ describe('useCancelTransaction', function () { // call onSubmit myself dispatchAction[dispatchAction.length - 1][0].value.props.onSubmit( - '0x5208', + GAS_LIMITS.SIMPLE, '0x1', ); @@ -86,9 +87,9 @@ describe('useCancelTransaction', function () { showModal({ name: 'CANCEL_TRANSACTION', transactionId, - newGasFee: '0x5208', + newGasFee: GAS_LIMITS.SIMPLE, defaultNewGasPrice: '0x1', - gasLimit: '0x5208', + gasLimit: GAS_LIMITS.SIMPLE, }), ), ).toStrictEqual(true); @@ -147,7 +148,7 @@ describe('useCancelTransaction', function () { ).toStrictEqual(transactionId); dispatchAction[dispatchAction.length - 1][0].value.props.onSubmit( - '0x5208', + GAS_LIMITS.SIMPLE, '0x1', ); @@ -156,9 +157,9 @@ describe('useCancelTransaction', function () { showModal({ name: 'CANCEL_TRANSACTION', transactionId, - newGasFee: '0x5208', + newGasFee: GAS_LIMITS.SIMPLE, defaultNewGasPrice: '0x1', - gasLimit: '0x5208', + gasLimit: GAS_LIMITS.SIMPLE, }), ), ).toStrictEqual(true); diff --git a/ui/hooks/useCopyToClipboard.js b/ui/hooks/useCopyToClipboard.js index 33635d722..6b0988369 100644 --- a/ui/hooks/useCopyToClipboard.js +++ b/ui/hooks/useCopyToClipboard.js @@ -1,5 +1,6 @@ import { useState, useCallback } from 'react'; import copyToClipboard from 'copy-to-clipboard'; +import { SECOND } from '../../shared/constants/time'; import { useTimeout } from './useTimeout'; /** @@ -9,7 +10,7 @@ import { useTimeout } from './useTimeout'; * * @return {[boolean, Function]} */ -const DEFAULT_DELAY = 3000; +const DEFAULT_DELAY = SECOND * 3; export function useCopyToClipboard(delay = DEFAULT_DELAY) { const [copied, setCopied] = useState(false); diff --git a/ui/hooks/useCurrencyDisplay.js b/ui/hooks/useCurrencyDisplay.js index 8b3f8271d..6b770e38e 100644 --- a/ui/hooks/useCurrencyDisplay.js +++ b/ui/hooks/useCurrencyDisplay.js @@ -4,11 +4,11 @@ import { formatCurrency, getValueFromWeiHex, } from '../helpers/utils/confirm-tx.util'; +import { getCurrentCurrency } from '../selectors'; import { - getCurrentCurrency, getConversionRate, getNativeCurrency, -} from '../selectors'; +} from '../ducks/metamask/metamask'; /** * Defines the shape of the options parameter for useCurrencyDisplay diff --git a/ui/hooks/useCurrencyDisplay.test.js b/ui/hooks/useCurrencyDisplay.test.js index 1231e5f14..89625a36b 100644 --- a/ui/hooks/useCurrencyDisplay.test.js +++ b/ui/hooks/useCurrencyDisplay.test.js @@ -1,11 +1,11 @@ import { renderHook } from '@testing-library/react-hooks'; import * as reactRedux from 'react-redux'; import sinon from 'sinon'; +import { getCurrentCurrency } from '../selectors'; import { - getCurrentCurrency, - getNativeCurrency, getConversionRate, -} from '../selectors'; + getNativeCurrency, +} from '../ducks/metamask/metamask'; import { useCurrencyDisplay } from './useCurrencyDisplay'; const tests = [ diff --git a/ui/hooks/useEthFiatAmount.js b/ui/hooks/useEthFiatAmount.js index 10ce71789..88ee9d9b9 100644 --- a/ui/hooks/useEthFiatAmount.js +++ b/ui/hooks/useEthFiatAmount.js @@ -1,12 +1,9 @@ import { useMemo } from 'react'; import { useSelector } from 'react-redux'; -import { - getConversionRate, - getCurrentCurrency, - getShouldShowFiat, -} from '../selectors'; +import { getCurrentCurrency, getShouldShowFiat } from '../selectors'; import { decEthToConvertedCurrency } from '../helpers/utils/conversions.util'; import { formatCurrency } from '../helpers/utils/confirm-tx.util'; +import { getConversionRate } from '../ducks/metamask/metamask'; /** * Get an Eth amount converted to fiat and formatted for display diff --git a/ui/hooks/useShouldShowSpeedUp.js b/ui/hooks/useShouldShowSpeedUp.js index 83c96ca3f..5a7e6e1f5 100644 --- a/ui/hooks/useShouldShowSpeedUp.js +++ b/ui/hooks/useShouldShowSpeedUp.js @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import { SECOND } from '../../shared/constants/time'; /** * Evaluates whether the transaction is eligible to be sped up, and registers @@ -24,7 +25,7 @@ export function useShouldShowSpeedUp(transactionGroup, isEarliestNonce) { // for determining enabled status change let timeoutId; if (!hasRetried && isEarliestNonce && !speedUpEnabled) { - if (Date.now() - submittedTime > 5000) { + if (Date.now() - submittedTime > SECOND * 5) { setSpeedUpEnabled(true); } else { timeoutId = setTimeout(() => { diff --git a/ui/hooks/useTokenFiatAmount.js b/ui/hooks/useTokenFiatAmount.js index 835b65936..1e3974525 100644 --- a/ui/hooks/useTokenFiatAmount.js +++ b/ui/hooks/useTokenFiatAmount.js @@ -2,11 +2,11 @@ import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { getTokenExchangeRates, - getConversionRate, getCurrentCurrency, getShouldShowFiat, } from '../selectors'; import { getTokenFiatAmount } from '../helpers/utils/token-util'; +import { getConversionRate } from '../ducks/metamask/metamask'; /** * Get the token balance converted to fiat and formatted for display diff --git a/ui/hooks/useTokenTracker.js b/ui/hooks/useTokenTracker.js index 448ec32b8..601254932 100644 --- a/ui/hooks/useTokenTracker.js +++ b/ui/hooks/useTokenTracker.js @@ -2,6 +2,7 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import TokenTracker from '@metamask/eth-token-tracker'; import { useSelector } from 'react-redux'; import { getCurrentChainId, getSelectedAddress } from '../selectors'; +import { SECOND } from '../../shared/constants/time'; import { useEqualityCheck } from './useEqualityCheck'; export function useTokenTracker( @@ -52,7 +53,7 @@ export function useTokenTracker( provider: global.ethereumProvider, tokens: tokenList, includeFailedTokens, - pollingInterval: 8000, + pollingInterval: SECOND * 8, }); tokenTracker.current.on('update', updateBalances); diff --git a/ui/hooks/useTokensToSearch.js b/ui/hooks/useTokensToSearch.js index 371530630..dd3d30f54 100644 --- a/ui/hooks/useTokensToSearch.js +++ b/ui/hooks/useTokensToSearch.js @@ -2,18 +2,19 @@ import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import contractMap from '@metamask/contract-metadata'; import BigNumber from 'bignumber.js'; -import { isEqual, shuffle } from 'lodash'; +import { isEqual, shuffle, uniqBy } from 'lodash'; import { getTokenFiatAmount } from '../helpers/utils/token-util'; import { getTokenExchangeRates, - getConversionRate, getCurrentCurrency, getSwapsDefaultToken, getCurrentChainId, } from '../selectors'; +import { getConversionRate } from '../ducks/metamask/metamask'; + import { getSwapsTokens } from '../ducks/swaps/swaps'; -import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { isSwapsDefaultTokenSymbol } from '../../shared/modules/swaps.utils'; +import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { useEqualityCheck } from './useEqualityCheck'; const tokenList = shuffle( @@ -119,7 +120,12 @@ export function useTokensToSearch({ usersTokens = [], topTokens = {} }) { others: [], }; - memoizedTokensToSearch.forEach((token) => { + const memoizedSwapsAndUserTokensWithoutDuplicities = uniqBy( + [...memoizedTokensToSearch, ...memoizedUsersToken], + 'address', + ); + + memoizedSwapsAndUserTokensWithoutDuplicities.forEach((token) => { const renderableDataToken = getRenderableTokenData( { ...usersTokensAddressMap[token.address], ...token }, tokenConversionRates, @@ -129,8 +135,7 @@ export function useTokensToSearch({ usersTokens = [], topTokens = {} }) { ); if ( isSwapsDefaultTokenSymbol(renderableDataToken.symbol, chainId) || - (usersTokensAddressMap[token.address] && - Number(renderableDataToken.balance ?? 0) !== 0) + usersTokensAddressMap[token.address] ) { tokensToSearchBuckets.owned.push(renderableDataToken); } else if (memoizedTopTokens[token.address]) { diff --git a/ui/hooks/useTransactionDisplayData.test.js b/ui/hooks/useTransactionDisplayData.test.js index e91566acf..3b36c255b 100644 --- a/ui/hooks/useTransactionDisplayData.test.js +++ b/ui/hooks/useTransactionDisplayData.test.js @@ -7,11 +7,10 @@ import transactions from '../../test/data/transaction-data.json'; import { getPreferences, getShouldShowFiat, - getNativeCurrency, getCurrentCurrency, getCurrentChainId, } from '../selectors'; -import { getTokens } from '../ducks/metamask/metamask'; +import { getTokens, getNativeCurrency } from '../ducks/metamask/metamask'; import { getMessage } from '../helpers/utils/i18n-helper'; import messages from '../../app/_locales/en/messages.json'; import { ASSET_ROUTE, DEFAULT_ROUTE } from '../helpers/constants/routes'; diff --git a/ui/hooks/useUserPreferencedCurrency.js b/ui/hooks/useUserPreferencedCurrency.js index d1a080954..50b742abf 100644 --- a/ui/hooks/useUserPreferencedCurrency.js +++ b/ui/hooks/useUserPreferencedCurrency.js @@ -1,9 +1,7 @@ import { useSelector } from 'react-redux'; -import { - getPreferences, - getShouldShowFiat, - getNativeCurrency, -} from '../selectors'; +import { getPreferences, getShouldShowFiat } from '../selectors'; +import { getNativeCurrency } from '../ducks/metamask/metamask'; + import { PRIMARY, SECONDARY, ETH } from '../helpers/constants/common'; /** diff --git a/ui/pages/add-token/add-token.component.js b/ui/pages/add-token/add-token.component.js index d5d02312e..36c215e2a 100644 --- a/ui/pages/add-token/add-token.component.js +++ b/ui/pages/add-token/add-token.component.js @@ -7,14 +7,14 @@ import { CONFIRM_ADD_TOKEN_ROUTE } from '../../helpers/constants/routes'; import TextField from '../../components/ui/text-field'; import PageContainer from '../../components/ui/page-container'; import { Tabs, Tab } from '../../components/ui/tabs'; -import { isValidHexAddress } from '../../../shared/modules/hexstring-utils'; import { addHexPrefix } from '../../../app/scripts/lib/util'; +import { isValidHexAddress } from '../../../shared/modules/hexstring-utils'; import ActionableMessage from '../swaps/actionable-message'; import Typography from '../../components/ui/typography'; import { TYPOGRAPHY, FONT_WEIGHT } from '../../helpers/constants/design-system'; import Button from '../../components/ui/button'; -import TokenList from './token-list'; import TokenSearch from './token-search'; +import TokenList from './token-list'; const emptyAddr = '0x0000000000000000000000000000000000000000'; diff --git a/ui/pages/asset/components/asset-options.js b/ui/pages/asset/components/asset-options.js index 3b3ee21c2..dbb4ba5c6 100644 --- a/ui/pages/asset/components/asset-options.js +++ b/ui/pages/asset/components/asset-options.js @@ -6,10 +6,11 @@ import { Menu, MenuItem } from '../../../components/ui/menu'; const AssetOptions = ({ onRemove, - onViewEtherscan, + onClickBlockExplorer, onViewAccountDetails, tokenSymbol, isNativeAsset, + isEthNetwork, }) => { const t = useContext(I18nContext); const [assetOptionsButtonElement, setAssetOptionsButtonElement] = useState( @@ -46,10 +47,10 @@ const AssetOptions = ({ data-testid="asset-options__etherscan" onClick={() => { setAssetOptionsOpen(false); - onViewEtherscan(); + onClickBlockExplorer(); }} > - {t('viewOnEtherscan')} + {isEthNetwork ? t('viewOnEtherscan') : t('viewinExplorer')} {isNativeAsset ? null : ( @@ -33,12 +45,14 @@ export default function NativeAsset({ nativeCurrency }) { accountName={selectedAccountName} assetName={nativeCurrency} onBack={() => history.push(DEFAULT_ROUTE)} + isEthNetwork={!rpcPrefs.blockExplorerUrl} optionsButton={ { + onClickBlockExplorer={() => { + blockExplorerLinkClickedEvent(); global.platform.openTab({ - url: getAccountLink(address, chainId, rpcPrefs), + url: accountLink, }); }} onViewAccountDetails={() => { diff --git a/ui/pages/asset/components/token-asset.js b/ui/pages/asset/components/token-asset.js index 76566037e..c5b6410c0 100644 --- a/ui/pages/asset/components/token-asset.js +++ b/ui/pages/asset/components/token-asset.js @@ -2,16 +2,17 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { createTokenTrackerLinkForChain } from '@metamask/etherscan-link'; - +import { getTokenTrackerLink } from '@metamask/etherscan-link'; import TransactionList from '../../../components/app/transaction-list'; import { TokenOverview } from '../../../components/app/wallet-overview'; import { getCurrentChainId, getSelectedIdentity, + getRpcPrefsForCurrentProvider, } from '../../../selectors/selectors'; import { DEFAULT_ROUTE } from '../../../helpers/constants/routes'; import { showModal } from '../../../store/actions'; +import { useNewMetricEvent } from '../../../hooks/useMetricEvent'; import AssetNavigation from './asset-navigation'; import AssetOptions from './asset-options'; @@ -19,10 +20,30 @@ import AssetOptions from './asset-options'; export default function TokenAsset({ token }) { const dispatch = useDispatch(); const chainId = useSelector(getCurrentChainId); + const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); const selectedIdentity = useSelector(getSelectedIdentity); const selectedAccountName = selectedIdentity.name; const selectedAddress = selectedIdentity.address; const history = useHistory(); + const tokenTrackerLink = getTokenTrackerLink( + token.address, + chainId, + null, + selectedAddress, + rpcPrefs, + ); + + const blockExplorerLinkClickedEvent = useNewMetricEvent({ + category: 'Navigation', + event: 'Clicked Block Explorer Link', + properties: { + link_type: 'Token Tracker', + action: 'Token Options', + block_explorer_domain: tokenTrackerLink + ? new URL(tokenTrackerLink)?.hostname + : '', + }, + }); return ( <> @@ -35,13 +56,10 @@ export default function TokenAsset({ token }) { onRemove={() => dispatch(showModal({ name: 'HIDE_TOKEN_CONFIRMATION', token })) } - onViewEtherscan={() => { - const url = createTokenTrackerLinkForChain( - token.address, - chainId, - selectedAddress, - ); - global.platform.openTab({ url }); + isEthNetwork={!rpcPrefs.blockExplorerUrl} + onClickBlockExplorer={() => { + blockExplorerLinkClickedEvent(); + global.platform.openTab({ url: tokenTrackerLink }); }} onViewAccountDetails={() => { dispatch(showModal({ name: 'ACCOUNT_DETAILS' })); diff --git a/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js b/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js index 8f586d65e..05bb4c77b 100644 --- a/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js +++ b/ui/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js @@ -350,7 +350,7 @@ export default class ConfirmApproveContent extends Component {
{this.renderApproveContentCard({ - symbol: , + symbol: , title: 'Permission', content: this.renderPermissionContent(), showEdit: true, diff --git a/ui/pages/confirm-approve/confirm-approve.js b/ui/pages/confirm-approve/confirm-approve.js index a0257e7a4..2283122cd 100644 --- a/ui/pages/confirm-approve/confirm-approve.js +++ b/ui/pages/confirm-approve/confirm-approve.js @@ -14,13 +14,12 @@ import { getTokenValueParam, } from '../../helpers/utils/token-util'; import { useTokenTracker } from '../../hooks/useTokenTracker'; -import { getTokens } from '../../ducks/metamask/metamask'; +import { getTokens, getNativeCurrency } from '../../ducks/metamask/metamask'; import { transactionFeeSelector, txDataSelector, getCurrentCurrency, getDomainMetadata, - getNativeCurrency, getUseNonceField, getCustomNonceValue, getNextSuggestedNonce, @@ -28,6 +27,7 @@ import { getIsEthGasPriceFetched, getIsMainnet, } from '../../selectors'; + import { currentNetworkTxListSelector } from '../../selectors/transactions'; import Loading from '../../components/ui/loading-screen'; import { getCustomTxParamsData } from './confirm-approve.util'; diff --git a/ui/pages/confirm-approve/confirm-approve.stories.js b/ui/pages/confirm-approve/confirm-approve.stories.js new file mode 100644 index 000000000..35ee95e31 --- /dev/null +++ b/ui/pages/confirm-approve/confirm-approve.stories.js @@ -0,0 +1,67 @@ +/* eslint-disable react/prop-types */ +import React, { useEffect } from 'react'; +import { text } from '@storybook/addon-knobs'; +import { useParams } from 'react-router-dom'; +import { useSelector } from 'react-redux'; +import { updateMetamaskState } from '../../store/actions'; +import { currentNetworkTxListSelector } from '../../selectors/transactions'; +import { store } from '../../../.storybook/preview'; + +import { + currentNetworkTxListSample, + domainMetadata, +} from '../../../.storybook/initial-states/approval-screens/token-approval'; +import ConfirmApprove from '.'; + +export default { + title: 'Confirmation Screens', +}; + +// transaction ID, maps to entry in state.metamask.currentNetworkTxList +const txId = 7900715443136469; + +const PageSet = ({ children }) => { + const origin = text('Origin', 'https://metamask.github.io'); + const domainIconUrl = text( + 'Icon URL', + 'https://metamask.github.io/test-dapp/metamask-fox.svg', + ); + + const currentNetworkTxList = useSelector(currentNetworkTxListSelector); + const transaction = currentNetworkTxList.find(({ id }) => id === txId); + + useEffect(() => { + transaction.origin = origin; + store.dispatch( + updateMetamaskState({ currentNetworkTxList: [transaction] }), + ); + }, [origin, transaction]); + + useEffect(() => { + store.dispatch( + updateMetamaskState({ + domainMetadata: { + [origin]: { + icon: domainIconUrl, + }, + }, + }), + ); + }, [domainIconUrl, origin]); + + const params = useParams(); + params.id = txId; + return children; +}; + +export const ApproveTokens = () => { + store.dispatch( + updateMetamaskState({ currentNetworkTxList: [currentNetworkTxListSample] }), + ); + store.dispatch(updateMetamaskState({ domainMetadata })); + return ( + + + + ); +}; diff --git a/ui/pages/confirm-decrypt-message/confirm-decrypt-message.component.js b/ui/pages/confirm-decrypt-message/confirm-decrypt-message.component.js index ffbbccf19..378c2cfd2 100644 --- a/ui/pages/confirm-decrypt-message/confirm-decrypt-message.component.js +++ b/ui/pages/confirm-decrypt-message/confirm-decrypt-message.component.js @@ -10,6 +10,7 @@ import Tooltip from '../../components/ui/tooltip'; import Copy from '../../components/ui/icon/copy-icon.component'; import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../shared/constants/app'; +import { SECOND } from '../../../shared/constants/time'; import { getEnvironmentType } from '../../../app/scripts/lib/util'; import { conversionUtil } from '../../helpers/utils/conversion-util'; @@ -91,7 +92,7 @@ export default class ConfirmDecryptMessage extends Component { }, }); this.setState({ hasCopied: true }); - setTimeout(() => this.setState({ hasCopied: false }), 3000); + setTimeout(() => this.setState({ hasCopied: false }), SECOND * 3); }; renderHeader = () => { diff --git a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js index 2382ea0bc..358c71f3c 100644 --- a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js +++ b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js @@ -30,6 +30,7 @@ export default class ConfirmEncryptionPublicKey extends Component { txData: PropTypes.object, domainMetadata: PropTypes.object, mostRecentOverviewPage: PropTypes.string.isRequired, + nativeCurrency: PropTypes.string.isRequired, }; state = { @@ -108,13 +109,13 @@ export default class ConfirmEncryptionPublicKey extends Component { }; renderBalance = () => { - const { conversionRate } = this.props; + const { conversionRate, nativeCurrency } = this.props; const { t } = this.context; const { fromAccount: { balance }, } = this.state; - const balanceInEther = conversionUtil(balance, { + const nativeCurrencyBalance = conversionUtil(balance, { fromNumericBase: 'hex', toNumericBase: 'dec', fromDenomination: 'WEI', @@ -128,7 +129,7 @@ export default class ConfirmEncryptionPublicKey extends Component { {`${t('balance')}:`}
- {`${balanceInEther} ETH`} + {`${nativeCurrencyBalance} ${nativeCurrency}`}
); diff --git a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.container.js b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.container.js index 3439589b6..a7e569ca6 100644 --- a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.container.js +++ b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.container.js @@ -16,6 +16,7 @@ import { import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck'; import { getMostRecentOverviewPage } from '../../ducks/history/history'; +import { getNativeCurrency } from '../../ducks/metamask/metamask'; import ConfirmEncryptionPublicKey from './confirm-encryption-public-key.component'; function mapStateToProps(state) { @@ -39,6 +40,7 @@ function mapStateToProps(state) { requesterAddress: null, conversionRate: conversionRateSelector(state), mostRecentOverviewPage: getMostRecentOverviewPage(state), + nativeCurrency: getNativeCurrency(state), }; } diff --git a/ui/pages/confirm-send-ether/confirm-send-ether.container.js b/ui/pages/confirm-send-ether/confirm-send-ether.container.js index 35f1ccd73..475ee5213 100644 --- a/ui/pages/confirm-send-ether/confirm-send-ether.container.js +++ b/ui/pages/confirm-send-ether/confirm-send-ether.container.js @@ -1,7 +1,7 @@ import { connect } from 'react-redux'; import { compose } from 'redux'; import { withRouter } from 'react-router-dom'; -import { updateSend } from '../../store/actions'; +import { updateSend } from '../../ducks/send/send.duck'; import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck'; import ConfirmSendEther from './confirm-send-ether.component'; diff --git a/ui/pages/confirm-send-token/confirm-send-token.container.js b/ui/pages/confirm-send-token/confirm-send-token.container.js index e7514acf9..fd869a9a7 100644 --- a/ui/pages/confirm-send-token/confirm-send-token.container.js +++ b/ui/pages/confirm-send-token/confirm-send-token.container.js @@ -2,13 +2,14 @@ import { connect } from 'react-redux'; import { compose } from 'redux'; import { withRouter } from 'react-router-dom'; import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck'; -import { updateSend, showSendTokenPage } from '../../store/actions'; +import { showSendTokenPage } from '../../store/actions'; import { conversionUtil } from '../../helpers/utils/conversion-util'; import { getTokenValueParam, getTokenAddressParam, } from '../../helpers/utils/token-util'; import { sendTokenTokenAmountAndToAddressSelector } from '../../selectors'; +import { updateSend } from '../../ducks/send/send.duck'; import ConfirmSendToken from './confirm-send-token.component'; const mapStateToProps = (state) => { diff --git a/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js b/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js index a03cee3ef..b15a5f99e 100644 --- a/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js +++ b/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js @@ -86,7 +86,7 @@ export default function ConfirmTokenTransactionBase({ primaryTotalTextOverride={
{`${tokensText} + `} - + {ethTransactionTotal}
} diff --git a/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js index cc637b2ba..8a8e9e47e 100644 --- a/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -27,8 +27,8 @@ import { TRANSACTION_STATUSES, } from '../../../shared/constants/transaction'; import { getTransactionTypeTitle } from '../../helpers/utils/transactions.util'; -import { toBuffer } from '../../../shared/modules/buffer-utils'; import ErrorMessage from '../../components/ui/error-message'; +import { toBuffer } from '../../../shared/modules/buffer-utils'; export default class ConfirmTransactionBase extends Component { static contextTypes = { diff --git a/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js index 181a216f3..6af7e49a9 100644 --- a/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -38,8 +38,8 @@ import { getIsEthGasPriceFetched, } from '../../selectors'; import { getMostRecentOverviewPage } from '../../ducks/history/history'; -import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils'; import { transactionMatchesNetwork } from '../../../shared/modules/transaction.utils'; +import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils'; import ConfirmTransactionBase from './confirm-transaction-base.component'; const casedContractMap = Object.keys(contractMap).reduce((acc, base) => { diff --git a/ui/pages/confirm-transaction/conf-tx.js b/ui/pages/confirm-transaction/conf-tx.js index 23f6e2375..4f2028197 100644 --- a/ui/pages/confirm-transaction/conf-tx.js +++ b/ui/pages/confirm-transaction/conf-tx.js @@ -38,7 +38,7 @@ function mapStateToProps(state) { unapprovedMsgCount, unapprovedPersonalMsgCount, unapprovedTypedMessagesCount, - send: state.metamask.send, + send: state.send, currentNetworkTxList: state.metamask.currentNetworkTxList, }; } diff --git a/ui/pages/confirm-transaction/confirm-transaction.container.js b/ui/pages/confirm-transaction/confirm-transaction.container.js index 21acaa7ff..b04ee76fa 100644 --- a/ui/pages/confirm-transaction/confirm-transaction.container.js +++ b/ui/pages/confirm-transaction/confirm-transaction.container.js @@ -19,7 +19,8 @@ import ConfirmTransaction from './confirm-transaction.component'; const mapStateToProps = (state, ownProps) => { const { - metamask: { send, unapprovedTxs }, + metamask: { unapprovedTxs }, + send, } = state; const { match: { params = {} }, diff --git a/ui/pages/create-account/connect-hardware/account-list.js b/ui/pages/create-account/connect-hardware/account-list.js index ae3b43437..a3bb775df 100644 --- a/ui/pages/create-account/connect-hardware/account-list.js +++ b/ui/pages/create-account/connect-hardware/account-list.js @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import getAccountLink from '../../../helpers/utils/account-link'; +import { getAccountLink } from '@metamask/etherscan-link'; + import Button from '../../../components/ui/button'; import Checkbox from '../../../components/ui/check-box'; import Dropdown from '../../../components/ui/dropdown'; @@ -83,7 +84,7 @@ class AccountList extends Component { } renderAccounts() { - const { accounts, connectedAccounts } = this.props; + const { accounts, connectedAccounts, rpcPrefs, chainId } = this.props; return (
@@ -130,11 +131,27 @@ class AccountList extends Component {
{ + const accountLink = getAccountLink( + account.address, + chainId, + rpcPrefs, + ); + this.context.trackEvent({ + category: 'Account', + event: 'Clicked Block Explorer Link', + properties: { + actions: 'Hardware Connect', + link_type: 'Account Tracker', + block_explorer_domain: accountLink + ? new URL(accountLink)?.hostname + : '', + }, + }); + global.platform.openTab({ + url: accountLink, + }); + }} target="_blank" rel="noopener noreferrer" title={this.context.t('etherscanView')} @@ -282,6 +299,7 @@ AccountList.propTypes = { AccountList.contextTypes = { t: PropTypes.func, + trackEvent: PropTypes.func, }; export default AccountList; diff --git a/ui/pages/create-account/connect-hardware/index.js b/ui/pages/create-account/connect-hardware/index.js index ef9cc3f80..501618727 100644 --- a/ui/pages/create-account/connect-hardware/index.js +++ b/ui/pages/create-account/connect-hardware/index.js @@ -10,6 +10,7 @@ import { } from '../../../selectors'; import { formatBalance } from '../../../helpers/utils/util'; import { getMostRecentOverviewPage } from '../../../ducks/history/history'; +import { SECOND } from '../../../../shared/constants/time'; import SelectHardware from './select-hardware'; import AccountList from './account-list'; @@ -100,7 +101,7 @@ class ConnectHardwareForm extends Component { // Autohide the alert after 5 seconds setTimeout((_) => { this.props.hideAlert(); - }, 5000); + }, SECOND * 5); } getPage = (device, page, hdPath) => { diff --git a/ui/pages/first-time-flow/select-action/select-action.component.js b/ui/pages/first-time-flow/select-action/select-action.component.js index 2f02df189..7420b1333 100644 --- a/ui/pages/first-time-flow/select-action/select-action.component.js +++ b/ui/pages/first-time-flow/select-action/select-action.component.js @@ -50,7 +50,7 @@ export default class SelectAction extends PureComponent {
- +
{t('noAlreadyHaveSeed')} @@ -70,7 +70,7 @@ export default class SelectAction extends PureComponent {
- +
{t('letsGoSetUp')} diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index faf86eb91..9ff9078f7 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -14,6 +14,7 @@ import ConnectedAccounts from '../connected-accounts'; import { Tabs, Tab } from '../../components/ui/tabs'; import { EthOverview } from '../../components/app/wallet-overview'; import WhatsNewPopup from '../../components/app/whats-new-popup'; +import RecoveryPhraseReminder from '../../components/app/recovery-phrase-reminder'; import { ASSET_ROUTE, @@ -76,6 +77,10 @@ export default class Home extends PureComponent { showWhatsNewPopup: PropTypes.bool.isRequired, hideWhatsNewPopup: PropTypes.func.isRequired, notificationsToShow: PropTypes.bool.isRequired, + showRecoveryPhraseReminder: PropTypes.bool.isRequired, + setRecoveryPhraseReminderHasBeenShown: PropTypes.func.isRequired, + setRecoveryPhraseReminderLastShown: PropTypes.func.isRequired, + seedPhraseBackedUp: PropTypes.bool.isRequired, }; state = { @@ -163,6 +168,15 @@ export default class Home extends PureComponent { } } + onRecoveryPhraseReminderClose = () => { + const { + setRecoveryPhraseReminderHasBeenShown, + setRecoveryPhraseReminderLastShown, + } = this.props; + setRecoveryPhraseReminderHasBeenShown(true); + setRecoveryPhraseReminderLastShown(new Date().getTime()); + }; + renderNotifications() { const { t } = this.context; const { @@ -325,6 +339,8 @@ export default class Home extends PureComponent { notificationsToShow, showWhatsNewPopup, hideWhatsNewPopup, + seedPhraseBackedUp, + showRecoveryPhraseReminder, } = this.props; if (forgottenPassword) { @@ -333,6 +349,8 @@ export default class Home extends PureComponent { return null; } + const showWhatsNew = notificationsToShow && showWhatsNewPopup; + return (
@@ -342,8 +360,12 @@ export default class Home extends PureComponent { exact />
- {notificationsToShow && showWhatsNewPopup ? ( - + {showWhatsNew ? : null} + {!showWhatsNew && showRecoveryPhraseReminder ? ( + ) : null} {isPopup && !connectedStatusPopoverHasBeenShown ? this.renderPopover() diff --git a/ui/pages/home/home.container.js b/ui/pages/home/home.container.js index f5ff5a966..2b989ae03 100644 --- a/ui/pages/home/home.container.js +++ b/ui/pages/home/home.container.js @@ -14,6 +14,7 @@ import { getInfuraBlocked, getShowWhatsNewPopup, getSortedNotificationsToShow, + getShowRecoveryPhraseReminder, } from '../../selectors'; import { @@ -25,6 +26,8 @@ import { setDefaultHomeActiveTabName, setWeb3ShimUsageAlertDismissed, setAlertEnabledness, + setRecoveryPhraseReminderHasBeenShown, + setRecoveryPhraseReminderLastShown, } from '../../store/actions'; import { setThreeBoxLastUpdated, hideWhatsNewPopup } from '../../ducks/app/app'; import { getWeb3ShimUsageAlertEnabledness } from '../../ducks/metamask/metamask'; @@ -107,6 +110,8 @@ const mapStateToProps = (state) => { infuraBlocked: getInfuraBlocked(state), notificationsToShow: getSortedNotificationsToShow(state).length > 0, showWhatsNewPopup: getShowWhatsNewPopup(state), + showRecoveryPhraseReminder: getShowRecoveryPhraseReminder(state), + seedPhraseBackedUp, }; }; @@ -132,6 +137,10 @@ const mapDispatchToProps = (dispatch) => ({ disableWeb3ShimUsageAlert: () => setAlertEnabledness(ALERT_TYPES.web3ShimUsage, false), hideWhatsNewPopup: () => dispatch(hideWhatsNewPopup()), + setRecoveryPhraseReminderHasBeenShown: () => + dispatch(setRecoveryPhraseReminderHasBeenShown()), + setRecoveryPhraseReminderLastShown: (lastShown) => + dispatch(setRecoveryPhraseReminderLastShown(lastShown)), }); export default compose( diff --git a/ui/pages/mobile-sync/mobile-sync.component.js b/ui/pages/mobile-sync/mobile-sync.component.js index 339bfa130..683310ba0 100644 --- a/ui/pages/mobile-sync/mobile-sync.component.js +++ b/ui/pages/mobile-sync/mobile-sync.component.js @@ -7,11 +7,12 @@ import qrCode from 'qrcode-generator'; import Button from '../../components/ui/button'; import LoadingScreen from '../../components/ui/loading-screen'; +import { MINUTE, SECOND } from '../../../shared/constants/time'; const PASSWORD_PROMPT_SCREEN = 'PASSWORD_PROMPT_SCREEN'; const REVEAL_SEED_SCREEN = 'REVEAL_SEED_SCREEN'; -const KEYS_GENERATION_TIME = 30000; -const IDLE_TIME = KEYS_GENERATION_TIME * 4; +const KEYS_GENERATION_TIME = SECOND * 30; +const IDLE_TIME = MINUTE * 2; export default class MobileSyncPage extends Component { static contextTypes = { diff --git a/ui/pages/permissions-connect/permissions-connect.component.js b/ui/pages/permissions-connect/permissions-connect.component.js index b9706faca..9a487dad3 100644 --- a/ui/pages/permissions-connect/permissions-connect.component.js +++ b/ui/pages/permissions-connect/permissions-connect.component.js @@ -3,12 +3,13 @@ import React, { Component } from 'react'; import { Switch, Route } from 'react-router-dom'; import { getEnvironmentType } from '../../../app/scripts/lib/util'; import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../shared/constants/app'; +import { MILLISECOND } from '../../../shared/constants/time'; import { DEFAULT_ROUTE } from '../../helpers/constants/routes'; import PermissionPageContainer from '../../components/app/permission-page-container'; import ChooseAccount from './choose-account'; import PermissionsRedirect from './redirect'; -const APPROVE_TIMEOUT = 1200; +const APPROVE_TIMEOUT = MILLISECOND * 1200; export default class PermissionConnect extends Component { static propTypes = { diff --git a/ui/pages/permissions-connect/permissions-connect.container.js b/ui/pages/permissions-connect/permissions-connect.container.js index abf349208..4e95406ec 100644 --- a/ui/pages/permissions-connect/permissions-connect.container.js +++ b/ui/pages/permissions-connect/permissions-connect.container.js @@ -2,12 +2,12 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { getPermissionsRequests, - getNativeCurrency, getAccountsWithLabels, getLastConnectedInfo, getDomainMetadata, getSelectedAddress, } from '../../selectors'; +import { getNativeCurrency } from '../../ducks/metamask/metamask'; import { formatDate } from '../../helpers/utils/util'; import { diff --git a/ui/pages/send/send-content/add-recipient/add-recipient.container.js b/ui/pages/send/send-content/add-recipient/add-recipient.container.js index 2e3ea94fc..c131ebb7f 100644 --- a/ui/pages/send/send-content/add-recipient/add-recipient.container.js +++ b/ui/pages/send/send-content/add-recipient/add-recipient.container.js @@ -7,7 +7,7 @@ import { getAddressBookEntry, } from '../../../../selectors'; -import { updateSendTo } from '../../../../store/actions'; +import { updateSendTo } from '../../../../ducks/send/send.duck'; import AddRecipient from './add-recipient.component'; export default connect(mapStateToProps, mapDispatchToProps)(AddRecipient); diff --git a/ui/pages/send/send-content/add-recipient/add-recipient.container.test.js b/ui/pages/send/send-content/add-recipient/add-recipient.container.test.js index 696ca926b..1d8e05bdc 100644 --- a/ui/pages/send/send-content/add-recipient/add-recipient.container.test.js +++ b/ui/pages/send/send-content/add-recipient/add-recipient.container.test.js @@ -1,6 +1,5 @@ import sinon from 'sinon'; - -import { updateSendTo } from '../../../../store/actions'; +import { updateSendTo } from '../../../../ducks/send/send.duck'; let mapStateToProps; let mapDispatchToProps; @@ -24,7 +23,7 @@ jest.mock('../../../../selectors', () => ({ ], })); -jest.mock('../../../../store/actions', () => ({ +jest.mock('../../../../ducks/send/send.duck.js', () => ({ updateSendTo: jest.fn(), })); diff --git a/ui/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js b/ui/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js index 593d3e57a..a2fe64b94 100644 --- a/ui/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js +++ b/ui/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js @@ -7,8 +7,11 @@ import { getSendMaxModeState, getBasicGasEstimateLoadingStatus, } from '../../../../../selectors'; -import { updateSendAmount, setMaxModeTo } from '../../../../../store/actions'; -import { updateSendErrors } from '../../../../../ducks/send/send.duck'; +import { + updateSendErrors, + updateSendAmount, + setMaxModeTo, +} from '../../../../../ducks/send/send.duck'; import { calcMaxAmount } from './amount-max-button.utils'; import AmountMaxButton from './amount-max-button.component'; diff --git a/ui/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.test.js b/ui/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.test.js index 90f95e6cb..cb86c88ff 100644 --- a/ui/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.test.js +++ b/ui/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.test.js @@ -1,8 +1,10 @@ import sinon from 'sinon'; -import { setMaxModeTo, updateSendAmount } from '../../../../../store/actions'; - -import { updateSendErrors } from '../../../../../ducks/send/send.duck'; +import { + updateSendErrors, + setMaxModeTo, + updateSendAmount, +} from '../../../../../ducks/send/send.duck'; let mapStateToProps; let mapDispatchToProps; @@ -28,11 +30,9 @@ jest.mock('./amount-max-button.utils.js', () => ({ calcMaxAmount: (mockObj) => mockObj.val + 1, })); -jest.mock('../../../../../store/actions', () => ({ +jest.mock('../../../../../ducks/send/send.duck', () => ({ setMaxModeTo: jest.fn(), updateSendAmount: jest.fn(), -})); -jest.mock('../../../../../ducks/send/send.duck', () => ({ updateSendErrors: jest.fn(), })); diff --git a/ui/pages/send/send-content/send-amount-row/send-amount-row.container.js b/ui/pages/send/send-content/send-amount-row/send-amount-row.container.js index f2d5b2e06..ea76e87a4 100644 --- a/ui/pages/send/send-content/send-amount-row/send-amount-row.container.js +++ b/ui/pages/send/send-content/send-amount-row/send-amount-row.container.js @@ -1,6 +1,5 @@ import { connect } from 'react-redux'; import { - getConversionRate, getGasTotal, getPrimaryCurrency, getSendToken, @@ -11,8 +10,12 @@ import { sendAmountIsInError, } from '../../../../selectors'; import { getAmountErrorObject, getGasFeeErrorObject } from '../../send.utils'; -import { setMaxModeTo, updateSendAmount } from '../../../../store/actions'; -import { updateSendErrors } from '../../../../ducks/send/send.duck'; +import { + updateSendErrors, + setMaxModeTo, + updateSendAmount, +} from '../../../../ducks/send/send.duck'; +import { getConversionRate } from '../../../../ducks/metamask/metamask'; import SendAmountRow from './send-amount-row.component'; export default connect(mapStateToProps, mapDispatchToProps)(SendAmountRow); diff --git a/ui/pages/send/send-content/send-amount-row/send-amount-row.container.test.js b/ui/pages/send/send-content/send-amount-row/send-amount-row.container.test.js index 6d3b06aef..edad05014 100644 --- a/ui/pages/send/send-content/send-amount-row/send-amount-row.container.test.js +++ b/ui/pages/send/send-content/send-amount-row/send-amount-row.container.test.js @@ -1,8 +1,10 @@ import sinon from 'sinon'; -import { setMaxModeTo, updateSendAmount } from '../../../../store/actions'; - -import { updateSendErrors } from '../../../../ducks/send/send.duck'; +import { + updateSendErrors, + setMaxModeTo, + updateSendAmount, +} from '../../../../ducks/send/send.duck'; let mapDispatchToProps; @@ -28,13 +30,10 @@ jest.mock('../../send.utils', () => ({ }), })); -jest.mock('../../../../store/actions', () => ({ - setMaxModeTo: jest.fn(), - updateSendAmount: jest.fn(), -})); - jest.mock('../../../../ducks/send/send.duck', () => ({ updateSendErrors: jest.fn(), + setMaxModeTo: jest.fn(), + updateSendAmount: jest.fn(), })); require('./send-amount-row.container.js'); diff --git a/ui/pages/send/send-content/send-asset-row/send-asset-row.container.js b/ui/pages/send/send-content/send-asset-row/send-asset-row.container.js index c511a47b8..61c659434 100644 --- a/ui/pages/send/send-content/send-asset-row/send-asset-row.container.js +++ b/ui/pages/send/send-content/send-asset-row/send-asset-row.container.js @@ -1,12 +1,12 @@ import { connect } from 'react-redux'; +import { getNativeCurrency } from '../../../../ducks/metamask/metamask'; import { getMetaMaskAccounts, - getNativeCurrency, getNativeCurrencyImage, getSendTokenAddress, getAssetImages, } from '../../../../selectors'; -import { updateSendToken } from '../../../../store/actions'; +import { updateSendToken } from '../../../../ducks/send/send.duck'; import SendAssetRow from './send-asset-row.component'; function mapStateToProps(state) { diff --git a/ui/pages/send/send-content/send-content.component.test.js b/ui/pages/send/send-content/send-content.component.test.js index ed4485d76..0e4eb69d8 100644 --- a/ui/pages/send/send-content/send-content.component.test.js +++ b/ui/pages/send/send-content/send-content.component.test.js @@ -12,8 +12,13 @@ import SendAssetRow from './send-asset-row/send-asset-row.container'; describe('SendContent Component', () => { let wrapper; + const defaultProps = { + showHexData: true, + gasIsExcessive: false, + }; + beforeEach(() => { - wrapper = shallow(, { + wrapper = shallow(, { context: { t: (str) => `${str}_t` }, }); }); diff --git a/ui/pages/send/send-content/send-gas-row/send-gas-row.container.js b/ui/pages/send/send-content/send-gas-row/send-gas-row.container.js index 32b0529c4..84d6886fb 100644 --- a/ui/pages/send/send-content/send-gas-row/send-gas-row.container.js +++ b/ui/pages/send/send-content/send-gas-row/send-gas-row.container.js @@ -1,6 +1,5 @@ import { connect } from 'react-redux'; import { - getConversionRate, getGasTotal, getGasPrice, getGasLimit, @@ -26,19 +25,18 @@ import { calcMaxAmount } from '../send-amount-row/amount-max-button/amount-max-b import { showGasButtonGroup, updateSendErrors, + setGasPrice, + setGasLimit, + setGasTotal, + updateSendAmount, } from '../../../../ducks/send/send.duck'; import { resetCustomData, setCustomGasPrice, setCustomGasLimit, } from '../../../../ducks/gas/gas.duck'; -import { - showModal, - setGasPrice, - setGasLimit, - setGasTotal, - updateSendAmount, -} from '../../../../store/actions'; +import { getConversionRate } from '../../../../ducks/metamask/metamask'; +import { showModal } from '../../../../store/actions'; import SendGasRow from './send-gas-row.component'; export default connect( diff --git a/ui/pages/send/send-content/send-gas-row/send-gas-row.container.test.js b/ui/pages/send/send-content/send-gas-row/send-gas-row.container.test.js index db322d25a..80757f230 100644 --- a/ui/pages/send/send-content/send-gas-row/send-gas-row.container.test.js +++ b/ui/pages/send/send-content/send-gas-row/send-gas-row.container.test.js @@ -1,11 +1,6 @@ import sinon from 'sinon'; -import { - showModal, - setGasPrice, - setGasTotal, - setGasLimit, -} from '../../../../store/actions'; +import { showModal } from '../../../../store/actions'; import { resetCustomData, @@ -13,7 +8,12 @@ import { setCustomGasLimit, } from '../../../../ducks/gas/gas.duck'; -import { showGasButtonGroup } from '../../../../ducks/send/send.duck'; +import { + showGasButtonGroup, + setGasPrice, + setGasTotal, + setGasLimit, +} from '../../../../ducks/send/send.duck'; let mapDispatchToProps; let mergeProps; @@ -39,13 +39,13 @@ jest.mock('../../send.utils.js', () => ({ jest.mock('../../../../store/actions', () => ({ showModal: jest.fn(), - setGasPrice: jest.fn(), - setGasTotal: jest.fn(), - setGasLimit: jest.fn(), })); jest.mock('../../../../ducks/send/send.duck', () => ({ showGasButtonGroup: jest.fn(), + setGasPrice: jest.fn(), + setGasTotal: jest.fn(), + setGasLimit: jest.fn(), })); jest.mock('../../../../ducks/gas/gas.duck', () => ({ diff --git a/ui/pages/send/send-content/send-hex-data-row/send-hex-data-row.container.js b/ui/pages/send/send-content/send-hex-data-row/send-hex-data-row.container.js index 20a8cad43..f645aff7a 100644 --- a/ui/pages/send/send-content/send-hex-data-row/send-hex-data-row.container.js +++ b/ui/pages/send/send-content/send-hex-data-row/send-hex-data-row.container.js @@ -1,12 +1,12 @@ import { connect } from 'react-redux'; -import { updateSendHexData } from '../../../../store/actions'; +import { updateSendHexData } from '../../../../ducks/send/send.duck'; import SendHexDataRow from './send-hex-data-row.component'; export default connect(mapStateToProps, mapDispatchToProps)(SendHexDataRow); function mapStateToProps(state) { return { - data: state.metamask.send.data, + data: state.send.data, }; } diff --git a/ui/pages/send/send-footer/send-footer.container.js b/ui/pages/send/send-footer/send-footer.container.js index e8ea039e1..8848255d3 100644 --- a/ui/pages/send/send-footer/send-footer.container.js +++ b/ui/pages/send/send-footer/send-footer.container.js @@ -1,7 +1,6 @@ import { connect } from 'react-redux'; import { addToAddressBook, - clearSend, signTokenTx, signTx, updateTransaction, @@ -15,10 +14,8 @@ import { getSendEditingTransactionId, getSendFromObject, getSendTo, - getSendToAccounts, getSendHexData, getTokenBalance, - getUnapprovedTxs, getSendErrors, isSendFormInError, getGasIsLoading, @@ -28,6 +25,11 @@ import { } from '../../../selectors'; import { getMostRecentOverviewPage } from '../../../ducks/history/history'; import { addHexPrefix } from '../../../../app/scripts/lib/util'; +import { + getSendToAccounts, + getUnapprovedTxs, +} from '../../../ducks/metamask/metamask'; +import { clearSend } from '../../../ducks/send/send.duck'; import SendFooter from './send-footer.component'; import { addressIsNew, @@ -86,7 +88,7 @@ function mapDispatchToProps(dispatch) { to, }); - sendToken + return sendToken ? dispatch(signTokenTx(sendToken.address, to, amount, txParams)) : dispatch(signTx(txParams)); }, diff --git a/ui/pages/send/send-footer/send-footer.container.test.js b/ui/pages/send/send-footer/send-footer.container.test.js index 7fb79cbae..3cb6e474e 100644 --- a/ui/pages/send/send-footer/send-footer.container.test.js +++ b/ui/pages/send/send-footer/send-footer.container.test.js @@ -1,11 +1,7 @@ import sinon from 'sinon'; +import { clearSend } from '../../../ducks/send/send.duck'; -import { - clearSend, - signTx, - signTokenTx, - addToAddressBook, -} from '../../../store/actions'; +import { signTx, signTokenTx, addToAddressBook } from '../../../store/actions'; import { addressIsNew, constructTxParams, @@ -23,12 +19,15 @@ jest.mock('react-redux', () => ({ jest.mock('../../../store/actions.js', () => ({ addToAddressBook: jest.fn(), - clearSend: jest.fn(), signTokenTx: jest.fn(), signTx: jest.fn(), updateTransaction: jest.fn(), })); +jest.mock('../../../ducks/send/send.duck.js', () => ({ + clearSend: jest.fn(), +})); + jest.mock('../../../selectors/send.js', () => ({ getGasLimit: (s) => `mockGasLimit:${s}`, getGasPrice: (s) => `mockGasPrice:${s}`, diff --git a/ui/pages/send/send-header/send-header.container.js b/ui/pages/send/send-header/send-header.container.js index 9f67cb2af..b66a9ba89 100644 --- a/ui/pages/send/send-header/send-header.container.js +++ b/ui/pages/send/send-header/send-header.container.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { clearSend } from '../../../store/actions'; +import { clearSend } from '../../../ducks/send/send.duck'; import { getTitleKey } from '../../../selectors'; import { getMostRecentOverviewPage } from '../../../ducks/history/history'; import SendHeader from './send-header.component'; diff --git a/ui/pages/send/send.constants.js b/ui/pages/send/send.constants.js index ffeaf0397..ba5113603 100644 --- a/ui/pages/send/send.constants.js +++ b/ui/pages/send/send.constants.js @@ -38,9 +38,6 @@ const KNOWN_RECIPIENT_ADDRESS_ERROR = 'knownAddressRecipient'; const CONTRACT_ADDRESS_ERROR = 'contractAddressError'; const CONFUSING_ENS_ERROR = 'confusingEnsDomain'; -const SIMPLE_GAS_COST = '0x5208'; // Hex for 21000, cost of a simple send. -const BASE_TOKEN_GAS_COST = '0x186a0'; // Hex for 100000, a base estimate for token transfers. - export { INSUFFICIENT_FUNDS_ERROR, INSUFFICIENT_TOKENS_ERROR, @@ -57,7 +54,5 @@ export { NEGATIVE_ETH_ERROR, REQUIRED_ERROR, CONFUSING_ENS_ERROR, - SIMPLE_GAS_COST, TOKEN_TRANSFER_FUNCTION_SIGNATURE, - BASE_TOKEN_GAS_COST, }; diff --git a/ui/pages/send/send.container.js b/ui/pages/send/send.container.js index 2c710bdc4..f942131dd 100644 --- a/ui/pages/send/send.container.js +++ b/ui/pages/send/send.container.js @@ -3,8 +3,6 @@ import { withRouter } from 'react-router-dom'; import { compose } from 'redux'; import { - getBlockGasLimit, - getConversionRate, getGasLimit, getGasPrice, getGasTotal, @@ -13,7 +11,6 @@ import { getSendTokenContract, getSendAmount, getSendEditingTransactionId, - getSendHexDataFeatureFlagState, getSendFromObject, getSendTo, getSendToNickname, @@ -26,19 +23,24 @@ import { getCurrentChainId, } from '../../selectors'; +import { showQrScanner, qrCodeDetected } from '../../store/actions'; import { + resetSendState, + updateSendErrors, updateSendTo, updateSendTokenBalance, updateGasData, setGasTotal, - showQrScanner, - qrCodeDetected, updateSendEnsResolution, updateSendEnsResolutionError, -} from '../../store/actions'; -import { resetSendState, updateSendErrors } from '../../ducks/send/send.duck'; +} from '../../ducks/send/send.duck'; import { fetchBasicGasEstimates } from '../../ducks/gas/gas.duck'; -import { getTokens } from '../../ducks/metamask/metamask'; +import { + getBlockGasLimit, + getConversionRate, + getSendHexDataFeatureFlagState, + getTokens, +} from '../../ducks/metamask/metamask'; import { isValidDomainName } from '../../helpers/utils/util'; import { calcGasTotal } from './send.utils'; import SendEther from './send.component'; diff --git a/ui/pages/send/send.container.test.js b/ui/pages/send/send.container.test.js index bb2aca2dd..3072b3243 100644 --- a/ui/pages/send/send.container.test.js +++ b/ui/pages/send/send.container.test.js @@ -4,9 +4,9 @@ import { updateSendTokenBalance, updateGasData, setGasTotal, -} from '../../store/actions'; - -import { updateSendErrors, resetSendState } from '../../ducks/send/send.duck'; + updateSendErrors, + resetSendState, +} from '../../ducks/send/send.duck'; let mapDispatchToProps; @@ -25,14 +25,12 @@ jest.mock('redux', () => ({ compose: (_, arg2) => () => arg2(), })); -jest.mock('../../store/actions', () => ({ - updateSendTokenBalance: jest.fn(), - updateGasData: jest.fn(), - setGasTotal: jest.fn(), -})); jest.mock('../../ducks/send/send.duck', () => ({ updateSendErrors: jest.fn(), resetSendState: jest.fn(), + updateSendTokenBalance: jest.fn(), + updateGasData: jest.fn(), + setGasTotal: jest.fn(), })); jest.mock('./send.utils.js', () => ({ diff --git a/ui/pages/send/send.utils.js b/ui/pages/send/send.utils.js index 889015a77..1d7fb3562 100644 --- a/ui/pages/send/send.utils.js +++ b/ui/pages/send/send.utils.js @@ -11,13 +11,12 @@ import { import { calcTokenAmount } from '../../helpers/utils/token-util'; import { addHexPrefix } from '../../../app/scripts/lib/util'; +import { GAS_LIMITS } from '../../../shared/constants/gas'; import { - BASE_TOKEN_GAS_COST, INSUFFICIENT_FUNDS_ERROR, INSUFFICIENT_TOKENS_ERROR, MIN_GAS_LIMIT_HEX, NEGATIVE_ETH_ERROR, - SIMPLE_GAS_COST, TOKEN_TRANSFER_FUNCTION_SIGNATURE, } from './send.constants'; @@ -208,10 +207,10 @@ async function estimateGasForSend({ // Geth will return '0x', and ganache-core v2.2.1 will return '0x0' const codeIsEmpty = !code || code === '0x' || code === '0x0'; if (codeIsEmpty) { - return SIMPLE_GAS_COST; + return GAS_LIMITS.SIMPLE; } } else if (sendToken && !to) { - return BASE_TOKEN_GAS_COST; + return GAS_LIMITS.BASE_TOKEN_ESTIMATE; } if (sendToken) { diff --git a/ui/pages/send/send.utils.test.js b/ui/pages/send/send.utils.test.js index 91b94d8ef..02b45f1fa 100644 --- a/ui/pages/send/send.utils.test.js +++ b/ui/pages/send/send.utils.test.js @@ -8,6 +8,7 @@ import { conversionUtil, } from '../../helpers/utils/conversion-util'; +import { GAS_LIMITS } from '../../../shared/constants/gas'; import { calcGasTotal, estimateGasForSend, @@ -23,8 +24,6 @@ import { } from './send.utils'; import { - BASE_TOKEN_GAS_COST, - SIMPLE_GAS_COST, INSUFFICIENT_FUNDS_ERROR, INSUFFICIENT_TOKENS_ERROR, } from './send.constants'; @@ -381,38 +380,38 @@ describe('send utils', () => { expect(result).toStrictEqual('0xabc16'); }); - it(`should return ${SIMPLE_GAS_COST} if ethQuery.getCode does not return '0x'`, async () => { + it(`should return ${GAS_LIMITS.SIMPLE} if ethQuery.getCode does not return '0x'`, async () => { expect(baseMockParams.estimateGasMethod.callCount).toStrictEqual(0); const result = await estimateGasForSend({ ...baseMockParams, to: '0x123', }); - expect(result).toStrictEqual(SIMPLE_GAS_COST); + expect(result).toStrictEqual(GAS_LIMITS.SIMPLE); }); - it(`should return ${SIMPLE_GAS_COST} if not passed a sendToken or truthy to address`, async () => { + it(`should return ${GAS_LIMITS.SIMPLE} if not passed a sendToken or truthy to address`, async () => { expect(baseMockParams.estimateGasMethod.callCount).toStrictEqual(0); const result = await estimateGasForSend({ ...baseMockParams, to: null }); - expect(result).toStrictEqual(SIMPLE_GAS_COST); + expect(result).toStrictEqual(GAS_LIMITS.SIMPLE); }); - it(`should not return ${SIMPLE_GAS_COST} if passed a sendToken`, async () => { + it(`should not return ${GAS_LIMITS.SIMPLE} if passed a sendToken`, async () => { expect(baseMockParams.estimateGasMethod.callCount).toStrictEqual(0); const result = await estimateGasForSend({ ...baseMockParams, to: '0x123', sendToken: { address: '0x0' }, }); - expect(result).not.toStrictEqual(SIMPLE_GAS_COST); + expect(result).not.toStrictEqual(GAS_LIMITS.SIMPLE); }); - it(`should return ${BASE_TOKEN_GAS_COST} if passed a sendToken but no to address`, async () => { + it(`should return ${GAS_LIMITS.BASE_TOKEN_ESTIMATE} if passed a sendToken but no to address`, async () => { const result = await estimateGasForSend({ ...baseMockParams, to: null, sendToken: { address: '0x0' }, }); - expect(result).toStrictEqual(BASE_TOKEN_GAS_COST); + expect(result).toStrictEqual(GAS_LIMITS.BASE_TOKEN_ESTIMATE); }); it(`should return the adjusted blockGasLimit if it fails with a 'Transaction execution error.'`, async () => { diff --git a/ui/pages/settings/advanced-tab/advanced-tab.component.test.js b/ui/pages/settings/advanced-tab/advanced-tab.component.test.js index 2cf193f52..eb78d3c21 100644 --- a/ui/pages/settings/advanced-tab/advanced-tab.component.test.js +++ b/ui/pages/settings/advanced-tab/advanced-tab.component.test.js @@ -15,6 +15,10 @@ describe('AdvancedTab Component', () => { setThreeBoxSyncingPermission={() => undefined} threeBoxDisabled threeBoxSyncingAllowed={false} + useLedgerLive={false} + setLedgerLivePreference={() => undefined} + setDismissSeedBackUpReminder={() => undefined} + dismissSeedBackUpReminder={false} />, { context: { @@ -37,6 +41,10 @@ describe('AdvancedTab Component', () => { setThreeBoxSyncingPermission={() => undefined} threeBoxDisabled threeBoxSyncingAllowed={false} + useLedgerLive={false} + setLedgerLivePreference={() => undefined} + setDismissSeedBackUpReminder={() => undefined} + dismissSeedBackUpReminder={false} />, { context: { diff --git a/ui/pages/settings/contact-list-tab/contact-list-tab.component.js b/ui/pages/settings/contact-list-tab/contact-list-tab.component.js index 53e38c0a3..1c8e132a0 100644 --- a/ui/pages/settings/contact-list-tab/contact-list-tab.component.js +++ b/ui/pages/settings/contact-list-tab/contact-list-tab.component.js @@ -49,7 +49,7 @@ export default class ContactListTab extends Component { return (
- Address book icon + Address book icon

{t('builContactList')}

{t('addFriendsAndAddresses')} diff --git a/ui/pages/settings/networks-tab/network-form/network-form.component.js b/ui/pages/settings/networks-tab/network-form/network-form.component.js index 07163a6a8..8381c921d 100644 --- a/ui/pages/settings/networks-tab/network-form/network-form.component.js +++ b/ui/pages/settings/networks-tab/network-form/network-form.component.js @@ -10,6 +10,7 @@ import { isSafeChainId, } from '../../../../../shared/modules/network.utils'; import { jsonRpcRequest } from '../../../../../shared/modules/rpc.utils'; +import { decimalToHex } from '../../../../helpers/utils/conversions.util'; const FORM_STATE_KEYS = [ 'rpcUrl', @@ -39,7 +40,7 @@ export default class NetworkForm extends PureComponent { isCurrentRpcTarget: PropTypes.bool, blockExplorerUrl: PropTypes.string, rpcPrefs: PropTypes.object, - rpcUrls: PropTypes.array, + networksToRender: PropTypes.array, isFullScreen: PropTypes.bool, }; @@ -279,6 +280,7 @@ export default class NetworkForm extends PureComponent { }) { const { errors } = this.state; const { viewOnly } = this.props; + const errorMessage = errors[fieldKey]?.msg || ''; return (

@@ -304,7 +306,7 @@ export default class NetworkForm extends PureComponent { margin="dense" value={value} disabled={viewOnly} - error={errors[fieldKey]} + error={errorMessage} autoFocus={autoFocus} />
@@ -327,31 +329,78 @@ export default class NetworkForm extends PureComponent { }); }; - hasError = (errorKey, errorVal) => { - return this.state.errors[errorKey] === errorVal; + setErrorEmpty = (errorKey) => { + this.setState({ + errors: { + ...this.state.errors, + [errorKey]: { + msg: '', + key: '', + }, + }, + }); + }; + + hasError = (errorKey, errorKeyVal) => { + return this.state.errors[errorKey]?.key === errorKeyVal; }; - validateChainIdOnChange = (chainIdArg = '') => { + hasErrors = () => { + const { errors } = this.state; + return Object.keys(errors).some((key) => { + const error = errors[key]; + // Do not factor in duplicate chain id error for submission disabling + if (key === 'chainId' && error.key === 'chainIdExistsErrorMsg') { + return false; + } + return error.key && error.msg; + }); + }; + + validateChainIdOnChange = (selfRpcUrl, chainIdArg = '') => { + const { networksToRender } = this.props; const chainId = chainIdArg.trim(); + let errorKey = ''; let errorMessage = ''; let radix = 10; + const hexChainId = chainId.startsWith('0x') + ? chainId + : `0x${decimalToHex(chainId)}`; + const [matchingChainId] = networksToRender.filter( + (e) => e.chainId === hexChainId && e.rpcUrl !== selfRpcUrl, + ); - if (chainId.startsWith('0x')) { + if (chainId === '') { + this.setErrorEmpty('chainId'); + return; + } else if (matchingChainId) { + errorKey = 'chainIdExistsErrorMsg'; + errorMessage = this.context.t('chainIdExistsErrorMsg', [ + matchingChainId.label ?? matchingChainId.labelKey, + ]); + } else if (chainId.startsWith('0x')) { radix = 16; if (!/^0x[0-9a-f]+$/iu.test(chainId)) { + errorKey = 'invalidHexNumber'; errorMessage = this.context.t('invalidHexNumber'); } else if (!isPrefixedFormattedHexString(chainId)) { errorMessage = this.context.t('invalidHexNumberLeadingZeros'); } } else if (!/^[0-9]+$/u.test(chainId)) { + errorKey = 'invalidNumber'; errorMessage = this.context.t('invalidNumber'); } else if (chainId.startsWith('0')) { + errorKey = 'invalidNumberLeadingZeros'; errorMessage = this.context.t('invalidNumberLeadingZeros'); } else if (!isSafeChainId(parseInt(chainId, radix))) { + errorKey = 'invalidChainIdTooBig'; errorMessage = this.context.t('invalidChainIdTooBig'); } - this.setErrorTo('chainId', errorMessage); + this.setErrorTo('chainId', { + key: errorKey, + msg: errorMessage, + }); }; /** @@ -366,6 +415,7 @@ export default class NetworkForm extends PureComponent { */ validateChainIdOnSubmit = async (formChainId, parsedChainId, rpcUrl) => { const { t } = this.context; + let errorKey; let errorMessage; let endpointChainId; let providerError; @@ -378,6 +428,7 @@ export default class NetworkForm extends PureComponent { } if (providerError || typeof endpointChainId !== 'string') { + errorKey = 'failedToFetchChainId'; errorMessage = t('failedToFetchChainId'); } else if (parsedChainId !== endpointChainId) { // Here, we are in an error state. The endpoint should always return a @@ -395,6 +446,7 @@ export default class NetworkForm extends PureComponent { } } + errorKey = 'endpointReturnedDifferentChainId'; errorMessage = t('endpointReturnedDifferentChainId', [ endpointChainId.length <= 12 ? endpointChainId @@ -402,12 +454,15 @@ export default class NetworkForm extends PureComponent { ]); } - if (errorMessage) { - this.setErrorTo('chainId', errorMessage); + if (errorKey) { + this.setErrorTo('chainId', { + key: errorKey, + msg: errorMessage, + }); return false; } - this.setErrorTo('chainId', ''); + this.setErrorEmpty('chainId'); return true; }; @@ -417,41 +472,59 @@ export default class NetworkForm extends PureComponent { }; validateBlockExplorerURL = (url, stateKey) => { + const { t } = this.context; if (!validUrl.isWebUri(url) && url !== '') { - this.setErrorTo( - stateKey, - this.context.t( - this.isValidWhenAppended(url) - ? 'urlErrorMsg' - : 'invalidBlockExplorerURL', - ), - ); + let errorKey; + let errorMessage; + + if (this.isValidWhenAppended(url)) { + errorKey = 'urlErrorMsg'; + errorMessage = t('urlErrorMsg'); + } else { + errorKey = 'invalidBlockExplorerURL'; + errorMessage = t('invalidBlockExplorerURL'); + } + + this.setErrorTo(stateKey, { + key: errorKey, + msg: errorMessage, + }); } else { - this.setErrorTo(stateKey, ''); + this.setErrorEmpty(stateKey); } }; validateUrlRpcUrl = (url, stateKey) => { const { t } = this.context; - const { rpcUrls } = this.props; + const { networksToRender } = this.props; const { chainId: stateChainId } = this.state; - const isValidUrl = validUrl.isWebUri(url) && url !== ''; - const chainIdFetchFailed = this.hasError( - 'chainId', - t('failedToFetchChainId'), - ); - - if (!isValidUrl) { - this.setErrorTo( - stateKey, - this.context.t( - this.isValidWhenAppended(url) ? 'urlErrorMsg' : 'invalidRPC', - ), - ); - } else if (rpcUrls.includes(url)) { - this.setErrorTo(stateKey, this.context.t('urlExistsErrorMsg')); + const isValidUrl = validUrl.isWebUri(url); + const chainIdFetchFailed = this.hasError('chainId', 'failedToFetchChainId'); + const [matchingRPCUrl] = networksToRender.filter((e) => e.rpcUrl === url); + + if (!isValidUrl && url !== '') { + let errorKey; + let errorMessage; + if (this.isValidWhenAppended(url)) { + errorKey = 'urlErrorMsg'; + errorMessage = t('urlErrorMsg'); + } else { + errorKey = 'invalidRPC'; + errorMessage = t('invalidRPC'); + } + this.setErrorTo(stateKey, { + key: errorKey, + msg: errorMessage, + }); + } else if (matchingRPCUrl) { + this.setErrorTo(stateKey, { + key: 'urlExistsErrorMsg', + msg: t('urlExistsErrorMsg', [ + matchingRPCUrl.label ?? matchingRPCUrl.labelKey, + ]), + }); } else { - this.setErrorTo(stateKey, ''); + this.setErrorEmpty(stateKey); } // Re-validate the chain id if it could not be found with previous rpc url @@ -480,18 +553,16 @@ export default class NetworkForm extends PureComponent { chainId = '', ticker, blockExplorerUrl, - errors, } = this.state; const deletable = !networksTabIsInAddMode && !isCurrentRpcTarget && !viewOnly; - const isSubmitDisabled = + this.hasErrors() || this.isSubmitting() || this.stateIsUnchanged() || !rpcUrl || - !chainId || - Object.values(errors).some((x) => x); + !chainId; return (
@@ -514,7 +585,7 @@ export default class NetworkForm extends PureComponent { textFieldId: 'chainId', onChange: this.setStateWithValue( 'chainId', - this.validateChainIdOnChange, + this.validateChainIdOnChange.bind(this, rpcUrl), ), value: chainId, tooltipText: viewOnly ? null : t('networkSettingsChainIdDescription'), diff --git a/ui/pages/settings/networks-tab/networks-tab.component.js b/ui/pages/settings/networks-tab/networks-tab.component.js index e4fb230c7..34419f04a 100644 --- a/ui/pages/settings/networks-tab/networks-tab.component.js +++ b/ui/pages/settings/networks-tab/networks-tab.component.js @@ -202,12 +202,12 @@ export default class NetworksTab extends PureComponent { {this.renderNetworksList()} {shouldRenderNetworkForm ? ( network.rpcUrl)} setRpcTarget={setRpcTarget} editRpc={editRpc} networkName={label || (labelKey && t(labelKey)) || ''} rpcUrl={rpcUrl} chainId={chainId} + networksToRender={networksToRender} ticker={ticker} onClear={(shouldUpdateHistory = true) => { setNetworksTabAddMode(false); diff --git a/ui/pages/swaps/actionable-message/index.scss b/ui/pages/swaps/actionable-message/index.scss index 50e871613..4838489d4 100644 --- a/ui/pages/swaps/actionable-message/index.scss +++ b/ui/pages/swaps/actionable-message/index.scss @@ -51,12 +51,18 @@ } &--danger { - background: $Red-100; - border: 1px solid $Red-500; + background: $Red-000; + border: 1px solid $Red-300; justify-content: flex-start; .actionable-message__message { - color: $Red-500; + color: $Black-100; + text-align: left; + } + + button { + background: $Red-500; + color: #fff; } } diff --git a/ui/pages/swaps/awaiting-swap/awaiting-swap.js b/ui/pages/swaps/awaiting-swap/awaiting-swap.js index 6cd28fff3..75f7a07b5 100644 --- a/ui/pages/swaps/awaiting-swap/awaiting-swap.js +++ b/ui/pages/swaps/awaiting-swap/awaiting-swap.js @@ -3,7 +3,7 @@ import React, { useContext, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { useHistory } from 'react-router-dom'; -import { createCustomExplorerLink } from '@metamask/etherscan-link'; +import { getBlockExplorerLink } from '@metamask/etherscan-link'; import { I18nContext } from '../../../contexts/i18n'; import { useNewMetricEvent } from '../../../hooks/useMetricEvent'; import { MetaMetricsContext } from '../../../contexts/metametrics.new'; @@ -28,6 +28,7 @@ import { prepareToLeaveSwaps, } from '../../../ducks/swaps/swaps'; import Mascot from '../../../components/ui/mascot'; +import Box from '../../../components/ui/box'; import { QUOTES_EXPIRED_ERROR, SWAP_FAILED_ERROR, @@ -37,7 +38,6 @@ import { OFFLINE_FOR_MAINTENANCE, SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP, } from '../../../../shared/constants/swaps'; -import { CHAIN_ID_TO_TYPE_MAP as VALID_INFURA_CHAIN_IDS } from '../../../../shared/constants/network'; import { isSwapsDefaultTokenSymbol } from '../../../../shared/modules/swaps.utils'; import PulseLoader from '../../../components/ui/pulse-loader'; @@ -45,7 +45,6 @@ import { ASSET_ROUTE, DEFAULT_ROUTE } from '../../../helpers/constants/routes'; import { getRenderableNetworkFeesForQuote } from '../swaps.util'; import SwapsFooter from '../swaps-footer'; -import { getBlockExplorerUrlForTx } from '../../../../shared/modules/transaction.utils'; import SwapFailureIcon from './swap-failure-icon'; import SwapSuccessIcon from './swap-success-icon'; @@ -100,33 +99,36 @@ export default function AwaitingSwap({ const hardwareWalletUsed = useSelector(isHardwareWallet); const hardwareWalletType = useSelector(getHardwareWalletType); + const sensitiveProperties = { + token_from: sourceTokenInfo?.symbol, + token_from_amount: fetchParams?.value, + token_to: destinationTokenInfo?.symbol, + request_type: fetchParams?.balanceError ? 'Quote' : 'Order', + slippage: fetchParams?.slippage, + custom_slippage: fetchParams?.slippage === 2, + gas_fees: feeinUnformattedFiat, + is_hardware_wallet: hardwareWalletUsed, + hardware_wallet_type: hardwareWalletType, + }; const quotesExpiredEvent = useNewMetricEvent({ event: 'Quotes Timed Out', - sensitiveProperties: { - token_from: sourceTokenInfo?.symbol, - token_from_amount: fetchParams?.value, - token_to: destinationTokenInfo?.symbol, - request_type: fetchParams?.balanceError ? 'Quote' : 'Order', - slippage: fetchParams?.slippage, - custom_slippage: fetchParams?.slippage === 2, - gas_fees: feeinUnformattedFiat, - is_hardware_wallet: hardwareWalletUsed, - hardware_wallet_type: hardwareWalletType, - }, + sensitiveProperties, + category: 'swaps', + }); + const makeAnotherSwapEvent = useNewMetricEvent({ + event: 'Make Another Swap', + sensitiveProperties, category: 'swaps', }); - let blockExplorerUrl; - if (txHash && rpcPrefs.blockExplorerUrl) { - blockExplorerUrl = getBlockExplorerUrlForTx({ hash: txHash }, rpcPrefs); - } else if (txHash && SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId]) { - blockExplorerUrl = createCustomExplorerLink( - txHash, - SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId], - ); - } else if (txHash && VALID_INFURA_CHAIN_IDS[chainId]) { - blockExplorerUrl = getBlockExplorerUrlForTx({ chainId, hash: txHash }); - } + const baseNetworkUrl = + rpcPrefs.blockExplorerUrl ?? + SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? + null; + const blockExplorerUrl = getBlockExplorerLink( + { hash: txHash, chainId }, + { blockExplorerUrl: baseNetworkUrl }, + ); const isCustomBlockExplorerUrl = SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] || @@ -212,7 +214,7 @@ export default function AwaitingSwap({ } else if (!errorKey && swapComplete) { headerText = t('swapTransactionComplete'); statusImage = ; - submitText = t('swapViewToken', [destinationTokenInfo.symbol]); + submitText = t('close'); descriptionText = t('swapTokenAvailable', [ { + return ( + + { + makeAnotherSwapEvent(); + dispatch(navigateBackToBuildQuote(history)); + }} + > + {t('makeAnotherSwap')} + + + ); + }; + return (
@@ -245,6 +263,7 @@ export default function AwaitingSwap({
{descriptionText}
{content}
+ {!errorKey && swapComplete && } { if (errorKey === OFFLINE_FOR_MAINTENANCE) { @@ -263,7 +282,8 @@ export default function AwaitingSwap({ } else if (errorKey) { await dispatch(navigateBackToBuildQuote(history)); } else if ( - isSwapsDefaultTokenSymbol(destinationTokenInfo?.symbol, chainId) + isSwapsDefaultTokenSymbol(destinationTokenInfo?.symbol, chainId) || + swapComplete ) { history.push(DEFAULT_ROUTE); } else { diff --git a/ui/pages/swaps/awaiting-swap/awaiting-swap.test.js b/ui/pages/swaps/awaiting-swap/awaiting-swap.test.js index af9850752..bdd91c871 100644 --- a/ui/pages/swaps/awaiting-swap/awaiting-swap.test.js +++ b/ui/pages/swaps/awaiting-swap/awaiting-swap.test.js @@ -27,7 +27,7 @@ describe('AwaitingSwap', () => { store, ); expect(getByText('Processing')).toBeInTheDocument(); - expect(getByText('View on Etherscan')).toBeInTheDocument(); + expect(getByText('ETH')).toBeInTheDocument(); expect(getByText('View in activity')).toBeInTheDocument(); expect( document.querySelector('.awaiting-swap__main-descrption'), diff --git a/ui/pages/swaps/awaiting-swap/index.scss b/ui/pages/swaps/awaiting-swap/index.scss index 0ad246934..f2792eb5a 100644 --- a/ui/pages/swaps/awaiting-swap/index.scss +++ b/ui/pages/swaps/awaiting-swap/index.scss @@ -20,6 +20,10 @@ justify-content: center; } + a { + color: $Blue-500; + } + &__status-image { margin-top: 12px; margin-bottom: 16px; diff --git a/ui/pages/swaps/awaiting-swap/view-on-ether-scan-link/view-on-ether-scan-link.js b/ui/pages/swaps/awaiting-swap/view-on-ether-scan-link/view-on-ether-scan-link.js index 15fb7d081..ee11b2981 100644 --- a/ui/pages/swaps/awaiting-swap/view-on-ether-scan-link/view-on-ether-scan-link.js +++ b/ui/pages/swaps/awaiting-swap/view-on-ether-scan-link/view-on-ether-scan-link.js @@ -2,6 +2,7 @@ import React, { useContext } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { I18nContext } from '../../../../contexts/i18n'; +import { useNewMetricEvent } from '../../../../hooks/useMetricEvent'; export default function ViewOnEtherScanLink({ txHash, @@ -9,13 +10,29 @@ export default function ViewOnEtherScanLink({ isCustomBlockExplorerUrl, }) { const t = useContext(I18nContext); + + const blockExplorerLinkClickedEvent = useNewMetricEvent({ + category: 'Swaps', + event: 'Clicked Block Explorer Link', + properties: { + link_type: 'Transaction Block Explorer', + action: 'Swap Transaction', + block_explorer_domain: blockExplorerUrl + ? new URL(blockExplorerUrl)?.hostname + : '', + }, + }); + return (
global.platform.openTab({ url: blockExplorerUrl })} + onClick={() => { + blockExplorerLinkClickedEvent(); + global.platform.openTab({ url: blockExplorerUrl }); + }} > {isCustomBlockExplorerUrl ? t('viewOnCustomBlockExplorer', [new URL(blockExplorerUrl).hostname]) diff --git a/ui/pages/swaps/build-quote/build-quote.js b/ui/pages/swaps/build-quote/build-quote.js index d9c0ea7c3..0ac479977 100644 --- a/ui/pages/swaps/build-quote/build-quote.js +++ b/ui/pages/swaps/build-quote/build-quote.js @@ -4,11 +4,9 @@ import { useDispatch, useSelector } from 'react-redux'; import classnames from 'classnames'; import { uniqBy, isEqual } from 'lodash'; import { useHistory } from 'react-router-dom'; -import { - createCustomTokenTrackerLink, - createTokenTrackerLinkForChain, -} from '@metamask/etherscan-link'; +import { getTokenTrackerLink } from '@metamask/etherscan-link'; import { MetaMetricsContext } from '../../../contexts/metametrics.new'; +import { useNewMetricEvent } from '../../../hooks/useMetricEvent'; import { useTokensToSearch, getRenderableTokenData, @@ -18,7 +16,7 @@ import { I18nContext } from '../../../contexts/i18n'; import DropdownInputPair from '../dropdown-input-pair'; import DropdownSearchList from '../dropdown-search-list'; import SlippageButtons from '../slippage-buttons'; -import { getTokens } from '../../../ducks/metamask/metamask'; +import { getTokens, getConversionRate } from '../../../ducks/metamask/metamask'; import InfoTooltip from '../../../components/ui/info-tooltip'; import ActionableMessage from '../actionable-message'; @@ -35,11 +33,11 @@ import { import { getSwapsDefaultToken, getTokenExchangeRates, - getConversionRate, getCurrentCurrency, getCurrentChainId, getRpcPrefsForCurrentProvider, } from '../../../selectors'; + import { getValueFromWeiHex, hexToDecimal, @@ -223,29 +221,34 @@ export default function BuildQuote({ ); }; - let blockExplorerTokenLink; - let blockExplorerLabel; - if (rpcPrefs.blockExplorerUrl) { - blockExplorerTokenLink = createCustomTokenTrackerLink( - selectedToToken.address, - rpcPrefs.blockExplorerUrl, - ); - blockExplorerLabel = new URL(rpcPrefs.blockExplorerUrl).hostname; - } else if (SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId]) { - blockExplorerTokenLink = createCustomTokenTrackerLink( - selectedToToken.address, - SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId], - ); - blockExplorerLabel = new URL( - SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId], - ).hostname; - } else { - blockExplorerTokenLink = createTokenTrackerLinkForChain( - selectedToToken.address, - chainId, - ); - blockExplorerLabel = t('etherscan'); - } + const blockExplorerTokenLink = getTokenTrackerLink( + selectedToToken.address, + chainId, + null, // no networkId + null, // no holderAddress + { + blockExplorerUrl: + rpcPrefs.blockExplorerUrl ?? + SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? + null, + }, + ); + + const blockExplorerLabel = rpcPrefs.blockExplorerUrl + ? new URL(blockExplorerTokenLink).hostname + : t('etherscan'); + + const blockExplorerLinkClickedEvent = useNewMetricEvent({ + category: 'Swaps', + event: 'Clicked Block Explorer Link', + properties: { + link_type: 'Token Tracker', + action: 'Swaps Confirmation', + block_explorer_domain: blockExplorerTokenLink + ? new URL(blockExplorerTokenLink)?.hostname + : '', + }, + }); const { destinationTokenAddedForSwap } = fetchParams || {}; const { address: toAddress } = toToken || {}; @@ -330,6 +333,38 @@ export default function BuildQuote({ dispatch(resetSwapsPostFetchState()); }, [dispatch]); + const BlockExplorerLink = () => { + return ( + { + blockExplorerLinkClickedEvent(); + global.platform.openTab({ + url: blockExplorerTokenLink, + }); + }} + target="_blank" + rel="noopener noreferrer" + > + {blockExplorerLabel} + + ); + }; + + let tokenVerificationDescription = ''; + if (blockExplorerTokenLink) { + if (occurances === 1) { + tokenVerificationDescription = t('verifyThisTokenOn', [ + , + ]); + } else if (occurances === 0) { + tokenVerificationDescription = t('verifyThisUnconfirmedTokenOn', [ + , + ]); + } + } + return (
@@ -401,7 +436,7 @@ export default function BuildQuote({ }} > {t('swapSwapSwitch')}
{toTokenIsNotDefault && (occurances < 2 ? (
{occurances === 1 ? t('swapTokenVerificationOnlyOneSource') - : t('swapTokenVerificationNoSource')} -
-
- {blockExplorerTokenLink && - t('verifyThisTokenOn', [ - - {blockExplorerLabel} - , - ])} + : t('swapTokenVerificationAddedManually')}
+
{tokenVerificationDescription}
} primaryAction={ @@ -467,7 +491,6 @@ export default function BuildQuote({ onClick: () => setVerificationClicked(true), } } - type="warning" withRightButton infoTooltipText={ blockExplorerTokenLink && @@ -488,7 +511,12 @@ export default function BuildQuote({ { + blockExplorerLinkClickedEvent(); + global.platform.openTab({ + url: blockExplorerTokenLink, + }); + }} target="_blank" rel="noopener noreferrer" > diff --git a/ui/pages/swaps/build-quote/index.scss b/ui/pages/swaps/build-quote/index.scss index 325e795d6..1ebf6cab6 100644 --- a/ui/pages/swaps/build-quote/index.scss +++ b/ui/pages/swaps/build-quote/index.scss @@ -109,6 +109,22 @@ width: 100%; } + .dropdown-input-pair { + .searchable-item-list { + &__item--add-token { + display: none; + } + } + + &__to { + .searchable-item-list { + &__item--add-token { + display: flex; + } + } + } + } + &__open-to-dropdown { max-height: 194px; diff --git a/ui/pages/swaps/countdown-timer/countdown-timer.js b/ui/pages/swaps/countdown-timer/countdown-timer.js index 41767ef3c..e35f666ce 100644 --- a/ui/pages/swaps/countdown-timer/countdown-timer.js +++ b/ui/pages/swaps/countdown-timer/countdown-timer.js @@ -6,6 +6,7 @@ import { Duration } from 'luxon'; import { I18nContext } from '../../../contexts/i18n'; import InfoTooltip from '../../../components/ui/info-tooltip'; import { getSwapsQuoteRefreshTime } from '../../../ducks/swaps/swaps'; +import { SECOND } from '../../../../shared/constants/time'; // Return the mm:ss start time of the countdown timer. // If time has elapsed between `timeStarted` the time current time, @@ -17,14 +18,14 @@ function getNewTimer(currentTime, timeStarted, timeBaseStart) { } function decreaseTimerByOne(timer) { - return Math.max(timer - 1000, 0); + return Math.max(timer - SECOND, 0); } function timeBelowWarningTime(timer, warningTime) { const [warningTimeMinutes, warningTimeSeconds] = warningTime.split(':'); return ( timer <= - (Number(warningTimeMinutes) * 60 + Number(warningTimeSeconds)) * 1000 + (Number(warningTimeMinutes) * 60 + Number(warningTimeSeconds)) * SECOND ); } @@ -52,7 +53,7 @@ export default function CountdownTimer({ if (intervalRef.current === undefined) { intervalRef.current = setInterval(() => { setTimer(decreaseTimerByOne); - }, 1000); + }, SECOND); } return function cleanup() { @@ -75,7 +76,7 @@ export default function CountdownTimer({ clearInterval(intervalRef.current); intervalRef.current = setInterval(() => { setTimer(decreaseTimerByOne); - }, 1000); + }, SECOND); } }, [timeStarted, timer, timerStart]); diff --git a/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.test.js b/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.test.js index 92a7024a0..0f09cbf81 100644 --- a/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.test.js +++ b/ui/pages/swaps/dropdown-input-pair/dropdown-input-pair.test.js @@ -1,6 +1,10 @@ import React from 'react'; +import configureMockStore from 'redux-mock-store'; -import { renderWithProvider } from '../../../../test/jest'; +import { + renderWithProvider, + createSwapsMockStore, +} from '../../../../test/jest'; import DropdownInputPair from '.'; const createProps = (customProps = {}) => { @@ -11,9 +15,11 @@ const createProps = (customProps = {}) => { describe('DropdownInputPair', () => { it('renders the component with initial props', () => { + const store = configureMockStore()(createSwapsMockStore()); const props = createProps(); const { getByPlaceholderText } = renderWithProvider( , + store, ); expect(getByPlaceholderText('0')).toBeInTheDocument(); expect( diff --git a/ui/pages/swaps/dropdown-search-list/dropdown-search-list.js b/ui/pages/swaps/dropdown-search-list/dropdown-search-list.js index 005294a34..3f236ef2d 100644 --- a/ui/pages/swaps/dropdown-search-list/dropdown-search-list.js +++ b/ui/pages/swaps/dropdown-search-list/dropdown-search-list.js @@ -5,6 +5,7 @@ import React, { useContext, useRef, } from 'react'; +import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { isEqual } from 'lodash'; @@ -12,6 +13,16 @@ import { I18nContext } from '../../../contexts/i18n'; import SearchableItemList from '../searchable-item-list'; import PulseLoader from '../../../components/ui/pulse-loader'; import UrlIcon from '../../../components/ui/url-icon'; +import ActionableMessage from '../actionable-message'; +import ImportToken from '../import-token'; +import { useNewMetricEvent } from '../../../hooks/useMetricEvent'; +import { + isHardwareWallet, + getHardwareWalletType, + getCurrentChainId, + getRpcPrefsForCurrentProvider, +} from '../../../selectors/selectors'; +import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/swaps'; export default function DropdownSearchList({ searchListClassName, @@ -31,10 +42,31 @@ export default function DropdownSearchList({ hideRightLabels, hideItemIf, listContainerClassName, + shouldSearchForImports, }) { const t = useContext(I18nContext); const [isOpen, setIsOpen] = useState(false); + const [isImportTokenModalOpen, setIsImportTokenModalOpen] = useState(false); const [selectedItem, setSelectedItem] = useState(startingItem); + const [tokenForImport, setTokenForImport] = useState(null); + + const hardwareWalletUsed = useSelector(isHardwareWallet); + const hardwareWalletType = useSelector(getHardwareWalletType); + const chainId = useSelector(getCurrentChainId); + const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); + + const tokenImportedEvent = useNewMetricEvent({ + event: 'Token Imported', + sensitiveProperties: { + symbol: tokenForImport?.symbol, + address: tokenForImport?.address, + chain_id: chainId, + is_hardware_wallet: hardwareWalletUsed, + hardware_wallet_type: hardwareWalletType, + }, + category: 'swaps', + }); + const close = useCallback(() => { setIsOpen(false); onClose?.(); @@ -49,6 +81,25 @@ export default function DropdownSearchList({ [onSelect, close], ); + const onOpenImportTokenModalClick = (item) => { + setTokenForImport(item); + setIsImportTokenModalOpen(true); + }; + + const onImportTokenClick = () => { + tokenImportedEvent(); + // Only when a user confirms import of a token, we add it and show it in a dropdown. + onSelect?.(tokenForImport); + setSelectedItem(tokenForImport); + setTokenForImport(null); + close(); + }; + + const onImportTokenCloseClick = () => { + setIsImportTokenModalOpen(false); + close(); + }; + const onClickSelector = useCallback(() => { if (!isOpen) { setIsOpen(true); @@ -81,6 +132,34 @@ export default function DropdownSearchList({ } }; + const blockExplorerLink = + rpcPrefs.blockExplorerUrl ?? + SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? + null; + + const blockExplorerLabel = rpcPrefs.blockExplorerUrl + ? new URL(blockExplorerLink).hostname + : t('etherscan'); + + const blockExplorerLinkClickedEvent = useNewMetricEvent({ + category: 'Swaps', + event: 'Clicked Block Explorer Link', + properties: { + link_type: 'Token Tracker', + action: 'Verify Contract Address', + block_explorer_domain: blockExplorerLink + ? new URL(blockExplorerLink)?.hostname + : '', + }, + }); + + const importTokenProps = { + onImportTokenCloseClick, + onImportTokenClick, + setIsImportTokenModalOpen, + tokenForImport, + }; + return (
+ {tokenForImport && isImportTokenModalOpen && ( + + )} {!isOpen && ( ) } @@ -148,6 +256,7 @@ export default function DropdownSearchList({ fuseSearchKeys={fuseSearchKeys} defaultToAll={defaultToAll} onClickItem={onClickItem} + onOpenImportTokenModalClick={onOpenImportTokenModalClick} maxListItems={maxListItems} className={classnames( 'dropdown-search-list__token-container', @@ -159,6 +268,7 @@ export default function DropdownSearchList({ hideRightLabels={hideRightLabels} hideItemIf={hideItemIf} listContainerClassName={listContainerClassName} + shouldSearchForImports={shouldSearchForImports} />
{ @@ -15,9 +19,11 @@ const createProps = (customProps = {}) => { describe('DropdownSearchList', () => { it('renders the component with initial props', () => { + const store = configureMockStore()(createSwapsMockStore()); const props = createProps(); const { container, getByText } = renderWithProvider( , + store, ); expect(container).toMatchSnapshot(); expect(getByText('symbol')).toBeInTheDocument(); diff --git a/ui/pages/swaps/dropdown-search-list/index.scss b/ui/pages/swaps/dropdown-search-list/index.scss index c75c22bbb..c8321e9df 100644 --- a/ui/pages/swaps/dropdown-search-list/index.scss +++ b/ui/pages/swaps/dropdown-search-list/index.scss @@ -63,7 +63,7 @@ cursor: pointer; position: relative; align-items: center; - width: 100%; + flex: 1; height: 60px; i { @@ -128,12 +128,16 @@ color: $Grey-500; min-height: 300px; position: relative; - z-index: 1; + z-index: 1002; background: white; border-radius: 6px; min-height: 194px; overflow: hidden; text-overflow: ellipsis; + + .searchable-item-list__item--add-token { + padding: 8px 0; + } } &__loading-item { diff --git a/ui/pages/swaps/import-token/import-token.js b/ui/pages/swaps/import-token/import-token.js new file mode 100644 index 000000000..c0a967a9c --- /dev/null +++ b/ui/pages/swaps/import-token/import-token.js @@ -0,0 +1,89 @@ +import React, { useContext } from 'react'; +import PropTypes from 'prop-types'; +import { I18nContext } from '../../../contexts/i18n'; +import UrlIcon from '../../../components/ui/url-icon'; +import Popover from '../../../components/ui/popover'; +import Button from '../../../components/ui/button'; +import Box from '../../../components/ui/box'; +import Typography from '../../../components/ui/typography'; +import ActionableMessage from '../actionable-message'; +import { + TYPOGRAPHY, + FONT_WEIGHT, + ALIGN_ITEMS, + DISPLAY, +} from '../../../helpers/constants/design-system'; + +export default function ImportToken({ + onImportTokenCloseClick, + onImportTokenClick, + setIsImportTokenModalOpen, + tokenForImport, +}) { + const t = useContext(I18nContext); + const ImportTokenModalFooter = ( + <> + + + + ); + + return ( + setIsImportTokenModalOpen(false)} + footer={ImportTokenModalFooter} + > + + + + + {tokenForImport.name} + + {t('contract')}: + + {tokenForImport.address} + + + + ); +} + +ImportToken.propTypes = { + onImportTokenCloseClick: PropTypes.func, + onImportTokenClick: PropTypes.func, + setIsImportTokenModalOpen: PropTypes.func, + tokenForImport: PropTypes.object, +}; diff --git a/ui/pages/swaps/import-token/import-token.test.js b/ui/pages/swaps/import-token/import-token.test.js new file mode 100644 index 000000000..24616bdf9 --- /dev/null +++ b/ui/pages/swaps/import-token/import-token.test.js @@ -0,0 +1,27 @@ +import React from 'react'; + +import { renderWithProvider } from '../../../../test/jest'; +import ImportToken from '.'; + +const createProps = (customProps = {}) => { + return { + onImportTokenCloseClick: jest.fn(), + onImportTokenClick: jest.fn(), + setIsImportTokenModalOpen: jest.fn(), + tokenForImport: { + symbol: 'POS', + name: 'PoSToken', + address: '0xee609fe292128cad03b786dbb9bc2634ccdbe7fc', + }, + ...customProps, + }; +}; + +describe('ImportToken', () => { + it('renders the component with initial props', () => { + const props = createProps(); + const { getByText } = renderWithProvider(); + expect(getByText(props.tokenForImport.name)).toBeInTheDocument(); + expect(getByText(props.tokenForImport.address)).toBeInTheDocument(); + }); +}); diff --git a/ui/pages/swaps/import-token/index.js b/ui/pages/swaps/import-token/index.js new file mode 100644 index 000000000..4196dce2c --- /dev/null +++ b/ui/pages/swaps/import-token/index.js @@ -0,0 +1 @@ +export { default } from './import-token'; diff --git a/ui/pages/swaps/import-token/index.scss b/ui/pages/swaps/import-token/index.scss new file mode 100644 index 000000000..28e7c396b --- /dev/null +++ b/ui/pages/swaps/import-token/index.scss @@ -0,0 +1,30 @@ +.import-token { + flex-direction: column; + + .actionable-message { + margin-top: 0; + + &--danger { + border-color: $Red-300; + background: $Red-000; + } + + &__message { + color: $Black-100; + text-align: left; + } + } + + &__contract-address { + border-radius: 8px; + background-color: $Grey-000; + padding: 5px 10px; + } + + &__token-icon { + font-size: $font-size-h2; + margin-top: 24px; + width: 69px; + height: 69px; + } +} diff --git a/ui/pages/swaps/index.scss b/ui/pages/swaps/index.scss index f65cab061..358f0d7a5 100644 --- a/ui/pages/swaps/index.scss +++ b/ui/pages/swaps/index.scss @@ -14,6 +14,7 @@ @import 'slippage-buttons/index'; @import 'swaps-footer/index'; @import 'view-quote/index'; +@import 'import-token/index'; .swaps { display: flex; diff --git a/ui/pages/swaps/intro-popup/__snapshots__/intro-popup.test.js.snap b/ui/pages/swaps/intro-popup/__snapshots__/intro-popup.test.js.snap deleted file mode 100644 index d9d0324df..000000000 --- a/ui/pages/swaps/intro-popup/__snapshots__/intro-popup.test.js.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`IntroPopup renders the component with initial props 1`] = ` -
-
-
-`; diff --git a/ui/pages/swaps/intro-popup/index.js b/ui/pages/swaps/intro-popup/index.js deleted file mode 100644 index 6460538b9..000000000 --- a/ui/pages/swaps/intro-popup/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './intro-popup'; diff --git a/ui/pages/swaps/intro-popup/index.scss b/ui/pages/swaps/intro-popup/index.scss deleted file mode 100644 index 48d79c5f9..000000000 --- a/ui/pages/swaps/intro-popup/index.scss +++ /dev/null @@ -1,71 +0,0 @@ -.intro-popup { - &__liquidity-sources-label { - @include H7; - - font-weight: bold; - margin-bottom: 6px; - color: $Black-100; - - @media screen and (min-width: 576px) { - @include H6; - } - } - - &__learn-more-header { - @include H4; - - font-weight: bold; - margin-bottom: 12px; - margin-top: 16px; - } - - &__learn-more-link { - @include H6; - - color: $Blue-500; - margin-bottom: 8px; - cursor: pointer; - } - - &__content { - margin-left: 24px; - - > img { - width: 96%; - margin-left: -9px; - } - } - - &__footer { - border-top: none; - } - - &__button { - border-radius: 100px; - height: 44px; - } - - &__source-logo-container { - width: 276px; - display: flex; - justify-content: center; - align-items: center; - padding: 20px 16px; - background: $Grey-000; - border-radius: 8px; - - @media screen and (min-width: 576px) { - width: 412px; - - img { - width: 364px; - } - } - } - - &__popover { - @media screen and (min-width: 576px) { - width: 460px; - } - } -} diff --git a/ui/pages/swaps/intro-popup/intro-popup.js b/ui/pages/swaps/intro-popup/intro-popup.js deleted file mode 100644 index 658c84bb9..000000000 --- a/ui/pages/swaps/intro-popup/intro-popup.js +++ /dev/null @@ -1,108 +0,0 @@ -import React, { useContext } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { useHistory } from 'react-router-dom'; -import PropTypes from 'prop-types'; -import { setSwapsFromToken } from '../../../ducks/swaps/swaps'; -import { I18nContext } from '../../../contexts/i18n'; -import { BUILD_QUOTE_ROUTE } from '../../../helpers/constants/routes'; -import { useNewMetricEvent } from '../../../hooks/useMetricEvent'; -import { getSwapsDefaultToken } from '../../../selectors'; -import Button from '../../../components/ui/button'; -import Popover from '../../../components/ui/popover'; - -export default function IntroPopup({ onClose }) { - const dispatch = useDispatch(useDispatch); - const history = useHistory(); - const t = useContext(I18nContext); - - const swapsDefaultToken = useSelector(getSwapsDefaultToken); - const enteredSwapsEvent = useNewMetricEvent({ - event: 'Swaps Opened', - properties: { - source: 'Intro popup', - active_currency: swapsDefaultToken.symbol, - }, - category: 'swaps', - }); - const blogPostVisitedEvent = useNewMetricEvent({ - event: 'Blog Post Visited ', - category: 'swaps', - }); - const contractAuditVisitedEvent = useNewMetricEvent({ - event: 'Contract Audit Visited', - category: 'swaps', - }); - const productOverviewDismissedEvent = useNewMetricEvent({ - event: 'Product Overview Dismissed', - category: 'swaps', - }); - - return ( -
- { - productOverviewDismissedEvent(); - onClose(); - }} - footerClassName="intro-popup__footer" - footer={ - - } - > -
-
- {t('swapIntroLiquiditySourcesLabel')} -
-
- -
-
- {t('swapIntroLearnMoreHeader')} -
-
{ - global.platform.openTab({ - url: - 'https://medium.com/metamask/introducing-metamask-swaps-84318c643785', - }); - blogPostVisitedEvent(); - }} - > - {t('swapIntroLearnMoreLink')} -
-
{ - global.platform.openTab({ - url: - 'https://diligence.consensys.net/audits/private/lsjipyllnw2/', - }); - contractAuditVisitedEvent(); - }} - > - {t('swapLearnMoreContractsAuditReview')} -
-
-
-
- ); -} - -IntroPopup.propTypes = { - onClose: PropTypes.func.isRequired, -}; diff --git a/ui/pages/swaps/intro-popup/intro-popup.test.js b/ui/pages/swaps/intro-popup/intro-popup.test.js deleted file mode 100644 index 049c73091..000000000 --- a/ui/pages/swaps/intro-popup/intro-popup.test.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import configureMockStore from 'redux-mock-store'; - -import { - renderWithProvider, - createSwapsMockStore, -} from '../../../../test/jest'; -import IntroPopup from '.'; - -const createProps = (customProps = {}) => { - return { - onClose: jest.fn(), - ...customProps, - }; -}; - -describe('IntroPopup', () => { - it('renders the component with initial props', () => { - const store = configureMockStore()(createSwapsMockStore()); - const props = createProps(); - const { container } = renderWithProvider(, store); - expect(container).toMatchSnapshot(); - }); -}); diff --git a/ui/pages/swaps/searchable-item-list/index.scss b/ui/pages/swaps/searchable-item-list/index.scss index 76b5e0578..85e4fb5a3 100644 --- a/ui/pages/swaps/searchable-item-list/index.scss +++ b/ui/pages/swaps/searchable-item-list/index.scss @@ -43,7 +43,7 @@ &__list-container { display: flex; flex-direction: column; - overflow-y: scroll; + overflow-y: auto; } &__item { @@ -63,7 +63,7 @@ } &:last-of-type { - border-bottom: 1px solid $Grey-100; + border-bottom: none; } &:hover, @@ -80,6 +80,38 @@ pointer-events: none; } + &--add-token { + min-height: auto; + opacity: 1; + pointer-events: none; + + &:hover { + background: none; + } + + .actionable-message { + margin: 0; + + &__message { + text-align: left; + color: $Black-100; + } + + a { + pointer-events: auto; + color: #037dd6; + cursor: pointer; + } + } + } + + .btn-primary { + @include H7; + + width: auto; + padding: 7px 11px; + } + > img { margin-top: -2px; } diff --git a/ui/pages/swaps/searchable-item-list/item-list/item-list.component.js b/ui/pages/swaps/searchable-item-list/item-list/item-list.component.js index 0601c7242..4c7442448 100644 --- a/ui/pages/swaps/searchable-item-list/item-list/item-list.component.js +++ b/ui/pages/swaps/searchable-item-list/item-list/item-list.component.js @@ -1,12 +1,23 @@ -import React from 'react'; +import React, { useContext } from 'react'; +import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import Identicon from '../../../../components/ui/identicon'; import UrlIcon from '../../../../components/ui/url-icon'; +import Button from '../../../../components/ui/button'; +import ActionableMessage from '../../actionable-message'; +import { I18nContext } from '../../../../contexts/i18n'; +import { + getCurrentChainId, + getRpcPrefsForCurrentProvider, +} from '../../../../selectors'; +import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../../shared/constants/swaps'; +import { useNewMetricEvent } from '../../../../hooks/useMetricEvent'; export default function ItemList({ results = [], onClickItem, + onOpenImportTokenModalClick, Placeholder, listTitle, maxListItems = 6, @@ -16,6 +27,32 @@ export default function ItemList({ hideItemIf, listContainerClassName, }) { + const t = useContext(I18nContext); + const chainId = useSelector(getCurrentChainId); + const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); + const blockExplorerLink = + rpcPrefs.blockExplorerUrl ?? + SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? + null; + + const blockExplorerLabel = rpcPrefs.blockExplorerUrl + ? new URL(blockExplorerLink).hostname + : t('etherscan'); + + const blockExplorerLinkClickedEvent = useNewMetricEvent({ + category: 'Swaps', + event: 'Clicked Block Explorer Link', + properties: { + link_type: 'Token Tracker', + action: 'Verify Contract Address', + block_explorer_domain: blockExplorerLink + ? new URL(blockExplorerLink)?.hostname + : '', + }, + }); + + // If there is a token for import based on a contract address, it's the only one in the list. + const hasTokenForImport = results.length === 1 && results[0].notImported; return results.length === 0 ? ( Placeholder && ) : ( @@ -35,7 +72,13 @@ export default function ItemList({ return null; } - const onClick = () => onClickItem?.(result); + const onClick = () => { + if (result.notImported) { + onOpenImportTokenModalClick(result); + } else { + onClickItem?.(result); + } + }; const { iconUrl, identiconAddress, @@ -96,9 +139,42 @@ export default function ItemList({
)}
+ {result.notImported && ( + + )}
); })} + {!hasTokenForImport && ( +
+ { + blockExplorerLinkClickedEvent(); + global.platform.openTab({ + url: blockExplorerLink, + }); + }} + target="_blank" + rel="noopener noreferrer" + > + {blockExplorerLabel} + , + ]) + } + /> +
+ )}
); @@ -117,6 +193,7 @@ ItemList.propTypes = { }), ), onClickItem: PropTypes.func, + onOpenImportTokenModalClick: PropTypes.func, Placeholder: PropTypes.func, listTitle: PropTypes.string, maxListItems: PropTypes.number, diff --git a/ui/pages/swaps/searchable-item-list/list-item-search/list-item-search.component.js b/ui/pages/swaps/searchable-item-list/list-item-search/list-item-search.component.js index 1be3e3efe..26fbc0124 100644 --- a/ui/pages/swaps/searchable-item-list/list-item-search/list-item-search.component.js +++ b/ui/pages/swaps/searchable-item-list/list-item-search/list-item-search.component.js @@ -1,9 +1,14 @@ import React, { useState, useEffect, useRef } from 'react'; +import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import Fuse from 'fuse.js'; +import log from 'loglevel'; import InputAdornment from '@material-ui/core/InputAdornment'; import TextField from '../../../../components/ui/text-field'; import { usePrevious } from '../../../../hooks/usePrevious'; +import { isValidHexAddress } from '../../../../../shared/modules/hexstring-utils'; +import { fetchToken } from '../../swaps.util'; +import { getCurrentChainId } from '../../../../selectors/selectors'; const renderAdornment = () => ( @@ -18,17 +23,53 @@ export default function ListItemSearch({ fuseSearchKeys, searchPlaceholderText, defaultToAll, + shouldSearchForImports, }) { const fuseRef = useRef(); const [searchQuery, setSearchQuery] = useState(''); + const chainId = useSelector(getCurrentChainId); - const handleSearch = (newSearchQuery) => { - setSearchQuery(newSearchQuery); + /** + * Search a custom token for import based on a contract address. + * @param {String} contractAddress + */ + const handleSearchTokenForImport = async (contractAddress) => { + setSearchQuery(contractAddress); + try { + const token = await fetchToken(contractAddress, chainId); + if (token) { + token.primaryLabel = token.symbol; + token.secondaryLabel = token.name; + token.notImported = true; + onSearch({ + searchQuery: contractAddress, + results: [token], + }); + return; + } + } catch (e) { + log.error('Token not found, show 0 results.', e); + } + onSearch({ + searchQuery: contractAddress, + results: [], // No token for import found. + }); + }; + + const handleSearch = async (newSearchQuery) => { + const trimmedNewSearchQuery = newSearchQuery.trim(); + const validHexAddress = isValidHexAddress(trimmedNewSearchQuery); const fuseSearchResult = fuseRef.current.search(newSearchQuery); + const results = + defaultToAll && newSearchQuery === '' ? listToSearch : fuseSearchResult; + if (shouldSearchForImports && results.length === 0 && validHexAddress) { + await handleSearchTokenForImport(trimmedNewSearchQuery); + return; + } + setSearchQuery(newSearchQuery); onSearch({ searchQuery: newSearchQuery, - results: - defaultToAll && newSearchQuery === '' ? listToSearch : fuseSearchResult, + results, }); }; @@ -83,4 +124,5 @@ ListItemSearch.propTypes = { fuseSearchKeys: PropTypes.arrayOf(PropTypes.object).isRequired, searchPlaceholderText: PropTypes.string, defaultToAll: PropTypes.bool, + shouldSearchForImports: PropTypes.bool, }; diff --git a/ui/pages/swaps/searchable-item-list/searchable-item-list.js b/ui/pages/swaps/searchable-item-list/searchable-item-list.js index ba871339e..318c2094e 100644 --- a/ui/pages/swaps/searchable-item-list/searchable-item-list.js +++ b/ui/pages/swaps/searchable-item-list/searchable-item-list.js @@ -12,11 +12,13 @@ export default function SearchableItemList({ listTitle, maxListItems, onClickItem, + onOpenImportTokenModalClick, Placeholder, searchPlaceholderText, hideRightLabels, hideItemIf, listContainerClassName, + shouldSearchForImports, }) { const itemListRef = useRef(); @@ -38,11 +40,13 @@ export default function SearchableItemList({ error={itemSelectorError} searchPlaceholderText={searchPlaceholderText} defaultToAll={defaultToAll} + shouldSearchForImports={shouldSearchForImports} /> { @@ -37,8 +41,12 @@ const createProps = (customProps = {}) => { describe('SearchableItemList', () => { it('renders the component with initial props', () => { + const store = configureMockStore()(createSwapsMockStore()); const props = createProps(); - const { getByText } = renderWithProvider(); + const { getByText } = renderWithProvider( + , + store, + ); expect(getByText(props.listTitle)).toBeInTheDocument(); expect(getByText(props.itemsToSearch[0].primaryLabel)).toBeInTheDocument(); expect( diff --git a/ui/pages/swaps/select-quote-popover/quote-details/quote-details.js b/ui/pages/swaps/select-quote-popover/quote-details/quote-details.js index 6d96123e0..86da97ca9 100644 --- a/ui/pages/swaps/select-quote-popover/quote-details/quote-details.js +++ b/ui/pages/swaps/select-quote-popover/quote-details/quote-details.js @@ -80,7 +80,7 @@ const QuoteDetails = ({
diff --git a/ui/pages/swaps/slippage-buttons/index.scss b/ui/pages/swaps/slippage-buttons/index.scss index f9e2df7bc..fcfab7eb9 100644 --- a/ui/pages/swaps/slippage-buttons/index.scss +++ b/ui/pages/swaps/slippage-buttons/index.scss @@ -10,7 +10,6 @@ margin-bottom: 0; margin-left: auto; margin-right: auto; - cursor: pointer; background: unset; margin-bottom: 8px; } diff --git a/ui/pages/swaps/swaps-gas-customization-modal/swaps-gas-customization-modal.container.js b/ui/pages/swaps/swaps-gas-customization-modal/swaps-gas-customization-modal.container.js index 15efd88d6..9bcd95717 100644 --- a/ui/pages/swaps/swaps-gas-customization-modal/swaps-gas-customization-modal.container.js +++ b/ui/pages/swaps/swaps-gas-customization-modal/swaps-gas-customization-modal.container.js @@ -8,9 +8,9 @@ import { getDefaultActiveButtonIndex, getRenderableGasButtonData, getUSDConversionRate, - getNativeCurrency, getSwapsDefaultToken, } from '../../../selectors'; +import { getNativeCurrency } from '../../../ducks/metamask/metamask'; import { getSwapsCustomizationModalPrice, diff --git a/ui/pages/swaps/swaps.util.js b/ui/pages/swaps/swaps.util.js index e72483f35..58ef33f63 100644 --- a/ui/pages/swaps/swaps.util.js +++ b/ui/pages/swaps/swaps.util.js @@ -16,6 +16,7 @@ import { WETH_SYMBOL, MAINNET_CHAIN_ID, } from '../../../shared/constants/network'; +import { SECOND } from '../../../shared/constants/time'; import { calcTokenValue, calcTokenAmount, @@ -47,6 +48,8 @@ const getBaseApi = function (type, chainId = MAINNET_CHAIN_ID) { return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/trades?`; case 'tokens': return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/tokens`; + case 'token': + return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/token`; case 'topAssets': return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/topAssets`; case 'featureFlag': @@ -237,7 +240,7 @@ export async function fetchTradesInfo( sourceToken, sourceAmount: calcTokenValue(value, sourceDecimals).toString(10), slippage, - timeout: 10000, + timeout: SECOND * 10, walletAddress: fromAddress, }; @@ -250,7 +253,7 @@ export async function fetchTradesInfo( const tradesResponse = await fetchWithCache( tradeURL, { method: 'GET' }, - { cacheRefreshTime: 0, timeout: 15000 }, + { cacheRefreshTime: 0, timeout: SECOND * 15 }, ); const newQuotes = tradesResponse.reduce((aggIdTradeMap, quote) => { if ( @@ -290,10 +293,20 @@ export async function fetchTradesInfo( return newQuotes; } +export async function fetchToken(contractAddress, chainId) { + const tokenUrl = getBaseApi('token', chainId); + const token = await fetchWithCache( + `${tokenUrl}?address=${contractAddress}`, + { method: 'GET' }, + { cacheRefreshTime: CACHE_REFRESH_FIVE_MINUTES }, + ); + return token; +} + export async function fetchTokens(chainId) { - const tokenUrl = getBaseApi('tokens', chainId); + const tokensUrl = getBaseApi('tokens', chainId); const tokens = await fetchWithCache( - tokenUrl, + tokensUrl, { method: 'GET' }, { cacheRefreshTime: CACHE_REFRESH_FIVE_MINUTES }, ); @@ -301,7 +314,7 @@ export async function fetchTokens(chainId) { SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId], ...tokens.filter((token) => { return ( - validateData(TOKEN_VALIDATORS, token, tokenUrl) && + validateData(TOKEN_VALIDATORS, token, tokensUrl) && !( isSwapsDefaultTokenSymbol(token.symbol, chainId) || isSwapsDefaultTokenAddress(token.address, chainId) diff --git a/ui/pages/swaps/view-quote/index.scss b/ui/pages/swaps/view-quote/index.scss index 64d3d59d7..3e2e421bc 100644 --- a/ui/pages/swaps/view-quote/index.scss +++ b/ui/pages/swaps/view-quote/index.scss @@ -95,46 +95,53 @@ &-wrapper { width: 100%; - &.medium .actionable-message, - &.fiat-error .actionable-message { - border-color: $Yellow-500; - background: $Yellow-100; + &.low, + &.medium, + &.high { + .actionable-message { + .actionable-message__message { + color: inherit; + } - .actionable-message__message { - color: inherit; + button { + font-size: $font-size-h8; + padding: 4px 12px; + border-radius: 42px; + } } + } - button { - background: $Yellow-500; - border-radius: 42px; + &.low { + .actionable-message { + button { + background: $Blue-500; + color: #fff; + } } } - &.high { + &.medium { .actionable-message { - border-color: $Red-500; - background: $Red-100; + border-color: $Yellow-500; + background: $Yellow-100; - .actionable-message__message { - color: $Red-500; + button { + background: $Yellow-500; } } + } - button { - background: $Red-500; - color: #fff; - border-radius: 42px; + &.high { + .actionable-message { + border-color: $Red-300; + background: $Red-000; - /* Offsets the width of ActionableMessage icon */ - margin-right: -22px; + button { + background: $Red-500; + color: #fff; + } } } - - /* Hides info tooltip if there's a fiat error message */ - &.fiat-error div[data-tooltipped] { - /* !important overrides style being applied directly to tooltip by component */ - display: none !important; - } } &-contents { @@ -160,10 +167,9 @@ width: 100%; align-items: center; justify-content: center; - width: intrinsic; /* Safari/WebKit uses a non-standard name */ - width: max-content; max-width: 340px; margin-top: 8px; + margin-bottom: 28px; @media screen and (min-width: 576px) { &--thin { diff --git a/ui/pages/swaps/view-quote/view-quote-price-difference.js b/ui/pages/swaps/view-quote/view-quote-price-difference.js index 61d1ad52b..327aaaf49 100644 --- a/ui/pages/swaps/view-quote/view-quote-price-difference.js +++ b/ui/pages/swaps/view-quote/view-quote-price-difference.js @@ -6,6 +6,11 @@ import { I18nContext } from '../../../contexts/i18n'; import ActionableMessage from '../actionable-message'; import Tooltip from '../../../components/ui/tooltip'; +import Box from '../../../components/ui/box'; +import { + JUSTIFY_CONTENT, + DISPLAY, +} from '../../../helpers/constants/design-system'; export default function ViewQuotePriceDifference(props) { const { @@ -28,9 +33,10 @@ export default function ViewQuotePriceDifference(props) { let priceDifferenceAcknowledgementText = ''; if (priceSlippageUnknownFiatValue) { // A calculation error signals we cannot determine dollar value - priceDifferenceMessage = t('swapPriceDifferenceUnavailable'); - priceDifferenceClass = 'fiat-error'; - priceDifferenceAcknowledgementText = t('continue'); + priceDifferenceTitle = t('swapPriceUnavailableTitle'); + priceDifferenceMessage = t('swapPriceUnavailableDescription'); + priceDifferenceClass = 'high'; + priceDifferenceAcknowledgementText = t('tooltipApproveButton'); } else { priceDifferenceTitle = t('swapPriceDifferenceTitle', [ priceDifferencePercentage, @@ -44,9 +50,7 @@ export default function ViewQuotePriceDifference(props) { priceSlippageFromDestination, // Destination tokens total value ]); priceDifferenceClass = usedQuote.priceSlippage.bucket; - priceDifferenceAcknowledgementText = t( - 'swapPriceDifferenceAcknowledgement', - ); + priceDifferenceAcknowledgementText = t('tooltipApproveButton'); } return ( @@ -60,11 +64,22 @@ export default function ViewQuotePriceDifference(props) { message={
- {priceDifferenceTitle && ( +
{priceDifferenceTitle}
- )} + + + +
{priceDifferenceMessage} {!acknowledged && (
@@ -78,13 +93,6 @@ export default function ViewQuotePriceDifference(props) {
)}
- - -
} /> diff --git a/ui/pages/swaps/view-quote/view-quote-price-difference.test.js b/ui/pages/swaps/view-quote/view-quote-price-difference.test.js index f18950464..cd94ab4a0 100644 --- a/ui/pages/swaps/view-quote/view-quote-price-difference.test.js +++ b/ui/pages/swaps/view-quote/view-quote-price-difference.test.js @@ -144,6 +144,6 @@ describe('View Price Quote Difference', () => { 'Could not determine price.'; renderComponent(props); - expect(component.html()).toContain('fiat-error'); + expect(component.html()).toContain('high'); }); }); diff --git a/ui/pages/swaps/view-quote/view-quote.js b/ui/pages/swaps/view-quote/view-quote.js index c472f1f40..217de73de 100644 --- a/ui/pages/swaps/view-quote/view-quote.js +++ b/ui/pages/swaps/view-quote/view-quote.js @@ -38,12 +38,13 @@ import { getTokenExchangeRates, getSwapsDefaultToken, getCurrentChainId, - getNativeCurrency, isHardwareWallet, getHardwareWalletType, } from '../../../selectors'; +import { getNativeCurrency, getTokens } from '../../../ducks/metamask/metamask'; + import { toPrecisionWithoutTrailingZeros } from '../../../helpers/utils/util'; -import { getTokens } from '../../../ducks/metamask/metamask'; + import { safeRefetchQuotes, setCustomApproveTxData, diff --git a/ui/selectors/confirm-transaction.js b/ui/selectors/confirm-transaction.js index ba837e8ab..70982d1b1 100644 --- a/ui/selectors/confirm-transaction.js +++ b/ui/selectors/confirm-transaction.js @@ -11,8 +11,8 @@ import { } from '../helpers/utils/confirm-tx.util'; import { sumHexes } from '../helpers/utils/transactions.util'; import { transactionMatchesNetwork } from '../../shared/modules/transaction.utils'; +import { getNativeCurrency } from '../ducks/metamask/metamask'; import { getCurrentChainId, deprecatedGetCurrentNetworkId } from './selectors'; -import { getNativeCurrency } from '.'; const unapprovedTxsSelector = (state) => state.metamask.unapprovedTxs; const unapprovedMsgsSelector = (state) => state.metamask.unapprovedMsgs; diff --git a/ui/selectors/custom-gas.js b/ui/selectors/custom-gas.js index ea64bcbe6..4f905a7fe 100644 --- a/ui/selectors/custom-gas.js +++ b/ui/selectors/custom-gas.js @@ -9,6 +9,8 @@ import { formatETHFee } from '../helpers/utils/formatters'; import { calcGasTotal } from '../pages/send/send.utils'; import { GAS_ESTIMATE_TYPES } from '../helpers/constants/common'; +import { BASIC_ESTIMATE_STATES, GAS_SOURCE } from '../ducks/gas/gas.duck'; +import { GAS_LIMITS } from '../../shared/constants/gas'; import { getCurrentCurrency, getIsMainnet, @@ -98,6 +100,28 @@ export function isCustomPriceSafe(state) { return customPriceSafe; } +export function isCustomPriceSafeForCustomNetwork(state) { + const estimatedPrice = state.gas.basicEstimates.average; + + const customGasPrice = getCustomGasPrice(state); + + if (!customGasPrice) { + return true; + } + + const customPriceSafe = conversionGreaterThan( + { + value: customGasPrice, + fromNumericBase: 'hex', + fromDenomination: 'WEI', + toDenomination: 'GWEI', + }, + { value: estimatedPrice, fromNumericBase: 'dec' }, + ); + + return customPriceSafe; +} + export function isCustomPriceExcessive(state, checkSend = false) { const customPrice = checkSend ? getGasPrice(state) : getCustomGasPrice(state); const fastPrice = getFastPriceEstimate(state); @@ -294,7 +318,7 @@ export function getRenderableEstimateDataForSmallButtonsFromGWEI(state) { const isMainnet = getIsMainnet(state); const showFiat = isMainnet || Boolean(showFiatInTestnets); const gasLimit = - state.metamask.send.gasLimit || getCustomGasLimit(state) || '0x5208'; + state.send.gasLimit || getCustomGasLimit(state) || GAS_LIMITS.SIMPLE; const { conversionRate } = state.metamask; const currentCurrency = getCurrentCurrency(state); const { @@ -361,13 +385,21 @@ export function getRenderableEstimateDataForSmallButtonsFromGWEI(state) { export function getIsEthGasPriceFetched(state) { const gasState = state.gas; return Boolean( - gasState.estimateSource === 'eth_gasprice' && - gasState.basicEstimateStatus === 'READY' && + gasState.estimateSource === GAS_SOURCE.ETHGASPRICE && + gasState.basicEstimateStatus === BASIC_ESTIMATE_STATES.READY && getIsMainnet(state), ); } export function getNoGasPriceFetched(state) { const gasState = state.gas; - return Boolean(gasState.basicEstimateStatus === 'FAILED'); + return Boolean(gasState.basicEstimateStatus === BASIC_ESTIMATE_STATES.FAILED); +} + +export function getIsGasEstimatesFetched(state) { + const gasState = state.gas; + return Boolean( + gasState.estimateSource === GAS_SOURCE.METASWAPS && + gasState.basicEstimateStatus === BASIC_ESTIMATE_STATES.READY, + ); } diff --git a/ui/selectors/custom-gas.test.js b/ui/selectors/custom-gas.test.js index 8838641fe..91344b96b 100644 --- a/ui/selectors/custom-gas.test.js +++ b/ui/selectors/custom-gas.test.js @@ -1,3 +1,4 @@ +import { GAS_LIMITS } from '../../shared/constants/gas'; import { getCustomGasLimit, getCustomGasPrice, @@ -110,10 +111,8 @@ describe('custom-gas selectors', () => { }); it('should return false gas.basicEstimates.price 0x28bed01600 (175) (checkSend=true)', () => { const mockState = { - metamask: { - send: { - gasPrice: '0x28bed0160', - }, + send: { + gasPrice: '0x28bed0160', }, gas: { customData: { price: null }, @@ -124,10 +123,8 @@ describe('custom-gas selectors', () => { }); it('should return true gas.basicEstimates.price 0x30e4f9b400 (210) (checkSend=true)', () => { const mockState = { - metamask: { - send: { - gasPrice: '0x30e4f9b400', - }, + send: { + gasPrice: '0x30e4f9b400', }, gas: { customData: { price: null }, @@ -220,9 +217,6 @@ describe('custom-gas selectors', () => { metamask: { conversionRate: 2557.1, currentCurrency: 'usd', - send: { - gasLimit: '0x5208', - }, preferences: { showFiatInTestnets: false, }, @@ -231,6 +225,9 @@ describe('custom-gas selectors', () => { chainId: '0x1', }, }, + send: { + gasLimit: GAS_LIMITS.SIMPLE, + }, gas: { basicEstimates: { blockTime: 14.16326530612245, @@ -271,9 +268,6 @@ describe('custom-gas selectors', () => { metamask: { conversionRate: 2557.1, currentCurrency: 'usd', - send: { - gasLimit: '0x5208', - }, preferences: { showFiatInTestnets: false, }, @@ -282,6 +276,9 @@ describe('custom-gas selectors', () => { chainId: '0x4', }, }, + send: { + gasLimit: GAS_LIMITS.SIMPLE, + }, gas: { basicEstimates: { blockTime: 14.16326530612245, @@ -322,9 +319,6 @@ describe('custom-gas selectors', () => { metamask: { conversionRate: 2557.1, currentCurrency: 'usd', - send: { - gasLimit: '0x5208', - }, preferences: { showFiatInTestnets: true, }, @@ -333,6 +327,9 @@ describe('custom-gas selectors', () => { chainId: '0x4', }, }, + send: { + gasLimit: GAS_LIMITS.SIMPLE, + }, gas: { basicEstimates: { safeLow: 5, @@ -367,9 +364,6 @@ describe('custom-gas selectors', () => { metamask: { conversionRate: 2557.1, currentCurrency: 'usd', - send: { - gasLimit: '0x5208', - }, preferences: { showFiatInTestnets: true, }, @@ -378,6 +372,9 @@ describe('custom-gas selectors', () => { chainId: '0x1', }, }, + send: { + gasLimit: GAS_LIMITS.SIMPLE, + }, gas: { basicEstimates: { safeLow: 5, @@ -393,7 +390,7 @@ describe('custom-gas selectors', () => { expect( getRenderableBasicEstimateData( test.mockState, - '0x5208', + GAS_LIMITS.SIMPLE, test.useFastestButtons, ), ).toStrictEqual(test.expectedResult); @@ -428,9 +425,6 @@ describe('custom-gas selectors', () => { metamask: { conversionRate: 255.71, currentCurrency: 'usd', - send: { - gasLimit: '0x5208', - }, preferences: { showFiatInTestnets: false, }, @@ -439,6 +433,9 @@ describe('custom-gas selectors', () => { chainId: '0x1', }, }, + send: { + gasLimit: GAS_LIMITS.SIMPLE, + }, gas: { basicEstimates: { safeLow: 25, @@ -473,9 +470,6 @@ describe('custom-gas selectors', () => { metamask: { conversionRate: 2557.1, currentCurrency: 'usd', - send: { - gasLimit: '0x5208', - }, preferences: { showFiatInTestnets: false, }, @@ -484,6 +478,9 @@ describe('custom-gas selectors', () => { chainId: '0x1', }, }, + send: { + gasLimit: GAS_LIMITS.SIMPLE, + }, gas: { basicEstimates: { blockTime: 14.16326530612245, @@ -524,9 +521,6 @@ describe('custom-gas selectors', () => { metamask: { conversionRate: 2557.1, currentCurrency: 'usd', - send: { - gasLimit: '0x5208', - }, preferences: { showFiatInTestnets: false, }, @@ -535,6 +529,9 @@ describe('custom-gas selectors', () => { chainId: '0x4', }, }, + send: { + gasLimit: GAS_LIMITS.SIMPLE, + }, gas: { basicEstimates: { blockTime: 14.16326530612245, @@ -575,9 +572,6 @@ describe('custom-gas selectors', () => { metamask: { conversionRate: 2557.1, currentCurrency: 'usd', - send: { - gasLimit: '0x5208', - }, preferences: { showFiatInTestnets: true, }, @@ -586,6 +580,9 @@ describe('custom-gas selectors', () => { chainId: '0x4', }, }, + send: { + gasLimit: GAS_LIMITS.SIMPLE, + }, gas: { basicEstimates: { safeLow: 50, @@ -620,9 +617,6 @@ describe('custom-gas selectors', () => { metamask: { conversionRate: 2557.1, currentCurrency: 'usd', - send: { - gasLimit: '0x5208', - }, preferences: { showFiatInTestnets: true, }, @@ -631,6 +625,9 @@ describe('custom-gas selectors', () => { chainId: '0x1', }, }, + send: { + gasLimit: GAS_LIMITS.SIMPLE, + }, gas: { basicEstimates: { safeLow: 50, diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 70c249ed1..a41005a7e 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -23,7 +23,8 @@ import { import { TEMPLATED_CONFIRMATION_MESSAGE_TYPES } from '../pages/confirmation/templates'; import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; -import { getNativeCurrency } from './send'; +import { DAY } from '../../shared/constants/time'; +import { getNativeCurrency } from '../ducks/metamask/metamask'; /** * One of the only remaining valid uses of selecting the network subkey of the @@ -563,3 +564,15 @@ export function getSortedNotificationsToShow(state) { ); return notificationsSortedByDate; } + +export function getShowRecoveryPhraseReminder(state) { + const { + recoveryPhraseReminderLastShown, + recoveryPhraseReminderHasBeenShown, + } = state.metamask; + + const currentTime = new Date().getTime(); + const frequency = recoveryPhraseReminderHasBeenShown ? DAY * 90 : DAY * 2; + + return currentTime - recoveryPhraseReminderLastShown >= frequency; +} diff --git a/ui/selectors/send-selectors-test-data.js b/ui/selectors/send-selectors-test-data.js index e6c0d230c..b2663aadb 100644 --- a/ui/selectors/send-selectors-test-data.js +++ b/ui/selectors/send-selectors-test-data.js @@ -150,21 +150,6 @@ const state = { }, ], selectedAddress: '0xd85a4b6a394794842887b8284293d69163007bbb', - send: { - gasLimit: '0xFFFF', - gasPrice: '0xaa', - gasTotal: '0xb451dc41b578', - tokenBalance: 3434, - from: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', - to: '0x987fedabc', - amount: '0x080', - memo: '', - errors: { - someError: null, - }, - maxModeOn: false, - editingTransactionId: 97531, - }, unapprovedTxs: { 4768706228115573: { id: 4768706228115573, @@ -210,8 +195,19 @@ const state = { identities: {}, send: { fromDropdownOpen: false, - toDropdownOpen: false, - errors: { someError: null }, + gasLimit: '0xFFFF', + gasPrice: '0xaa', + gasTotal: '0xb451dc41b578', + tokenBalance: 3434, + from: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', + to: '0x987fedabc', + amount: '0x080', + memo: '', + errors: { + someError: null, + }, + maxModeOn: false, + editingTransactionId: 97531, }, }; diff --git a/ui/selectors/send.js b/ui/selectors/send.js index d6f291c9f..3d0b11d83 100644 --- a/ui/selectors/send.js +++ b/ui/selectors/send.js @@ -1,31 +1,17 @@ import abi from 'human-standard-token-abi'; import { calcGasTotal } from '../pages/send/send.utils'; import { - accountsWithSendEtherInfoSelector, - getAddressBook, getSelectedAccount, getTargetAccount, getAveragePriceEstimateInHexWEI, } from '.'; -export function getBlockGasLimit(state) { - return state.metamask.currentBlockGasLimit; -} - -export function getConversionRate(state) { - return state.metamask.conversionRate; -} - -export function getNativeCurrency(state) { - return state.metamask.nativeCurrency; -} - export function getGasLimit(state) { - return state.metamask.send.gasLimit || '0'; + return state.send.gasLimit || '0'; } export function getGasPrice(state) { - return state.metamask.send.gasPrice || getAveragePriceEstimateInHexWEI(state); + return state.send.gasPrice || getAveragePriceEstimateInHexWEI(state); } export function getGasTotal(state) { @@ -38,7 +24,7 @@ export function getPrimaryCurrency(state) { } export function getSendToken(state) { - return state.metamask.send.token; + return state.send.token; } export function getSendTokenAddress(state) { @@ -53,19 +39,15 @@ export function getSendTokenContract(state) { } export function getSendAmount(state) { - return state.metamask.send.amount; + return state.send.amount; } export function getSendHexData(state) { - return state.metamask.send.data; -} - -export function getSendHexDataFeatureFlagState(state) { - return state.metamask.featureFlags.sendHexData; + return state.send.data; } export function getSendEditingTransactionId(state) { - return state.metamask.send.editingTransactionId; + return state.send.editingTransactionId; } export function getSendErrors(state) { @@ -77,7 +59,7 @@ export function sendAmountIsInError(state) { } export function getSendFrom(state) { - return state.metamask.send.from; + return state.send.from; } export function getSendFromBalance(state) { @@ -93,36 +75,27 @@ export function getSendFromObject(state) { } export function getSendMaxModeState(state) { - return state.metamask.send.maxModeOn; + return state.send.maxModeOn; } export function getSendTo(state) { - return state.metamask.send.to; + return state.send.to; } export function getSendToNickname(state) { - return state.metamask.send.toNickname; + return state.send.toNickname; } -export function getSendToAccounts(state) { - const fromAccounts = accountsWithSendEtherInfoSelector(state); - const addressBookAccounts = getAddressBook(state); - return [...fromAccounts, ...addressBookAccounts]; -} export function getTokenBalance(state) { - return state.metamask.send.tokenBalance; + return state.send.tokenBalance; } export function getSendEnsResolution(state) { - return state.metamask.send.ensResolution; + return state.send.ensResolution; } export function getSendEnsResolutionError(state) { - return state.metamask.send.ensResolutionError; -} - -export function getUnapprovedTxs(state) { - return state.metamask.unapprovedTxs; + return state.send.ensResolutionError; } export function getQrCodeData(state) { diff --git a/ui/selectors/send.test.js b/ui/selectors/send.test.js index bab7b63fe..aadbc28e5 100644 --- a/ui/selectors/send.test.js +++ b/ui/selectors/send.test.js @@ -1,9 +1,5 @@ import sinon from 'sinon'; -import { TRANSACTION_STATUSES } from '../../shared/constants/transaction'; import { - getBlockGasLimit, - getConversionRate, - getNativeCurrency, getGasLimit, getGasPrice, getGasTotal, @@ -17,12 +13,9 @@ import { getSendFrom, getSendFromBalance, getSendFromObject, - getSendHexDataFeatureFlagState, getSendMaxModeState, getSendTo, - getSendToAccounts, getTokenBalance, - getUnapprovedTxs, gasFeeIsInError, getGasLoadingError, getGasButtonGroupShown, @@ -84,18 +77,6 @@ describe('send selectors', () => { }); }); - describe('getBlockGasLimit', () => { - it('should return the current block gas limit', () => { - expect(getBlockGasLimit(mockState)).toStrictEqual('0x4c1878'); - }); - }); - - describe('getConversionRate()', () => { - it('should return the eth conversion rate', () => { - expect(getConversionRate(mockState)).toStrictEqual(1200.88200327); - }); - }); - describe('getCurrentAccountWithSendEtherInfo()', () => { it('should return the currently selected account with identity info', () => { expect(getCurrentAccountWithSendEtherInfo(mockState)).toStrictEqual({ @@ -108,12 +89,6 @@ describe('send selectors', () => { }); }); - describe('getNativeCurrency()', () => { - it('should return the ticker symbol of the selected network', () => { - expect(getNativeCurrency(mockState)).toStrictEqual('ETH'); - }); - }); - describe('getGasLimit()', () => { it('should return the send.gasLimit', () => { expect(getGasLimit(mockState)).toStrictEqual('0xFFFF'); @@ -136,7 +111,7 @@ describe('send selectors', () => { it('should return the symbol of the send token', () => { expect( getPrimaryCurrency({ - metamask: { send: { token: { symbol: 'DEF' } } }, + send: { token: { symbol: 'DEF' } }, }), ).toStrictEqual('DEF'); }); @@ -146,13 +121,11 @@ describe('send selectors', () => { it('should return the current send token if set', () => { expect( getSendToken({ - metamask: { - send: { - token: { - address: '0x8d6b81208414189a58339873ab429b6c47ab92d3', - decimals: 4, - symbol: 'DEF', - }, + send: { + token: { + address: '0x8d6b81208414189a58339873ab429b6c47ab92d3', + decimals: 4, + symbol: 'DEF', }, }, }), @@ -168,13 +141,11 @@ describe('send selectors', () => { it('should return the contract at the send token address', () => { expect( getSendTokenContract({ - metamask: { - send: { - token: { - address: '0x8d6b81208414189a58339873ab429b6c47ab92d3', - decimals: 4, - symbol: 'DEF', - }, + send: { + token: { + address: '0x8d6b81208414189a58339873ab429b6c47ab92d3', + decimals: 4, + symbol: 'DEF', }, }, }), @@ -182,10 +153,7 @@ describe('send selectors', () => { }); it('should return null if send token is not set', () => { - const modifiedMetamaskState = { ...mockState.metamask, send: {} }; - expect( - getSendTokenContract({ ...mockState, metamask: modifiedMetamaskState }), - ).toBeNull(); + expect(getSendTokenContract({ ...mockState, send: {} })).toBeNull(); }); }); @@ -207,12 +175,6 @@ describe('send selectors', () => { }); }); - describe('getSendHexDataFeatureFlagState()', () => { - it('should return the sendHexData feature flag state', () => { - expect(getSendHexDataFeatureFlagState(mockState)).toStrictEqual(true); - }); - }); - describe('getSendFrom()', () => { it('should return the send.from', () => { expect(getSendFrom(mockState)).toStrictEqual( @@ -228,11 +190,10 @@ describe('send selectors', () => { it('should get the selected account balance if the send.from does not exist', () => { const editedMockState = { - metamask: { - ...mockState.metamask, - send: { - from: null, - }, + ...mockState, + send: { + ...mockState.send, + from: null, }, }; expect(getSendFromBalance(editedMockState)).toStrictEqual('0x0'); @@ -251,11 +212,9 @@ describe('send selectors', () => { it('should return the current account if send.from does not exist', () => { const editedMockState = { - metamask: { - ...mockState.metamask, - send: { - from: null, - }, + ...mockState, + send: { + from: null, }, }; expect(getSendFromObject(editedMockState)).toStrictEqual({ @@ -279,78 +238,12 @@ describe('send selectors', () => { }); }); - describe('getSendToAccounts()', () => { - it('should return an array including all the users accounts and the address book', () => { - expect(getSendToAccounts(mockState)).toStrictEqual([ - { - code: '0x', - balance: '0x47c9d71831c76efe', - nonce: '0x1b', - address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', - name: 'Send Account 1', - }, - { - code: '0x', - balance: '0x37452b1315889f80', - nonce: '0xa', - address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', - name: 'Send Account 2', - }, - { - code: '0x', - balance: '0x30c9d71831c76efe', - nonce: '0x1c', - address: '0x2f8d4a878cfa04a6e60d46362f5644deab66572d', - name: 'Send Account 3', - }, - { - code: '0x', - balance: '0x0', - nonce: '0x0', - address: '0xd85a4b6a394794842887b8284293d69163007bbb', - name: 'Send Account 4', - }, - { - address: '0x06195827297c7a80a443b6894d3bdb8824b43896', - name: 'Address Book Account 1', - chainId: '0x3', - }, - ]); - }); - }); - describe('getTokenBalance()', () => { it('should', () => { expect(getTokenBalance(mockState)).toStrictEqual(3434); }); }); - describe('getUnapprovedTxs()', () => { - it('should return the unapproved txs', () => { - expect(getUnapprovedTxs(mockState)).toStrictEqual({ - 4768706228115573: { - id: 4768706228115573, - time: 1487363153561, - status: TRANSACTION_STATUSES.UNAPPROVED, - gasMultiplier: 1, - metamaskNetworkId: '3', - txParams: { - from: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', - to: '0x18a3462427bcc9133bb46e88bcbe39cd7ef0e761', - value: '0xde0b6b3a7640000', - metamaskId: 4768706228115573, - metamaskNetworkId: '3', - gas: '0x5209', - }, - txFee: '17e0186e60800', - txValue: 'de0b6b3a7640000', - maxCost: 'de234b52e4a0800', - gasPrice: '4a817c800', - }, - }); - }); - }); - describe('send-amount-row selectors', () => { describe('sendAmountIsInError()', () => { it('should return true if send.errors.amount is truthy', () => { @@ -436,9 +329,7 @@ describe('send selectors', () => { describe('send-header selectors', () => { const getMetamaskSendMockState = (send) => { return { - metamask: { - send: { ...send }, - }, + send: { ...send }, }; }; diff --git a/ui/store/actionConstants.js b/ui/store/actionConstants.js index 8a9f5cd84..d16caa61f 100644 --- a/ui/store/actionConstants.js +++ b/ui/store/actionConstants.js @@ -39,24 +39,6 @@ export const COMPLETED_TX = 'COMPLETED_TX'; export const TRANSACTION_ERROR = 'TRANSACTION_ERROR'; export const UPDATE_TRANSACTION_PARAMS = 'UPDATE_TRANSACTION_PARAMS'; export const SET_NEXT_NONCE = 'SET_NEXT_NONCE'; -// send screen -export const UPDATE_GAS_LIMIT = 'UPDATE_GAS_LIMIT'; -export const UPDATE_GAS_PRICE = 'UPDATE_GAS_PRICE'; -export const UPDATE_GAS_TOTAL = 'UPDATE_GAS_TOTAL'; -export const UPDATE_SEND_HEX_DATA = 'UPDATE_SEND_HEX_DATA'; -export const UPDATE_SEND_TOKEN_BALANCE = 'UPDATE_SEND_TOKEN_BALANCE'; -export const UPDATE_SEND_TO = 'UPDATE_SEND_TO'; -export const UPDATE_SEND_AMOUNT = 'UPDATE_SEND_AMOUNT'; -export const UPDATE_SEND_ERRORS = 'UPDATE_SEND_ERRORS'; -export const UPDATE_MAX_MODE = 'UPDATE_MAX_MODE'; -export const UPDATE_SEND = 'UPDATE_SEND'; -export const UPDATE_SEND_TOKEN = 'UPDATE_SEND_TOKEN'; -export const CLEAR_SEND = 'CLEAR_SEND'; -export const GAS_LOADING_STARTED = 'GAS_LOADING_STARTED'; -export const GAS_LOADING_FINISHED = 'GAS_LOADING_FINISHED'; -export const UPDATE_SEND_ENS_RESOLUTION = 'UPDATE_SEND_ENS_RESOLUTION'; -export const UPDATE_SEND_ENS_RESOLUTION_ERROR = - 'UPDATE_SEND_ENS_RESOLUTION_ERROR'; // config screen export const SET_RPC_TARGET = 'SET_RPC_TARGET'; export const SET_PROVIDER_TYPE = 'SET_PROVIDER_TYPE'; diff --git a/ui/store/actions.js b/ui/store/actions.js index f5c28ee24..f2269cdad 100644 --- a/ui/store/actions.js +++ b/ui/store/actions.js @@ -3,7 +3,6 @@ import pify from 'pify'; import log from 'loglevel'; import { capitalize } from 'lodash'; import getBuyEthUrl from '../../app/scripts/lib/buy-eth-url'; -import { calcTokenBalance, estimateGasForSend } from '../pages/send/send.utils'; import { fetchLocale, loadRelativeTimeFormatLocaleData, @@ -13,7 +12,6 @@ import { getSymbolAndDecimals } from '../helpers/utils/token-util'; import switchDirection from '../helpers/utils/switch-direction'; import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../shared/constants/app'; import { hasUnconfirmedTransactions } from '../helpers/utils/confirm-tx.util'; -import { setCustomGasLimit } from '../ducks/gas/gas.duck'; import txHelper from '../helpers/utils/tx-helper'; import { getEnvironmentType, addHexPrefix } from '../../app/scripts/lib/util'; import { @@ -22,8 +20,9 @@ import { } from '../selectors'; import { switchedToUnconnectedAccount } from '../ducks/alerts/unconnected-account'; import { getUnconnectedAccountAlertEnabledness } from '../ducks/metamask/metamask'; -import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { LISTED_CONTRACT_ADDRESSES } from '../../shared/constants/tokens'; +import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; +import { clearSend } from '../ducks/send/send.duck'; import * as actionConstants from './actionConstants'; let background = null; @@ -38,7 +37,6 @@ export function goHome() { type: actionConstants.GO_HOME, }; } - // async actions export function tryUnlockMetamask(password) { @@ -624,138 +622,18 @@ export function signTypedMsg(msgData) { } export function signTx(txData) { - return (dispatch) => { + return async (dispatch) => { + dispatch(showLoadingIndication()); global.ethQuery.sendTransaction(txData, (err) => { if (err) { dispatch(displayWarning(err.message)); } }); + dispatch(hideLoadingIndication()); dispatch(showConfTxPage()); }; } -export function setGasLimit(gasLimit) { - return { - type: actionConstants.UPDATE_GAS_LIMIT, - value: gasLimit, - }; -} - -export function setGasPrice(gasPrice) { - return { - type: actionConstants.UPDATE_GAS_PRICE, - value: gasPrice, - }; -} - -export function setGasTotal(gasTotal) { - return { - type: actionConstants.UPDATE_GAS_TOTAL, - value: gasTotal, - }; -} - -export function updateGasData({ - gasPrice, - blockGasLimit, - selectedAddress, - sendToken, - to, - value, - data, -}) { - return (dispatch) => { - dispatch(gasLoadingStarted()); - return estimateGasForSend({ - estimateGasMethod: promisifiedBackground.estimateGas, - blockGasLimit, - selectedAddress, - sendToken, - to, - value, - estimateGasPrice: gasPrice, - data, - }) - .then((gas) => { - dispatch(setGasLimit(gas)); - dispatch(setCustomGasLimit(gas)); - dispatch(updateSendErrors({ gasLoadingError: null })); - dispatch(gasLoadingFinished()); - }) - .catch((err) => { - log.error(err); - dispatch(updateSendErrors({ gasLoadingError: 'gasLoadingError' })); - dispatch(gasLoadingFinished()); - }); - }; -} - -export function gasLoadingStarted() { - return { - type: actionConstants.GAS_LOADING_STARTED, - }; -} - -export function gasLoadingFinished() { - return { - type: actionConstants.GAS_LOADING_FINISHED, - }; -} - -export function updateSendTokenBalance({ sendToken, tokenContract, address }) { - return (dispatch) => { - const tokenBalancePromise = tokenContract - ? tokenContract.balanceOf(address) - : Promise.resolve(); - return tokenBalancePromise - .then((usersToken) => { - if (usersToken) { - const newTokenBalance = calcTokenBalance({ sendToken, usersToken }); - dispatch(setSendTokenBalance(newTokenBalance)); - } - }) - .catch((err) => { - log.error(err); - updateSendErrors({ tokenBalance: 'tokenBalanceError' }); - }); - }; -} - -export function updateSendErrors(errorObject) { - return { - type: actionConstants.UPDATE_SEND_ERRORS, - value: errorObject, - }; -} - -export function setSendTokenBalance(tokenBalance) { - return { - type: actionConstants.UPDATE_SEND_TOKEN_BALANCE, - value: tokenBalance, - }; -} - -export function updateSendHexData(value) { - return { - type: actionConstants.UPDATE_SEND_HEX_DATA, - value, - }; -} - -export function updateSendTo(to, nickname = '') { - return { - type: actionConstants.UPDATE_SEND_TO, - value: { to, nickname }, - }; -} - -export function updateSendAmount(amount) { - return { - type: actionConstants.UPDATE_SEND_AMOUNT, - value: amount, - }; -} - export function updateCustomNonce(value) { return { type: actionConstants.UPDATE_CUSTOM_NONCE, @@ -763,57 +641,15 @@ export function updateCustomNonce(value) { }; } -export function setMaxModeTo(bool) { - return { - type: actionConstants.UPDATE_MAX_MODE, - value: bool, - }; -} - -export function updateSend(newSend) { - return { - type: actionConstants.UPDATE_SEND, - value: newSend, - }; -} - -export function updateSendToken(token) { - return { - type: actionConstants.UPDATE_SEND_TOKEN, - value: token, - }; -} - -export function clearSend() { - return { - type: actionConstants.CLEAR_SEND, - }; -} - -export function updateSendEnsResolution(ensResolution) { - return { - type: actionConstants.UPDATE_SEND_ENS_RESOLUTION, - payload: ensResolution, - }; -} - -export function updateSendEnsResolutionError(errorMessage) { - return { - type: actionConstants.UPDATE_SEND_ENS_RESOLUTION_ERROR, - payload: errorMessage, - }; -} - export function signTokenTx(tokenAddress, toAddress, amount, txData) { return async (dispatch) => { dispatch(showLoadingIndication()); try { const token = global.eth.contract(abi).at(tokenAddress); - const txPromise = token.transfer(toAddress, addHexPrefix(amount), txData); + token.transfer(toAddress, addHexPrefix(amount), txData); dispatch(showConfTxPage()); dispatch(hideLoadingIndication()); - await txPromise; } catch (error) { dispatch(hideLoadingIndication()); dispatch(displayWarning(error.message)); @@ -2570,16 +2406,24 @@ export function setConnectedStatusPopoverHasBeenShown() { }; } -export async function setAlertEnabledness(alertId, enabledness) { - await promisifiedBackground.setAlertEnabledness(alertId, enabledness); -} - -export async function setUnconnectedAccountAlertShown(origin) { - await promisifiedBackground.setUnconnectedAccountAlertShown(origin); +export function setRecoveryPhraseReminderHasBeenShown() { + return () => { + background.setRecoveryPhraseReminderHasBeenShown((err) => { + if (err) { + throw new Error(err.message); + } + }); + }; } -export async function setWeb3ShimUsageAlertDismissed(origin) { - await promisifiedBackground.setWeb3ShimUsageAlertDismissed(origin); +export function setRecoveryPhraseReminderLastShown(lastShown) { + return () => { + background.setRecoveryPhraseReminderLastShown(lastShown, (err) => { + if (err) { + throw new Error(err.message); + } + }); + }; } export function loadingMethodDataStarted() { @@ -2848,6 +2692,18 @@ export function setLedgerLivePreference(value) { }; } +// Wrappers around promisifedBackground +/** + * The "actions" below are not actions nor action creators. They cannot use + * dispatch nor should they be dispatched when used. Instead they can be + * called directly. These wrappers will be moved into their location at some + * point in the future. + */ + +export function estimateGas(params) { + return promisifiedBackground.estimateGas(params); +} + // MetaMetrics /** * @typedef {import('../../shared/constants/metametrics').MetaMetricsEventPayload} MetaMetricsEventPayload @@ -2879,3 +2735,15 @@ export function updateViewedNotifications(notificationIdViewedStatusMap) { notificationIdViewedStatusMap, ); } + +export async function setAlertEnabledness(alertId, enabledness) { + await promisifiedBackground.setAlertEnabledness(alertId, enabledness); +} + +export async function setUnconnectedAccountAlertShown(origin) { + await promisifiedBackground.setUnconnectedAccountAlertShown(origin); +} + +export async function setWeb3ShimUsageAlertDismissed(origin) { + await promisifiedBackground.setWeb3ShimUsageAlertDismissed(origin); +} diff --git a/ui/store/actions.test.js b/ui/store/actions.test.js index 409f03853..d28defb8c 100644 --- a/ui/store/actions.test.js +++ b/ui/store/actions.test.js @@ -5,6 +5,7 @@ import EthQuery from 'eth-query'; import enLocale from '../../app/_locales/en/messages.json'; import MetaMaskController from '../../app/scripts/metamask-controller'; import { TRANSACTION_STATUSES } from '../../shared/constants/transaction'; +import { GAS_LIMITS } from '../../shared/constants/gas'; import * as actions from './actions'; const middleware = [thunk]; @@ -618,7 +619,7 @@ describe('Actions', () => { it('calls setCurrentCurrency', async () => { const store = mockStore(); - background.setCurrentCurrency.callsFake((_, cb) => cb()); + background.setCurrentCurrency = sinon.stub().callsFake((_, cb) => cb()); actions._setBackgroundConnection(background); await store.dispatch(actions.setCurrentCurrency('jpy')); @@ -627,9 +628,9 @@ describe('Actions', () => { it('throws if setCurrentCurrency throws', async () => { const store = mockStore(); - background.setCurrentCurrency.callsFake((_, cb) => - cb(new Error('error')), - ); + background.setCurrentCurrency = sinon + .stub() + .callsFake((_, cb) => cb(new Error('error'))); actions._setBackgroundConnection(background); const expectedActions = [ @@ -837,7 +838,9 @@ describe('Actions', () => { it('errors in when sendTransaction throws', async () => { const store = mockStore(); const expectedActions = [ + { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'DISPLAY_WARNING', value: 'error' }, + { type: 'HIDE_LOADING_INDICATION' }, { type: 'SHOW_CONF_TX_PAGE', id: undefined }, ]; @@ -852,67 +855,6 @@ describe('Actions', () => { }); }); - describe('#updatedGasData', () => { - it('errors when get code does not return', async () => { - const store = mockStore(); - - background.estimateGas = sinon.stub().rejects(); - - actions._setBackgroundConnection(background); - - global.eth = { - getCode: sinon.stub().rejects(), - }; - - 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 - }; - - await store.dispatch(actions.updateGasData(mockData)); - - expect(store.getActions()).toStrictEqual(expectedActions); - }); - - it('returns default gas limit for basic eth transaction', async () => { - const mockData = { - gasPrice: '0x3b9aca00', - blockGasLimit: '0x6ad79a', // 7002010 - selectedAddress: '0x0DCD5D886577d5081B0c52e242Ef29E70Be3E7bc', - to: '0xEC1Adf982415D2Ef5ec55899b9Bfb8BC0f29251B', - value: '0xde0b6b3a7640000', // 1000000000000000000 - }; - - global.eth = { - getCode: sinon.stub().returns('0x'), - }; - 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)); - expect(store.getActions()).toStrictEqual(expectedActions); - }); - }); - describe('#signTokenTx', () => { it('calls eth.contract', async () => { global.eth = { @@ -929,7 +871,7 @@ describe('Actions', () => { describe('#updateTransaction', () => { const txParams = { from: '0x1', - gas: '0x5208', + gas: GAS_LIMITS.SIMPLE, gasPrice: '0x3b9aca00', to: '0x2', value: '0x0', @@ -998,7 +940,7 @@ describe('Actions', () => { id: '1', value: { from: '0x1', - gas: '0x5208', + gas: GAS_LIMITS.SIMPLE, gasPrice: '0x3b9aca00', to: '0x2', value: '0x0', diff --git a/yarn.lock b/yarn.lock index cfd31b2f5..c5537275a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2620,21 +2620,26 @@ prop-types "^15.7.2" react-is "^16.8.0" -"@metamask/auto-changelog@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@metamask/auto-changelog/-/auto-changelog-1.0.0.tgz#ca6a71d1b983cf08b715bdcd8e240d746974d0c7" - integrity sha512-3Bcm+JsEmNllPi7kRtzS6EAjYTzz+Isa4QFq2DQ4DFwIsv2HUxdR+KNU2GJ1BdX4lbPcQTrpTdaPgBZ9G4NhLA== +"@metamask/auto-changelog@^2.1.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@metamask/auto-changelog/-/auto-changelog-2.3.0.tgz#e8edf9210753b495d799b64af07de24ed0839004" + integrity sha512-l5Tk9Dx1+wF3L1ZvN+HObS7R077BDErRmElq5LckJTtAbAhEhg/MMD/6u2yZBmVIlPrint29BVt3tsqRp1GjIA== dependencies: cross-spawn "^7.0.3" diff "^5.0.0" semver "^7.3.5" yargs "^17.0.1" -"@metamask/contract-metadata@^1.19.0", "@metamask/contract-metadata@^1.22.0", "@metamask/contract-metadata@^1.24.0": +"@metamask/contract-metadata@^1.19.0", "@metamask/contract-metadata@^1.25.0": version "1.25.0" resolved "https://registry.yarnpkg.com/@metamask/contract-metadata/-/contract-metadata-1.25.0.tgz#442ace91fb40165310764b68d8096d0017bb0492" integrity sha512-yhmYB9CQPv0dckNcPoWDcgtrdUp0OgK0uvkRE5QIBv4b3qENI1/03BztvK2ijbTuMlORUpjPq7/1MQDUPoRPVw== +"@metamask/contract-metadata@^1.26.0": + version "1.26.0" + resolved "https://registry.yarnpkg.com/@metamask/contract-metadata/-/contract-metadata-1.26.0.tgz#06be4f4dc645da69f6364f75cb2bd47afa642fe6" + integrity sha512-58A8csapIPoc854n6AI+3jwJcQfh75BzVrl6SAySgJ9fWTa1XItm9Tx/ORaqYrwaR/9JqH4SnkbXtqyFwuUL6w== + "@metamask/controllers@^5.0.0": version "5.1.0" resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-5.1.0.tgz#02c1957295bcb6db1655a716d165665d170e7f34" @@ -2663,20 +2668,20 @@ web3 "^0.20.7" web3-provider-engine "^16.0.1" -"@metamask/controllers@^8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-8.0.0.tgz#42ac5aaef67a03d3fe599a67a36597e01902ca8d" - integrity sha512-TrteMifsCxV1g3WHcSD1X98fF4hKep3sXZNGfrvkPqa8mrF03hJke21WBSTRtvJ3vkNLRWgi+5I6lVXFTzbYuQ== +"@metamask/controllers@^9.0.0": + version "9.1.0" + resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-9.1.0.tgz#4434f22eba2522889224b35aa08bc7b67d7248b7" + integrity sha512-jn/F0BNbaPsgEevHaPqk0lGAONKom4re1a4yBC67h7Vu6yu26CRi30SJl4xIh3IW4+ySbPhVLaiXFiXr3fESRQ== dependencies: - "@metamask/contract-metadata" "^1.24.0" + "@metamask/contract-metadata" "^1.25.0" "@types/uuid" "^8.3.0" async-mutex "^0.2.6" babel-runtime "^6.26.0" eth-ens-namehash "^2.0.8" eth-json-rpc-infura "^5.1.0" - eth-keyring-controller "^6.1.0" + eth-keyring-controller "^6.2.1" eth-method-registry "1.1.0" - eth-phishing-detect "^1.1.13" + eth-phishing-detect "^1.1.14" eth-query "^2.1.2" eth-rpc-errors "^4.0.0" eth-sig-util "^3.0.0" @@ -2690,6 +2695,7 @@ isomorphic-fetch "^3.0.0" jsonschema "^1.2.4" nanoid "^3.1.12" + punycode "^2.1.1" single-call-balance-checker-abi "^1.0.0" uuid "^8.3.2" web3 "^0.20.7" @@ -10509,20 +10515,6 @@ eth-ens-namehash@^1.0.2: idna-uts46 "^1.0.1" js-sha3 "^0.5.7" -eth-hd-keyring@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/eth-hd-keyring/-/eth-hd-keyring-3.5.0.tgz#3976d83a27b24305481c389178f290d9264e839d" - integrity sha512-Ix1LcWYxHMxCCSIMz+TLXLtt50zF6ZDd/TRVXthdw91IwOk1ajuf7QHg3bCDcfeUpdf9oEpwIPbL3xjDqEEjYw== - dependencies: - bip39 "^2.2.0" - eth-sig-util "^2.4.4" - eth-simple-keyring "^3.5.0" - ethereumjs-abi "^0.6.5" - ethereumjs-util "^5.1.1" - ethereumjs-wallet "^0.6.0" - events "^1.1.1" - xtend "^4.0.1" - eth-hd-keyring@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/eth-hd-keyring/-/eth-hd-keyring-3.6.0.tgz#6835d30aa411b8d3ef098e82f6427b5325082abb" @@ -10603,25 +10595,10 @@ eth-json-rpc-middleware@^6.0.0: pify "^3.0.0" safe-event-emitter "^1.0.1" -eth-keyring-controller@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/eth-keyring-controller/-/eth-keyring-controller-6.1.0.tgz#dc9313d0b793e085dc1badf84dd4f5e3004e127e" - integrity sha512-wPxH++98VDBcDv9YkPzxhZC0gF1ixuRbyKR2u/NOT/roBpNQDe4reqyllBRC7jhPehiKnRxzf7r6HEyirRnPxQ== - dependencies: - bip39 "^2.4.0" - bluebird "^3.5.0" - browser-passworder "^2.0.3" - eth-hd-keyring "^3.5.0" - eth-sig-util "^1.4.0" - eth-simple-keyring "^3.5.0" - ethereumjs-util "^5.1.2" - loglevel "^1.5.0" - obs-store "^4.0.3" - -eth-keyring-controller@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/eth-keyring-controller/-/eth-keyring-controller-6.2.0.tgz#c649e7ced9bc9a11c2a6ab48234fae212569d390" - integrity sha512-UYYs+hTgrJNqy7xkI56QpekfsPuZw4kLxrFEUkHefCkBv9poSg/Abx4rvBsRwcj7yxXcxfgTNtoltJfR2we4uw== +eth-keyring-controller@^6.1.0, eth-keyring-controller@^6.2.0, eth-keyring-controller@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/eth-keyring-controller/-/eth-keyring-controller-6.2.1.tgz#61901071fc74059ed37cb5ae93870fdcae6e3781" + integrity sha512-x2gTM1iHp2Kbvdtd9Eslysw0qzVZiqOzpVB3AU/ni2Xiit+rlcv2H80zYKjrEwlfWFDj4YILD3bOqlnEMmRJOA== dependencies: bip39 "^2.4.0" bluebird "^3.5.0" @@ -10698,7 +10675,7 @@ eth-rpc-errors@^4.0.0, eth-rpc-errors@^4.0.2: dependencies: fast-safe-stringify "^2.0.6" -eth-sig-util@^1.4.0, eth-sig-util@^1.4.2: +eth-sig-util@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/eth-sig-util/-/eth-sig-util-1.4.2.tgz#8d958202c7edbaae839707fba6f09ff327606210" integrity sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA= @@ -10706,7 +10683,7 @@ eth-sig-util@^1.4.0, eth-sig-util@^1.4.2: ethereumjs-abi "git+https://github.com/ethereumjs/ethereumjs-abi.git" ethereumjs-util "^5.1.1" -eth-sig-util@^2.0.0, eth-sig-util@^2.4.4, eth-sig-util@^2.5.0: +eth-sig-util@^2.0.0: version "2.5.4" resolved "https://registry.yarnpkg.com/eth-sig-util/-/eth-sig-util-2.5.4.tgz#577b01fe491b6bf59b0464be09633e20c1677bc5" integrity sha512-aCMBwp8q/4wrW4QLsF/HYBOSA7TpLKmkVwP3pYQNkEEseW2Rr8Z5Uxc9/h6HX+OG3tuHo+2bINVSihIeBfym6A== @@ -10726,18 +10703,6 @@ eth-sig-util@^3.0.0, eth-sig-util@^3.0.1: tweetnacl "^1.0.3" tweetnacl-util "^0.15.0" -eth-simple-keyring@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/eth-simple-keyring/-/eth-simple-keyring-3.5.0.tgz#c7fa285ca58d31ef44bc7db678b689f9ffd7b453" - integrity sha512-z9IPt9aoMWAw5Zc3Jk/HKbWPJNc7ivZ5ECNtl3ZoQUGRnwoWO71W5+liVPJtXFNacGOOGsBfqTqrXL9C4EnYYQ== - dependencies: - eth-sig-util "^2.5.0" - ethereumjs-abi "^0.6.5" - ethereumjs-util "^5.1.1" - ethereumjs-wallet "^0.6.0" - events "^1.1.1" - xtend "^4.0.1" - eth-simple-keyring@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/eth-simple-keyring/-/eth-simple-keyring-4.2.0.tgz#c197a4bd4cce7d701b5f3607d0b843112ddb17e3" @@ -10828,7 +10793,7 @@ ethereum-ens-network-map@^1.0.0, ethereum-ens-network-map@^1.0.2: resolved "https://registry.yarnpkg.com/ethereum-ens-network-map/-/ethereum-ens-network-map-1.0.2.tgz#4e27bad18dae7bd95d84edbcac2c9e739fc959b9" integrity sha512-5qwJ5n3YhjSpE6O/WEBXCAb2nagUgyagJ6C0lGUBWC4LjKp/rRzD+pwtDJ6KCiITFEAoX4eIrWOjRy0Sylq5Hg== -ethereumjs-abi@0.6.8, ethereumjs-abi@^0.6.4, ethereumjs-abi@^0.6.5, ethereumjs-abi@^0.6.8: +ethereumjs-abi@0.6.8, ethereumjs-abi@^0.6.4, ethereumjs-abi@^0.6.8: version "0.6.8" resolved "https://registry.yarnpkg.com/ethereumjs-abi/-/ethereumjs-abi-0.6.8.tgz#71bc152db099f70e62f108b7cdfca1b362c6fcae" integrity sha512-Tx0r/iXI6r+lRsdvkFDlut0N08jWMnKRZ6Gkq+Nmw75lZe4e6o3EkSnkaBP5NF6+m5PTGAr9JP43N3LyeoglsA== @@ -10951,19 +10916,19 @@ ethereumjs-util@^5.0.0, ethereumjs-util@^5.0.1, ethereumjs-util@^5.1.1, ethereum safe-buffer "^5.1.1" secp256k1 "^3.0.1" -ethereumjs-util@^7.0.2: - version "7.0.8" - resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.0.8.tgz#5258762b7b17e3d828e41834948363ff0a703ffd" - integrity sha512-JJt7tDpCAmDPw/sGoFYeq0guOVqT3pTE9xlEbBmc/nlCij3JRCoS2c96SQ6kXVHOT3xWUNLDm5QCJLQaUnVAtQ== +ethereumjs-util@^7.0.10: + version "7.0.10" + resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.0.10.tgz#5fb7b69fa1fda0acc59634cf39d6b0291180fc1f" + integrity sha512-c/xThw6A+EAnej5Xk5kOzFzyoSnw0WX0tSlZ6pAsfGVvQj3TItaDg9b1+Fz1RJXA+y2YksKwQnuzgt1eY6LKzw== dependencies: - "@types/bn.js" "^4.11.3" + "@types/bn.js" "^5.1.0" bn.js "^5.1.2" create-hash "^1.1.2" ethereum-cryptography "^0.1.3" ethjs-util "0.1.6" rlp "^2.2.4" -ethereumjs-util@^7.0.9: +ethereumjs-util@^7.0.2, ethereumjs-util@^7.0.9: version "7.0.9" resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.0.9.tgz#2038baeb30f370a3e576ec175bd70bbbb6807d42" integrity sha512-cRqvYYKJoitq6vMKMf8pXeVwvTrX+dRD0JwHaYqm8jvogK14tqIoCWH/KUHcRwnVxVXEYF/o6pup5jRG4V0xzg== @@ -11026,7 +10991,7 @@ ethereumjs-vm@^2.1.0, ethereumjs-vm@^2.3.4, ethereumjs-vm@^2.6.0: rustbn.js "~0.2.0" safe-buffer "^5.1.1" -ethereumjs-wallet@0.6.5, ethereumjs-wallet@^0.6.0, ethereumjs-wallet@^0.6.4: +ethereumjs-wallet@0.6.5, ethereumjs-wallet@^0.6.4: version "0.6.5" resolved "https://registry.yarnpkg.com/ethereumjs-wallet/-/ethereumjs-wallet-0.6.5.tgz#685e9091645cee230ad125c007658833991ed474" integrity sha512-MDwjwB9VQVnpp/Dc1XzA6J1a3wgHQ4hSvA1uWNatdpOrtCbPVuQSKSyRnjLvS0a+KKMw2pvQ9Ybqpb3+eW8oNA== @@ -13533,6 +13498,13 @@ history@^4.9.0: tiny-warning "^1.0.0" value-equal "^1.0.1" +history@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/history/-/history-5.0.0.tgz#0cabbb6c4bbf835addb874f8259f6d25101efd08" + integrity sha512-3NyRMKIiFSJmIPdq7FxkNMJkQ7ZEtVblOQ38VtKaA0zZMW1Eo6Q6W8oDKEflr1kNNTItSnk4JMCO1deeSgbLLg== + dependencies: + "@babel/runtime" "^7.7.6" + hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -23548,14 +23520,7 @@ semver@7.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== -semver@^7.2.1, semver@^7.3.2, semver@^7.3.4: - version "7.3.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" - integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== - dependencies: - lru-cache "^6.0.0" - -semver@^7.3.5: +semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: version "7.3.5" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== @@ -25592,9 +25557,9 @@ trezor-link@1.7.3: whatwg-fetch "^3.5.0" trim-newlines@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30" - integrity sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA== + version "3.0.1" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" + integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== trim-repeated@^1.0.0: version "1.0.0" @@ -26328,12 +26293,7 @@ uuid@^3.1.0, uuid@^3.2.1, uuid@^3.2.2, uuid@^3.3.2, uuid@^3.3.3: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ== -uuid@^8.0.0: - version "8.3.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31" - integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg== - -uuid@^8.3.0, uuid@^8.3.2: +uuid@^8.0.0, uuid@^8.3.0, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==