diff --git a/.circleci/config.yml b/.circleci/config.yml index 3313db7f8..973ee001a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -304,7 +304,7 @@ jobs: steps: - run: name: Validate changelog - command: yarn auto-changelog validate + command: yarn lint:changelog - when: condition: matches: @@ -313,7 +313,7 @@ jobs: steps: - run: name: Validate release candidate changelog - command: yarn auto-changelog validate --rc + command: yarn lint:changelog:rc test-deps-audit: diff --git a/.circleci/scripts/chrome-install.sh b/.circleci/scripts/chrome-install.sh index 2691ebf72..fd20877f8 100755 --- a/.circleci/scripts/chrome-install.sh +++ b/.circleci/scripts/chrome-install.sh @@ -4,11 +4,13 @@ set -e set -u set -o pipefail -CHROME_VERSION='87.0.4280.88-1' +# To get the latest version, see +CHROME_VERSION='93.0.4577.63-1' CHROME_BINARY="google-chrome-stable_${CHROME_VERSION}_amd64.deb" CHROME_BINARY_URL="https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/${CHROME_BINARY}" -CHROME_BINARY_SHA512SUM='19eea1d1be171cab60ce5135572da9388b4b72e313118478b53f65c0bf2293733809282736b98ef828a208b7426e5191258f8c666cba7510b8bf5c92d0010a47' +# To retrieve this checksum, run the `wget` and `shasum` commands below +CHROME_BINARY_SHA512SUM='4102ba417b41820da68b7e8e12018ed2268f30e0210f8f227aeeabf6bd9265dd95ad206993d5626ac1c70a07185fd3ed4eef8a71ee2f5b0770015302c0d26f58' wget -O "${CHROME_BINARY}" -t 5 "${CHROME_BINARY_URL}" diff --git a/.depcheckrc.yml b/.depcheckrc.yml index b1892acb9..70214b911 100644 --- a/.depcheckrc.yml +++ b/.depcheckrc.yml @@ -11,21 +11,22 @@ ignores: # # dev deps # - + # safety fallback for npm lifecycle scripts, not used normally - "@lavamoat/preinstall-always-fail" # used in testing + ci - "@metamask/auto-changelog" # invoked as `auto-changelog` - "@metamask/forwarder" - "@metamask/test-dapp" - - "chromedriver" - - "geckodriver" - - "ganache-cli" - - "lavamoat-viz" - "@sentry/cli" # invoked as `sentry-cli` + - "chromedriver" + - "depcheck" # ooo meta + - "ganache-cli" + - "geckodriver" + - "jest" + - "lavamoat-viz" - "prettier-plugin-sort-json" # automatically imported by prettier - "source-map-explorer" - - "depcheck" # ooo meta # development tool - "yarn-deduplicate" # storybook diff --git a/.eslintrc.js b/.eslintrc.js index d3763fdef..14d5482de 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -53,6 +53,26 @@ module.exports = { 'prefer-object-spread': 'error', 'require-atomic-updates': 'off', + // This is the same as our default config, but for the noted exceptions + 'spaced-comment': [ + 'error', + 'always', + { + markers: [ + 'global', + 'globals', + 'eslint', + 'eslint-disable', + '*package', + '!', + ',', + // Local additions + '/:', // This is for our code fences + ], + exceptions: ['=', '-'], + }, + ], + 'import/no-unassigned-import': 'off', 'no-invalid-this': 'off', @@ -112,6 +132,7 @@ module.exports = { 'ui/**/*.test.js', 'ui/__mocks__/*.js', 'shared/**/*.test.js', + 'development/**/*.test.js', ], extends: ['@metamask/eslint-config-mocha'], rules: { @@ -129,7 +150,12 @@ module.exports = { }, }, { - files: ['ui/**/*.test.js', 'ui/__mocks__/*.js', 'shared/**/*.test.js'], + files: [ + 'ui/**/*.test.js', + 'ui/__mocks__/*.js', + 'shared/**/*.test.js', + 'development/**/*.test.js', + ], extends: ['@metamask/eslint-config-jest'], rules: { 'jest/no-restricted-matchers': 'off', @@ -166,6 +192,16 @@ module.exports = { sourceType: 'script', }, }, + { + files: [ + 'app/scripts/lockdown-run.js', + 'test/unit-global/protect-intrinsics.test.js', + ], + globals: { + harden: 'readonly', + Compartment: 'readonly', + }, + }, ], settings: { diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 9bef72a01..b97bb8247 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -22,6 +22,6 @@ jobs: url-to-cladocument: 'https://metamask.io/cla.html' # This branch can't have protections, commits are made directly to the specified branch. branch: 'cla-signatures' - allowlist: 'dependabot[bot],metamaskbot,muji' + allowlist: 'dependabot[bot],metamaskbot' allow-organization-members: true blockchain-storage-flag: false diff --git a/.storybook/images/UNI.png b/.storybook/images/UNI.png new file mode 100644 index 000000000..01ffcf726 Binary files /dev/null and b/.storybook/images/UNI.png differ diff --git a/.storybook/initial-states/approval-screens/add-suggested-token.js b/.storybook/initial-states/approval-screens/add-suggested-token.js new file mode 100644 index 000000000..0b623e3f7 --- /dev/null +++ b/.storybook/initial-states/approval-screens/add-suggested-token.js @@ -0,0 +1,65 @@ +export const suggestedTokens = { + "0x6b175474e89094c44da98b954eedeac495271d0f": { + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "symbol": "META", + "decimals": 18, + "image": "metamark.svg", + "unlisted": false + }, + "0xB8c77482e45F1F44dE1745F52C74426C631bDD52": { + "address": "0xB8c77482e45F1F44dE1745F52C74426C631bDD52", + "symbol": "0X", + "decimals": 18, + "image": "0x.svg", + "unlisted": false + }, + "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984": { + "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + "symbol": "AST", + "decimals": 18, + "image": "ast.png", + "unlisted": false + }, + "0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2": { + "address": "0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2", + "symbol": "BAT", + "decimals": 18, + "image": "BAT_icon.svg", + "unlisted": false + }, + "0xe83cccfabd4ed148903bf36d4283ee7c8b3494d1": { + "address": "0xe83cccfabd4ed148903bf36d4283ee7c8b3494d1", + "symbol": "CVL", + "decimals": 18, + "image": "CVL_token.svg", + "unlisted": false + }, + "0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e": { + "address": "0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e", + "symbol": "GLA", + "decimals": 18, + "image": "gladius.svg", + "unlisted": false + }, + "0x467Bccd9d29f223BcE8043b84E8C8B282827790F": { + "address": "0x467Bccd9d29f223BcE8043b84E8C8B282827790F", + "symbol": "GNO", + "decimals": 18, + "image": "gnosis.svg", + "unlisted": false + }, + "0xff20817765cb7f73d4bde2e66e067e58d11095c2": { + "address": "0xff20817765cb7f73d4bde2e66e067e58d11095c2", + "symbol": "OMG", + "decimals": 18, + "image": "omg.jpg", + "unlisted": false + }, + "0x8e870d67f660d95d5be530380d0ec0bd388289e1": { + "address": "0x8e870d67f660d95d5be530380d0ec0bd388289e1", + "symbol": "WED", + "decimals": 18, + "image": "wed.png", + "unlisted": false + }, + } \ No newline at end of file diff --git a/.storybook/initial-states/approval-screens/add-token.js b/.storybook/initial-states/approval-screens/add-token.js new file mode 100644 index 000000000..c5a7699f1 --- /dev/null +++ b/.storybook/initial-states/approval-screens/add-token.js @@ -0,0 +1,56 @@ +export const tokens = { + "0x33f90dee07c6e8b9682dd20f73e6c358b2ed0f03": { + "address": "0x33f90dee07c6e8b9682dd20f73e6c358b2ed0f03", + "symbol": "TRDT", + "decimals": 18, + "unlisted": false + }, + "0x39013f961c378f02c2b82a6e1d31e9812786fd9d": { + "address": "0x39013f961c378f02c2b82a6e1d31e9812786fd9d", + "symbol": "SMS", + "decimals": 18, + "unlisted": false + }, + "0x78b7fada55a64dd895d8c8c35779dd8b67fa8a05": { + "address": "0x78b7fada55a64dd895d8c8c35779dd8b67fa8a05", + "symbol": "ATL", + "decimals": 18, + "unlisted": false + }, + "0xfd8971d5e8e1740ce2d0a84095fca4de729d0c16": { + "address": "0xfd8971d5e8e1740ce2d0a84095fca4de729d0c16", + "symbol": "ZLA", + "decimals": 18, + "unlisted": false + }, + "0xe83cccfabd4ed148903bf36d4283ee7c8b3494d1": { + "address": "0xe83cccfabd4ed148903bf36d4283ee7c8b3494d1", + "symbol": "BTT", + "decimals": 18, + "unlisted": false + }, + "0x7a07e1a0c2514d51132183ecfea2a880ec3b7648": { + "address": "0x7a07e1a0c2514d51132183ecfea2a880ec3b7648", + "symbol": "IXE", + "decimals": 18, + "unlisted": false + }, + "0x467Bccd9d29f223BcE8043b84E8C8B282827790F": { + "address": "0x467Bccd9d29f223BcE8043b84E8C8B282827790F", + "symbol": "TEL", + "decimals": 18, + "unlisted": false + }, + "0xff20817765cb7f73d4bde2e66e067e58d11095c2": { + "address": "0xff20817765cb7f73d4bde2e66e067e58d11095c2", + "symbol": "AMP", + "decimals": 18, + "unlisted": false + }, + "0x15bda08c3afbf5955d6e9b235fd55a1fd0dbc829": { + "address": "0x15bda08c3afbf5955d6e9b235fd55a1fd0dbc829", + "symbol": "APC", + "decimals": 18, + "unlisted": false + }, + } \ No newline at end of file diff --git a/.storybook/main.js b/.storybook/main.js index 310bce17d..843c63b77 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -12,6 +12,10 @@ module.exports = { './i18n-party-addon/register.js', ], webpackFinal: async (config) => { + config.context = process.cwd() + config.node = { + __filename: true + } config.module.strictExportPresence = true; config.module.rules.push({ test: /\.scss$/, diff --git a/.storybook/preview.js b/.storybook/preview.js index 525401408..30f6d69ea 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -45,6 +45,10 @@ const styles = { alignItems: 'center', }; +export const getNewState = (state, props) => { + return Object.assign(state, props); +} + export const store = configureStore(testData); const history = createBrowserHistory(); const proxiedBackground = new Proxy({}, { diff --git a/.storybook/test-data.js b/.storybook/test-data.js index eede73242..766196d2f 100644 --- a/.storybook/test-data.js +++ b/.storybook/test-data.js @@ -1,795 +1,1320 @@ import { TRANSACTION_STATUSES } from '../shared/constants/transaction'; const state = { - "invalidCustomNetwork": { - "state": "CLOSED", - "networkName": "" + invalidCustomNetwork: { + state: 'CLOSED', + networkName: '', }, - "unconnectedAccount": { - "state": "CLOSED" + unconnectedAccount: { + state: 'CLOSED', }, - "activeTab": {}, - "metamask": { - "networkDetails": { - "EIPS": { - "1559": true + activeTab: { + id: 113, + title: 'E2E Test Dapp', + origin: 'https://metamask.github.io', + protocol: 'https:', + url: 'https://metamask.github.io/test-dapp/', + }, + metamask: { + networkDetails: { + EIPS: { + 1559: true, }, }, - "isInitialized": true, - "isUnlocked": true, - "isAccountMenuOpen": false, - "rpcUrl": "https://rawtestrpc.metamask.io/", - "identities": { - "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4": { - "name": "This is a Really Long Account Name", - "address": "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4" + isInitialized: true, + isUnlocked: true, + isAccountMenuOpen: false, + rpcUrl: 'https://rawtestrpc.metamask.io/', + identities: { + '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4': { + name: 'This is a Really Long Account Name', + address: '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4', }, - "0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e": { - "name": "Account 2", - "address": "0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e" + '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e': { + name: 'Account 2', + address: '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e', + }, + '0x9d0ba4ddac06032527b140912ec808ab9451b788': { + name: 'Account 3', + address: '0x9d0ba4ddac06032527b140912ec808ab9451b788', }, - "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" + unapprovedTxs: { + 3111025347726181: { + id: 3111025347726181, + time: 1620710815484, + status: 'unapproved', + metamaskNetworkId: '3', + msgParams: '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4', + chainId: '0x3', + loadingDefaults: false, + txParams: { + from: '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4', + to: '0xaD6D458402F60fD3Bd25163575031ACDce07538D', + value: '0x0', + data: + '0xa9059cbb000000000000000000000000b19ac54efa18cc3a14a5b821bfec73d284bf0c5e0000000000000000000000000000000000000000000000003782dace9d900000', + gas: '0xcb28', + gasPrice: '0x77359400', }, - "type": "standard", - "origin": "metamask", - "transactionCategory": "transfer", - "history": [ + 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" + 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" + type: 'standard', + origin: 'metamask', + transactionCategory: 'transfer', }, [ { - "op": "replace", - "path": "/loadingDefaults", - "value": false, - "note": "Added new unapproved transaction.", - "timestamp": 1620710815497 - } - ] - ] - } + op: 'replace', + path: '/loadingDefaults', + value: false, + note: 'Added new unapproved transaction.', + timestamp: 1620710815497, + }, + ], + ], + }, }, - "frequentRpcList": [], - "addressBook": { - "undefined": { - "0": { - "address": "0x39a4e4Af7cCB654dB9500F258c64781c8FbD39F0", - "name": "", - "isEns": false - } - } + frequentRpcList: [], + addressBook: { + undefined: { + 0: { + address: '0x39a4e4Af7cCB654dB9500F258c64781c8FbD39F0', + name: '', + isEns: false, + }, + }, }, - "contractExchangeRates": { - "0xad6d458402f60fd3bd25163575031acdce07538d": 0 + recipient: { + address: '0x39a4e4Af7cCB654dB9500F258c64781c8FbD39F0', + nickname: 'John Doe', + error: '', + warning: '', }, - "tokens": [ + addresses: [ { - "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" + address: '0x39a4e4Af7cCB654dB9500F258c64781c8FbD39F0', + name: 'DAI', + isEns: false, }, - "0x095ea7b3": { - "name": "Approve Tokens" - } - }, - "participateInMetaMetrics": true, - "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": { - "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4": { - "address": "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4", - "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": { - "7786962153682822": { - "id": 7786962153682822, - "msgParams": "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4", - "time": 1622687544054, - "status": "unapproved", - "type": "eth_getEncryptionPublicKey", - "origin": "https://metamask.github.io" - } - }, - "unapprovedEncryptionPublicKeyMsgCount": 0, - "unapprovedTypedMessages": {}, - "unapprovedTypedMessagesCount": 0, - "keyringTypes": [ - "Simple Key Pair", - "HD Key Tree", - "Trezor Hardware", - "Ledger Hardware" - ], - "keyrings": [ { - "type": "HD Key Tree", - "accounts": [ - "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4", - "0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e", - "0x9d0ba4ddac06032527b140912ec808ab9451b788" - ] - } + address: '1x39a4e4Af7cCB654dB9500F258c64781c8FbD39F0', + name: 'ETH', + isEns: true, + }, ], - "frequentRpcListDetail": [ + contractExchangeRates: { + '0xaD6D458402F60fD3Bd25163575031ACDce07538D': 0, + }, + tokens: [ { - "rpcUrl": "http://localhost:8545", - "chainId": "0x539", - "ticker": "ETH", - "nickname": "Localhost 8545", - "rpcPrefs": {} - } + address: '0xaD6D458402F60fD3Bd25163575031ACDce07538D', + symbol: 'DAI', + decimals: 18, + }, ], - "accountTokens": { - "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4": { - "0x1": [ + 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, + 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: { + '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4': { + address: '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4', + balance: '0x176e5b6f173ebe66', + }, + '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e': { + address: '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e', + balance: '0x2d3142f5000', + }, + '0x9d0ba4ddac06032527b140912ec808ab9451b788': { + address: '0x9d0ba4ddac06032527b140912ec808ab9451b788', + balance: '0x15f6f0b9d4f8d000', + }, + }, + currentBlockGasLimit: '0x793af4', + currentNetworkTxList: [ + { + chainId: '0x38', + dappSuggestedGasFees: null, + firstRetryBlockNumber: '0x9c2686', + hash: + '0xf45e7a751adfc0fbadccc972816baf33eb34543e52ace51f0f8d0d7f357afdc6', + history: [ { - "address": "0x6b175474e89094c44da98b954eedeac495271d0f", - "symbol": "DAI", - "decimals": 18 + chainId: '0x38', + dappSuggestedGasFees: null, + id: 2360388496987298, + loadingDefaults: true, + metamaskNetworkId: '56', + origin: 'metamask', + status: 'unapproved', + time: 1629582710520, + txParams: { + data: + '0xa9059cbb0000000000000000000000004ef2d5a1d056e7c9e8bcdbf2bd9ac0df749a1c2900000000000000000000000000000000000000000000000029a2241af62c0000', + from: '0x17f62b1b2407c41c43e14da0699d6b4b0a521548', + gas: '0x2eb27', + gasPrice: '0x12a05f200', + to: '0x2e8c05582176fa93b4590382e8290c73deb82176', + type: '0x0', + value: '0x0', + }, + type: 'transfer', + }, + [ + { + note: 'Added new unapproved transaction.', + op: 'replace', + path: '/loadingDefaults', + timestamp: 1629582710530, + value: false, + }, + ], + [ + { + note: 'txStateManager: setting status to approved', + op: 'replace', + path: '/status', + timestamp: 1629582711218, + value: 'approved', + }, + ], + [ + { + note: 'transactions#approveTransaction', + op: 'add', + path: '/txParams/nonce', + timestamp: 1629582711220, + value: '0x15b', + }, + { + op: 'add', + path: '/nonceDetails', + value: { + local: { + details: { + highest: 347, + startPoint: 347, + }, + name: 'local', + nonce: 347, + }, + network: { + details: { + baseCount: 347, + blockNumber: '0x9c2682', + }, + name: 'network', + nonce: 347, + }, + params: { + highestLocallyConfirmed: 327, + highestSuggested: 347, + nextNetworkNonce: 347, + }, + }, + }, + ], + [ + { + note: 'transactions#signTransaction: add r, s, v values', + op: 'add', + path: '/r', + timestamp: 1629582711236, + value: + '0x90a4dfb0646eef9815454d0ab543b5844acb8772101084565155c93ecce8ed69', + }, + { + op: 'add', + path: '/s', + value: + '0x7fd317c727025490f282c7990b8518a7dab7521b1ada1cb639f887966bc078df', + }, + { + op: 'add', + path: '/v', + value: '0x93', + }, + ], + [ + { + note: 'txStateManager: setting status to signed', + op: 'replace', + path: '/status', + timestamp: 1629582711236, + value: 'signed', + }, + ], + [ + { + note: 'transactions#publishTransaction', + op: 'add', + path: '/rawTx', + timestamp: 1629582711237, + value: + '0xf8ad82015b85012a05f2008302eb27942e8c05582176fa93b4590382e8290c73deb8217680b844a9059cbb0000000000000000000000004ef2d5a1d056e7c9e8bcdbf2bd9ac0df749a1c2900000000000000000000000000000000000000000000000029a2241af62c00008193a090a4dfb0646eef9815454d0ab543b5844acb8772101084565155c93ecce8ed69a07fd317c727025490f282c7990b8518a7dab7521b1ada1cb639f887966bc078df', + }, + ], + [ + { + note: 'transactions#setTxHash', + op: 'add', + path: '/hash', + timestamp: 1629582711336, + value: + '0xf45e7a751adfc0fbadccc972816baf33eb34543e52ace51f0f8d0d7f357afdc6', + }, + ], + [ + { + note: 'txStateManager - add submitted time stamp', + op: 'add', + path: '/submittedTime', + timestamp: 1629582711337, + value: 1629582711337, + }, + ], + [ + { + note: 'txStateManager: setting status to submitted', + op: 'replace', + path: '/status', + timestamp: 1629582711338, + value: 'submitted', + }, + ], + [ + { + note: 'transactions/pending-tx-tracker#event: tx:block-update', + op: 'add', + path: '/firstRetryBlockNumber', + timestamp: 1629582711878, + value: '0x9c2686', + }, + ], + [ + { + note: 'txStateManager: setting status to confirmed', + op: 'replace', + path: '/status', + timestamp: 1629582721178, + value: 'confirmed', + }, + { + op: 'add', + path: '/txReceipt', + value: { + blockHash: + '0x30bf5dfa12e460a5d121267c00ba3047a14ba286e0c4fe75fa979010f527cba0', + blockNumber: '9c2688', + contractAddress: null, + cumulativeGasUsed: '19a4942', + from: '0x17f62b1b2407c41c43e14da0699d6b4b0a521548', + gasUsed: '1f21a', + logs: [ + { + address: '0x2e8c05582176fa93b4590382e8290c73deb82176', + blockHash: + '0x30bf5dfa12e460a5d121267c00ba3047a14ba286e0c4fe75fa979010f527cba0', + blockNumber: '9c2688', + data: + '0x00000000000000000000000000000000000000000000000028426c213d688000', + logIndex: '245', + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x00000000000000000000000017f62b1b2407c41c43e14da0699d6b4b0a521548', + '0x0000000000000000000000004ef2d5a1d056e7c9e8bcdbf2bd9ac0df749a1c29', + ], + transactionHash: + '0xf45e7a751adfc0fbadccc972816baf33eb34543e52ace51f0f8d0d7f357afdc6', + transactionIndex: 'ae', + }, + { + address: '0x2e8c05582176fa93b4590382e8290c73deb82176', + blockHash: + '0x30bf5dfa12e460a5d121267c00ba3047a14ba286e0c4fe75fa979010f527cba0', + blockNumber: '9c2688', + data: + '0x000000000000000000000000000000000000000000000000006a94d74f430000', + logIndex: '246', + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x00000000000000000000000017f62b1b2407c41c43e14da0699d6b4b0a521548', + '0x000000000000000000000000c825413863f677a2012bb8db3a5e4a18bbf29e56', + ], + transactionHash: + '0xf45e7a751adfc0fbadccc972816baf33eb34543e52ace51f0f8d0d7f357afdc6', + transactionIndex: 'ae', + }, + { + address: '0x2e8c05582176fa93b4590382e8290c73deb82176', + blockHash: + '0x30bf5dfa12e460a5d121267c00ba3047a14ba286e0c4fe75fa979010f527cba0', + blockNumber: '9c2688', + data: + '0x000000000000000000000000000000000000000000000000001ff973cafa8000', + logIndex: '247', + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x00000000000000000000000017f62b1b2407c41c43e14da0699d6b4b0a521548', + '0x0000000000000000000000004ef2d5a1d056e7c9e8bcdbf2bd9ac0df749a1c29', + ], + transactionHash: + '0xf45e7a751adfc0fbadccc972816baf33eb34543e52ace51f0f8d0d7f357afdc6', + transactionIndex: 'ae', + }, + ], + logsBloom: + '0x20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000100000000000020000000000000000000000000000000008000000000080000000000000000000000000000000000000000040000000000000000000000040000000000200000010000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000200000000000000000000800000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000', + status: '0x1', + to: '0x2e8c05582176fa93b4590382e8290c73deb82176', + transactionHash: + '0xf45e7a751adfc0fbadccc972816baf33eb34543e52ace51f0f8d0d7f357afdc6', + transactionIndex: 'ae', + type: '0x0', + }, + }, + ], + [ + { + note: 'transactions#confirmTransaction - add txReceipt', + op: 'replace', + path: '/txReceipt/transactionIndex', + timestamp: 1629582721183, + value: 'ae', + }, + { + op: 'replace', + path: '/txReceipt/logs/2/logIndex', + value: '247', + }, + { + op: 'replace', + path: '/txReceipt/logs/2/transactionIndex', + value: 'ae', + }, + { + op: 'replace', + path: '/txReceipt/logs/2/blockNumber', + value: '9c2688', + }, + { + op: 'replace', + path: '/txReceipt/logs/1/logIndex', + value: '246', + }, + { + op: 'replace', + path: '/txReceipt/logs/1/transactionIndex', + value: 'ae', + }, + { + op: 'replace', + path: '/txReceipt/logs/1/blockNumber', + value: '9c2688', + }, + { + op: 'replace', + path: '/txReceipt/logs/0/logIndex', + value: '245', + }, + { + op: 'replace', + path: '/txReceipt/logs/0/transactionIndex', + value: 'ae', + }, + { + op: 'replace', + path: '/txReceipt/logs/0/blockNumber', + value: '9c2688', + }, + { + op: 'replace', + path: '/txReceipt/cumulativeGasUsed', + value: '19a4942', + }, + { + op: 'replace', + path: '/txReceipt/blockNumber', + value: '9c2688', + }, + ], + ], + id: 7900715443136469, + loadingDefaults: false, + metamaskNetworkId: '56', + nonceDetails: { + local: { + details: { + highest: 347, + startPoint: 347, + }, + name: 'local', + nonce: 347, + }, + network: { + details: { + baseCount: 347, + blockNumber: '0x9c2682', + }, + name: 'network', + nonce: 347, + }, + params: { + highestLocallyConfirmed: 327, + highestSuggested: 347, + nextNetworkNonce: 347, + }, + }, + origin: 'metamask', + r: '0x90a4dfb0646eef9815454d0ab543b5844acb8772101084565155c93ecce8ed69', + rawTx: + '0xf8ad82015b85012a05f2008302eb27942e8c05582176fa93b4590382e8290c73deb8217680b844a9059cbb0000000000000000000000004ef2d5a1d056e7c9e8bcdbf2bd9ac0df749a1c2900000000000000000000000000000000000000000000000029a2241af62c00008193a090a4dfb0646eef9815454d0ab543b5844acb8772101084565155c93ecce8ed69a07fd317c727025490f282c7990b8518a7dab7521b1ada1cb639f887966bc078df', + s: '0x7fd317c727025490f282c7990b8518a7dab7521b1ada1cb639f887966bc078df', + status: 'confirmed', + submittedTime: 1629582711337, + time: 1629582710520, + txParams: { + data: + '0xa9059cbb0000000000000000000000004ef2d5a1d056e7c9e8bcdbf2bd9ac0df749a1c2900000000000000000000000000000000000000000000000029a2241af62c0000', + from: '0x17f62b1b2407c41c43e14da0699d6b4b0a521548', + gas: '0x2eb27', + gasPrice: '0x12a05f200', + nonce: '0x15b', + to: '0x2e8c05582176fa93b4590382e8290c73deb82176', + type: '0x0', + value: '0x0', + }, + txReceipt: { + blockHash: + '0x30bf5dfa12e460a5d121267c00ba3047a14ba286e0c4fe75fa979010f527cba0', + blockNumber: { + length: 1, + negative: 0, + red: null, + words: [10233480, null], + }, + contractAddress: null, + cumulativeGasUsed: { + length: 1, + negative: 0, + red: null, + words: [26888514, null], + }, + from: '0x17f62b1b2407c41c43e14da0699d6b4b0a521548', + gasUsed: '1f21a', + logs: [ + { + address: '0x2e8c05582176fa93b4590382e8290c73deb82176', + blockHash: + '0x30bf5dfa12e460a5d121267c00ba3047a14ba286e0c4fe75fa979010f527cba0', + blockNumber: { + length: 1, + negative: 0, + red: null, + words: [10233480, null], + }, + data: + '0x00000000000000000000000000000000000000000000000028426c213d688000', + logIndex: { + length: 1, + negative: 0, + red: null, + words: [581, null], + }, + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x00000000000000000000000017f62b1b2407c41c43e14da0699d6b4b0a521548', + '0x0000000000000000000000004ef2d5a1d056e7c9e8bcdbf2bd9ac0df749a1c29', + ], + transactionHash: + '0xf45e7a751adfc0fbadccc972816baf33eb34543e52ace51f0f8d0d7f357afdc6', + transactionIndex: { + length: 1, + negative: 0, + red: null, + words: [174, null], + }, + }, + { + address: '0x2e8c05582176fa93b4590382e8290c73deb82176', + blockHash: + '0x30bf5dfa12e460a5d121267c00ba3047a14ba286e0c4fe75fa979010f527cba0', + blockNumber: { + length: 1, + negative: 0, + red: null, + words: [10233480, null], + }, + data: + '0x000000000000000000000000000000000000000000000000006a94d74f430000', + logIndex: { + length: 1, + negative: 0, + red: null, + words: [582, null], + }, + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x00000000000000000000000017f62b1b2407c41c43e14da0699d6b4b0a521548', + '0x000000000000000000000000c825413863f677a2012bb8db3a5e4a18bbf29e56', + ], + transactionHash: + '0xf45e7a751adfc0fbadccc972816baf33eb34543e52ace51f0f8d0d7f357afdc6', + transactionIndex: { + length: 1, + negative: 0, + red: null, + words: [174, null], + }, + }, + { + address: '0x2e8c05582176fa93b4590382e8290c73deb82176', + blockHash: + '0x30bf5dfa12e460a5d121267c00ba3047a14ba286e0c4fe75fa979010f527cba0', + blockNumber: { + length: 1, + negative: 0, + red: null, + words: [10233480, null], + }, + data: + '0x000000000000000000000000000000000000000000000000001ff973cafa8000', + logIndex: { + length: 1, + negative: 0, + red: null, + words: [583, null], + }, + removed: false, + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x00000000000000000000000017f62b1b2407c41c43e14da0699d6b4b0a521548', + '0x0000000000000000000000004ef2d5a1d056e7c9e8bcdbf2bd9ac0df749a1c29', + ], + transactionHash: + '0xf45e7a751adfc0fbadccc972816baf33eb34543e52ace51f0f8d0d7f357afdc6', + transactionIndex: { + length: 1, + negative: 0, + red: null, + words: [174, null], + }, + }, + ], + logsBloom: + '0x20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000100000000000020000000000000000000000000000000008000000000080000000000000000000000000000000000000000040000000000000000000000040000000000200000010000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000200000000000000000000800000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000', + status: '0x1', + to: '0x2e8c05582176fa93b4590382e8290c73deb82176', + transactionHash: + '0xf45e7a751adfc0fbadccc972816baf33eb34543e52ace51f0f8d0d7f357afdc6', + transactionIndex: { + length: 1, + negative: 0, + red: null, + words: [174, null], + }, + type: '0x0', + }, + type: 'transfer', + v: '0x93', + }, + ], + 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: { + 7786962153682822: { + id: 7786962153682822, + msgParams: '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4', + time: 1622687544054, + status: 'unapproved', + type: 'eth_getEncryptionPublicKey', + origin: 'https://metamask.github.io', + }, + }, + unapprovedEncryptionPublicKeyMsgCount: 0, + unapprovedTypedMessages: {}, + unapprovedTypedMessagesCount: 0, + keyringTypes: [ + 'Simple Key Pair', + 'HD Key Tree', + 'Trezor Hardware', + 'Ledger Hardware', + ], + keyrings: [ + { + 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 - } + address: '0x0d8775f648430679a709e98d2b0cb6250d2887ef', + symbol: 'BAT', + decimals: 18, + }, ], - "0x3": [ + '0x3': [ { - "address": "0xad6d458402f60fd3bd25163575031acdce07538d", - "symbol": "DAI", - "decimals": 18 - } - ] + address: '0xaD6D458402F60fD3Bd25163575031ACDce07538D', + symbol: 'DAI', + decimals: 18, + }, + ], }, - "0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e": {}, - "0x9d0ba4ddac06032527b140912ec808ab9451b788": {} + '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e': {}, + '0x9d0ba4ddac06032527b140912ec808ab9451b788': {}, }, - "accountHiddenTokens": { - "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4": { - "0x3": [] - } - }, - "assetImages": { - "0xad6d458402f60fd3bd25163575031acdce07538d": "./images/logo.png" - }, - "hiddenTokens": [], - "suggestedTokens": {}, - "useNonceField": false, - "usePhishDetect": true, - "lostIdentities": {}, - "forgottenPassword": false, - "ipfsGateway": "dweb.link", - "infuraBlocked": false, - "migratedPrivacyMode": false, - "selectedAddress": "0x9d0ba4ddac06032527b140912ec808ab9451b788", - "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" - }, - "hash": "0x2de9256a7c604586f7ecfd87ae9509851e217f588f9f85feed793c54ed2ce0aa", - "transactionCategory": "incoming" + accountHiddenTokens: { + '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4': { + '0x3': [], }, - "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 + assetImages: { + '0xaD6D458402F60fD3Bd25163575031ACDce07538D': './sai.svg', }, - "permissionsRequests": [], - "permissionsDescriptions": {}, - "domains": { - "https://app.uniswap.org": { - "permissions": [ + hiddenTokens: [], + suggestedTokens: {}, + useNonceField: false, + usePhishDetect: true, + lostIdentities: {}, + forgottenPassword: false, + ipfsGateway: 'dweb.link', + infuraBlocked: false, + migratedPrivacyMode: false, + selectedAddress: '0x9d0ba4ddac06032527b140912ec808ab9451b788', + 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', + }, + 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": [ + '@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: 'limitResponseLength', + value: 1, + name: 'primaryAccountOnly', }, { - "type": "filterResponse", - "value": [ - "0x64a845a5b02460acf8a3d84503b0d68d028b4bb4" - ], - "name": "exposedAccounts" - } - ] - } - ] - } - }, - "permissionsLog": [ - { - "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 - }, - "requestTime": 1602643170686, - "response": { - "id": 522690215, - "jsonrpc": "2.0", - "result": [] - }, - "responseTime": 1602643170688, - "success": true - }, - { - "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": [] - }, - "responseTime": 1602643172935, - "success": true - }, - { - "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 - }, - "requestTime": 1620710669962, - "response": { - "id": 4279100021, - "jsonrpc": "2.0", - "result": [] - }, - "responseTime": 1620710669963, - "success": true - }, - { - "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 - }, - { - "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 - } - ], - "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": { - "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": {} - }, - "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: 'filterResponse', + value: ['0x64a845a5b02460acf8a3d84503b0d68d028b4bb4'], + name: 'exposedAccounts', + }, + ], }, - "type": "standard", - "origin": "https://metamask.github.io", - "transactionCategory": "approve" + ], + }, + }, + permissionsLog: [ + { + 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, + }, + requestTime: 1602643170686, + response: { + id: 522690215, + jsonrpc: '2.0', + result: [], + }, + responseTime: 1602643170688, + success: true, + }, + { + 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: [], + }, + responseTime: 1602643172935, + success: true, + }, + { + 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, + }, + requestTime: 1620710669962, + response: { + id: 4279100021, + jsonrpc: '2.0', + result: [], + }, + responseTime: 1620710669963, + success: true, + }, + { + 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, + }, + { + 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, + }, + ], + permissionsHistory: { + 'https://metamask.github.io': { + 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', + }, + 'https://app.uniswap.org': { + name: 'Uniswap', + icon: './UNI.png', + lastUpdated: 1620723443380, + host: 'app.uniswap.org', + }, + }, + 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: { + 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: {}, + }, + history: { + mostRecentOverviewPage: '/', + }, + send: { + toDropdownOpen: false, + gasButtonGroupShown: true, + errors: {}, + asset: { + type: 'NATIVE', + balance: '0x0', + details: null, + }, + }, + confirmTransaction: { + txData: { + id: 3111025347726181, + time: 1620723786838, + status: 'unapproved', + metamaskNetworkId: '3', + chainId: '0x3', + loadingDefaults: false, + txParams: { + from: '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4', + to: '0xaD6D458402F60fD3Bd25163575031ACDce07538D', + value: '0x0', + data: + '0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170', + gas: '0xea60', + gasPrice: '0x4a817c800', + }, + type: 'transfer', + 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" - } + op: 'replace', + path: '/loadingDefaults', + value: false, + note: 'Added new unapproved transaction.', + timestamp: 1620723786844, + }, + ], ], - "functionFragment": { - "type": "function", - "name": "approve", - "constant": false, - "inputs": [ + }, + 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: '_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 - } + name: '_value', + type: 'uint256', + indexed: null, + components: null, + arrayLength: null, + arrayChildren: null, + baseType: 'uint256', + _isParamType: true, + }, ], - "outputs": [ + outputs: [ { - "name": "success", - "type": "bool", - "indexed": null, - "components": null, - "arrayLength": null, - "arrayChildren": null, - "baseType": "bool", - "_isParamType": true - } + name: 'success', + type: 'bool', + indexed: null, + components: null, + arrayLength: null, + arrayChildren: null, + baseType: 'bool', + _isParamType: true, + }, ], - "payable": false, - "stateMutability": "nonpayable", - "gas": null, - "_isFragment": true + payable: false, + stateMutability: 'nonpayable', + gas: null, + _isFragment: true, + }, + name: 'approve', + signature: 'approve(address,uint256)', + sighash: '0x095ea7b3', + value: { + type: 'BigNumber', + hex: '0x00', }, - "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": "" + 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 - } + 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" + gas: { + customData: { + price: null, + limit: '0xcb28', }, - "basicEstimates": { - "average": 2 + basicEstimates: { + average: 2, }, - "basicEstimateIsLoading": false - } -} + basicEstimateIsLoading: false, + }, +}; export default state; diff --git a/README.md b/README.md index b05ac8625..adbdc25b1 100644 --- a/README.md +++ b/README.md @@ -28,13 +28,15 @@ To learn how to contribute to the MetaMask project itself, visit our [Internal D Uncompressed builds can be found in `/dist`, compressed builds can be found in `/builds` once they're built. +See the [build system readme](./development/build/README.md) for build system usage information. + ## Contributing ### Development builds To start a development build (e.g. with logging and file watching) run `yarn start`. -To start the [React DevTools](https://github.com/facebook/react-devtools) and [Redux DevTools Extension](http://extension.remotedev.io) +To start the [React DevTools](https://github.com/facebook/react-devtools) and [Redux DevTools Extension](https://github.com/reduxjs/redux-devtools/tree/main/extension) alongside the app, use `yarn start:dev`. - React DevTools will open in a separate window; no browser extension is required - Redux DevTools will need to be installed as a browser extension. Open the Redux Remote Devtools to access Redux state logs. This can be done by either right clicking within the web browser to bring up the context menu, expanding the Redux DevTools panel and clicking Open Remote DevTools OR clicking the Redux DevTools extension icon and clicking Open Remote DevTools. diff --git a/app/_locales/am/messages.json b/app/_locales/am/messages.json index 2a19d1ced..50fa63a2e 100644 --- a/app/_locales/am/messages.json +++ b/app/_locales/am/messages.json @@ -38,9 +38,6 @@ "addNetwork": { "message": "አውታረ መረብ አክል" }, - "addRecipient": { - "message": "ተቀባይ አክል" - }, "addSuggestedTokens": { "message": "የተጠቆሙ ተለዋጭ ስሞችን አክል" }, @@ -53,9 +50,6 @@ "addToken": { "message": "ተለዋጭ ስም አክል" }, - "addTokens": { - "message": "ተለዋጭ ስሞችን አክል" - }, "advanced": { "message": "የላቀ" }, @@ -276,7 +270,7 @@ "message": "አስርዮሽ ቢያንስ 0 ቢበዛ ደግሞ 36 መሆን አለባቸው።" }, "defaultNetwork": { - "message": "የ Ether ግብይቶች ንቡር አውታረ መረብ Main Net ነው።" + "message": "የ Ether ግብይቶች ንቡር አውታረ መረብ Mainnet ነው።" }, "delete": { "message": "ሰርዝ" @@ -543,7 +537,7 @@ "letsGoSetUp": { "message": "አዎ፣ እናደራጅ!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "እነዚህን ተለዋጭ ስሞች ለማከል ይፈልጋሉ?" }, "links": { @@ -892,9 +886,6 @@ "sendTokens": { "message": "ተለዋጭ ስሞችን ላክ" }, - "sentEther": { - "message": "የተላከ ether" - }, "separateEachWord": { "message": "እያንዳንዱን ቃል በነጠላ ክፍት ቦታ ይለያዩ" }, diff --git a/app/_locales/ar/messages.json b/app/_locales/ar/messages.json index 4b7cd7bb7..5f37cb4e1 100644 --- a/app/_locales/ar/messages.json +++ b/app/_locales/ar/messages.json @@ -38,9 +38,6 @@ "addNetwork": { "message": "أضف شبكة" }, - "addRecipient": { - "message": "إضافة مستلم" - }, "addSuggestedTokens": { "message": "أضف العملات الرمزية المقترحة" }, @@ -53,9 +50,6 @@ "addToken": { "message": "إضافة عملة رمزية" }, - "addTokens": { - "message": "إضافة عملات رمزية" - }, "advanced": { "message": "إعدادات متقدمة" }, @@ -539,7 +533,7 @@ "letsGoSetUp": { "message": "نعم، دعنا نبدأ التثبيت!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "هل ترغب في إضافة هذه الرموز؟" }, "links": { @@ -888,9 +882,6 @@ "sendTokens": { "message": "إرسال عملات رمزية" }, - "sentEther": { - "message": "أرسل عملة إيثير" - }, "separateEachWord": { "message": "افصل كل كلمة بمسافة واحدة" }, diff --git a/app/_locales/bg/messages.json b/app/_locales/bg/messages.json index 803779b79..57196e3b3 100644 --- a/app/_locales/bg/messages.json +++ b/app/_locales/bg/messages.json @@ -38,9 +38,6 @@ "addNetwork": { "message": "Добавяне на мрежа" }, - "addRecipient": { - "message": "Добавете получател" - }, "addSuggestedTokens": { "message": "Добавете препоръчани жетони" }, @@ -53,9 +50,6 @@ "addToken": { "message": "Добавяне на жетон" }, - "addTokens": { - "message": "Добавяне на жетони" - }, "advanced": { "message": "Разширени" }, @@ -276,7 +270,7 @@ "message": "Десетичните знаци трябва да бъдат най-малко 0 и не повече от 36." }, "defaultNetwork": { - "message": "Мрежата по подразбиране за Ether транзакции е Main Net." + "message": "Мрежата по подразбиране за Ether транзакции е Mainnet." }, "delete": { "message": "Изтриване" @@ -539,7 +533,7 @@ "letsGoSetUp": { "message": "Да, нека да настроим нещата!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Искате ли да добавите тези жетони?" }, "links": { @@ -891,9 +885,6 @@ "sendTokens": { "message": "Изпращане на жетони" }, - "sentEther": { - "message": "изпратен етер" - }, "separateEachWord": { "message": "Отделете всяка дума с интервал" }, diff --git a/app/_locales/bn/messages.json b/app/_locales/bn/messages.json index e0d45ad97..19c45effd 100644 --- a/app/_locales/bn/messages.json +++ b/app/_locales/bn/messages.json @@ -38,9 +38,6 @@ "addNetwork": { "message": "নেটওয়ার্ক যোগ করুন" }, - "addRecipient": { - "message": "প্রাপক যোগ করুন" - }, "addSuggestedTokens": { "message": "প্রস্তাবিত টোকেনগুলি যোগ করুন" }, @@ -53,9 +50,6 @@ "addToken": { "message": "টোকেন যোগ করুন" }, - "addTokens": { - "message": "টোকেনগুলি যোগ করুন" - }, "advanced": { "message": "উন্নত" }, @@ -543,7 +537,7 @@ "letsGoSetUp": { "message": "হ্যাঁ, তাহলে সেট আপ করে নেওয়া যাক!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "আপনি কি এই টোকেনগুলি যোগ করতে চান?" }, "links": { @@ -895,9 +889,6 @@ "sendTokens": { "message": "টোকেনগুলি পাঠান" }, - "sentEther": { - "message": "পাঠানো ইথার " - }, "separateEachWord": { "message": "প্রতিটি শব্দকে একটি স্পেস দিয়ে আলাদা করুন" }, diff --git a/app/_locales/ca/messages.json b/app/_locales/ca/messages.json index ab0cbd0a8..fe3311ead 100644 --- a/app/_locales/ca/messages.json +++ b/app/_locales/ca/messages.json @@ -38,9 +38,6 @@ "addNetwork": { "message": "Afegir Xarxa" }, - "addRecipient": { - "message": "Afegeix un recipient" - }, "addSuggestedTokens": { "message": "Afegir Fitxes Suggerides" }, @@ -53,9 +50,6 @@ "addToken": { "message": "Afegir Fitxa" }, - "addTokens": { - "message": "Afegeix tokens" - }, "advanced": { "message": "Configuració avançada" }, @@ -273,7 +267,7 @@ "message": "Els decimals han de ser al menys 0, i no més de 36." }, "defaultNetwork": { - "message": "La xarxa per defecte per a les transaccions Ether és Main Net." + "message": "La xarxa per defecte per a les transaccions Ether és Mainnet." }, "delete": { "message": "Suprimeix" @@ -530,7 +524,7 @@ "letsGoSetUp": { "message": "Sí, posem-nos en marxa!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "T'agradaria afegir aquestes fitxes?" }, "links": { @@ -873,9 +867,6 @@ "sendTokens": { "message": "Enviar Fitxes" }, - "sentEther": { - "message": "envia ether" - }, "separateEachWord": { "message": "Separa cada paraula amb un sol espai" }, diff --git a/app/_locales/cs/messages.json b/app/_locales/cs/messages.json index 3e158ca01..5ff4fa58b 100644 --- a/app/_locales/cs/messages.json +++ b/app/_locales/cs/messages.json @@ -11,9 +11,6 @@ "addToken": { "message": "Přidat token" }, - "addTokens": { - "message": "Přidat tokeny" - }, "amount": { "message": "Částka" }, @@ -104,7 +101,7 @@ "message": "Desetinných míst musí být od 0 do 36." }, "defaultNetwork": { - "message": "Výchozí síť pro Etherové transakce je Main Net." + "message": "Výchozí síť pro Etherové transakce je Mainnet." }, "depositEther": { "message": "Vložit Ether" @@ -214,7 +211,7 @@ "learnMore": { "message": "Zjistěte více." }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Chcete přidat tyto tokeny?" }, "links": { diff --git a/app/_locales/da/messages.json b/app/_locales/da/messages.json index 8a5143637..c4c147bda 100644 --- a/app/_locales/da/messages.json +++ b/app/_locales/da/messages.json @@ -38,9 +38,6 @@ "addNetwork": { "message": "Tilføj netværk" }, - "addRecipient": { - "message": "Tilføj modtager" - }, "addSuggestedTokens": { "message": "Tilføj foreslåede tokens" }, @@ -53,9 +50,6 @@ "addToken": { "message": "Tilføj Polet" }, - "addTokens": { - "message": "Tilføj tokens" - }, "advanced": { "message": "Avanceret" }, @@ -276,7 +270,7 @@ "message": "Decimaler skal være mindst 0 og højst 36." }, "defaultNetwork": { - "message": "Standardnetværket for Ether-transaktioner er Main Net." + "message": "Standardnetværket for Ether-transaktioner er Mainnet." }, "delete": { "message": "Slet" @@ -536,7 +530,7 @@ "letsGoSetUp": { "message": "Ja, lad os komme i gang!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Ønsker du at tilføje disse tokens?" }, "loadMore": { @@ -873,9 +867,6 @@ "sendTokens": { "message": "Send tokens" }, - "sentEther": { - "message": "sendte ether" - }, "separateEachWord": { "message": "Separer hvert ord med et enkelt mellemrum" }, diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 116d81b4b..d6e52d78b 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -35,9 +35,6 @@ "addNetwork": { "message": "Netzwerk hinzufügen" }, - "addRecipient": { - "message": "Empfänger hinzufügen" - }, "addSuggestedTokens": { "message": "Vorgeschlagene Token hinzufügen" }, @@ -50,9 +47,6 @@ "addToken": { "message": "Token hinzufügen" }, - "addTokens": { - "message": "Token hinzufügen" - }, "advanced": { "message": "Erweitert" }, @@ -195,7 +189,7 @@ "message": " Verbinde zum Kovan Testnetzwerk" }, "connectingToMainnet": { - "message": "Verbinde zum Ethereum Main Net" + "message": "Verbinde zum Ethereum Mainnet" }, "connectingToRinkeby": { "message": " Verbinde zum Rinkeby Testnetzwerk" @@ -267,7 +261,7 @@ "message": "Die Dezimalangabe muss mindestens 0 und nicht höher als 36 sein." }, "defaultNetwork": { - "message": "Das Standardnetzwerk für Ether Transaktionen ist das Main Net." + "message": "Das Standardnetzwerk für Ether Transaktionen ist das Mainnet." }, "delete": { "message": "Löschen" @@ -531,7 +525,7 @@ "letsGoSetUp": { "message": "Ja, legen wir los!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Möchtest du diese Token hinzufügen?" }, "loadMore": { @@ -547,7 +541,7 @@ "message": "Ausloggen" }, "mainnet": { - "message": "Ethereum Main Net" + "message": "Ethereum Mainnet" }, "memo": { "message": " Memo" @@ -864,9 +858,6 @@ "sendTokens": { "message": "Token senden" }, - "sentEther": { - "message": "Ether senden" - }, "separateEachWord": { "message": "Trennen Sie die Wörter mit einem einzelnen Leerzeichen voneinander" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 50edf7795..e7239b9ae 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -38,9 +38,6 @@ "addNetwork": { "message": "Προσθήκη Δικτύου" }, - "addRecipient": { - "message": "Προσθήκη Παραλήπτη" - }, "addSuggestedTokens": { "message": "Προσθέστε τα Προτεινόμενα Tokens" }, @@ -53,9 +50,6 @@ "addToken": { "message": "Προσθήκη Token" }, - "addTokens": { - "message": "Προσθήκη Token" - }, "advanced": { "message": "Σύνθετες" }, @@ -273,7 +267,7 @@ "message": "Τα δεκαδικά πρέπει να είναι τουλάχιστον 0 και όχι πάνω από 36." }, "defaultNetwork": { - "message": "Το προεπιλεγμένο δίκτυο για συναλλαγές Ether είναι το Main Net." + "message": "Το προεπιλεγμένο δίκτυο για συναλλαγές Ether είναι το Mainnet." }, "delete": { "message": "Διαγραφή" @@ -540,7 +534,7 @@ "letsGoSetUp": { "message": "Ναι, ας το εγκαταστήσουμε!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Θέλετε να προσθέσετε αυτά τα token;" }, "links": { @@ -892,9 +886,6 @@ "sendTokens": { "message": "Στείλτε Tokens" }, - "sentEther": { - "message": "απεσταλμένα ether" - }, "separateEachWord": { "message": "Διαχωρίστε κάθε λέξη με ένα μόνο κενό" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 3f4fe103d..69f5cd019 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -52,6 +52,9 @@ "addContact": { "message": "Add contact" }, + "addCustomToken": { + "message": "Add Custom Token" + }, "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" @@ -79,9 +82,6 @@ "addNetwork": { "message": "Add Network" }, - "addRecipient": { - "message": "Add Recipient" - }, "addSuggestedTokens": { "message": "Add Suggested Tokens" }, @@ -94,9 +94,6 @@ "addToken": { "message": "Add Token" }, - "addTokens": { - "message": "Add Tokens" - }, "addressBookIcon": { "message": "Address book icon" }, @@ -243,6 +240,25 @@ "basic": { "message": "Basic" }, + "betaMetamaskDescription": { + "message": "Trusted by millions, MetaMask is a secure wallet making the world of web3 accessible to all." + }, + "betaMetamaskDescriptionExplanation": { + "message": "Use this version to test upcoming features before they’re released. Your use and feedback helps us build the best version of MetaMask possible. Your use of MetaMask Beta is subject to our standard $1 as well as our $2. As a Beta, there may be an increased risk of bugs. By proceeding, you accept and acknowledge these risks, as well as those risks found in our Terms and Beta Terms.", + "description": "$1 represents localization item betaMetamaskDescriptionExplanationTermsLinkText. $2 represents localization item betaMetamaskDescriptionExplanationBetaTermsLinkText" + }, + "betaMetamaskDescriptionExplanationBetaTermsLinkText": { + "message": "Supplemental Beta Terms" + }, + "betaMetamaskDescriptionExplanationTermsLinkText": { + "message": "Terms" + }, + "betaMetamaskVersion": { + "message": "MetaMask Beta Version" + }, + "betaWelcome": { + "message": "Welcome to MetaMask Beta" + }, "blockExplorerUrl": { "message": "Block Explorer URL" }, @@ -289,6 +305,9 @@ "cancel": { "message": "Cancel" }, + "cancelEdit": { + "message": "Cancel Edit" + }, "cancelPopoverTitle": { "message": "Cancel transaction" }, @@ -323,10 +342,10 @@ "message": "Confirm password" }, "confirmSecretBackupPhrase": { - "message": "Confirm your Secret Backup Phrase" + "message": "Confirm your Secret Recovery Phrase" }, "confirmSeedPhrase": { - "message": "Confirm Seed Phrase" + "message": "Confirm Secret Recovery Phrase" }, "confirmed": { "message": "Confirmed" @@ -501,6 +520,9 @@ "currentLanguage": { "message": "Current Language" }, + "currentlyUnavailable": { + "message": "Unavailable on this network" + }, "customGas": { "message": "Customize Gas" }, @@ -549,7 +571,7 @@ "message": "Decrypt request" }, "defaultNetwork": { - "message": "The default network for Ether transactions is Main Net." + "message": "The default network for Ether transactions is Mainnet." }, "delete": { "message": "Delete" @@ -594,10 +616,10 @@ "message": "Dismiss" }, "dismissReminderDescriptionField": { - "message": "Turn this on to dismiss the recovery phrase backup reminder message. We highly recommend that you back up your Secret Recovery Phrase to avoid loss of funds" + "message": "Turn this on to dismiss the Secret Recovery Phrase backup reminder message. We highly recommend that you back up your Secret Recovery Phrase to avoid loss of funds" }, "dismissReminderField": { - "message": "Dismiss recovery phrase backup reminder" + "message": "Dismiss Secret Recovery Phrase backup reminder" }, "domain": { "message": "Domain" @@ -612,7 +634,7 @@ "message": "Download Google Chrome" }, "downloadSecretBackup": { - "message": "Download this Secret Backup Phrase and keep it stored safely on an external encrypted hard drive or storage medium." + "message": "Download this Secret Recovery Phrase and keep it stored safely on an external encrypted hard drive or storage medium." }, "downloadStateLogs": { "message": "Download State Logs" @@ -630,16 +652,16 @@ "message": "How should I choose?" }, "editGasEducationHighExplanation": { - "message": "This is best for swaps or other time sensitive transactions. If a swap takes too long to process it will often fail and you may lose funds." + "message": "This is best for time sensitive transactions (like Swaps) as it increases the likelihood of a successful transaction. If a Swap takes too long to process it may fail and result in losing some of your gas fee." }, "editGasEducationLowExplanation": { - "message": "A lower gas fee should only be selected for transactions where processing time is less important. With a lower fee, it can be hard to predict when (or if) your transaction will be successful." + "message": "A lower gas fee should only be used when processing time is less important. Lower fees make it hard predict when (or if) your transaction will be successful." }, "editGasEducationMediumExplanation": { - "message": "A medium gas fee is good for sending, withdrawing or other non-time sensitive but important transactions." + "message": "A medium gas fee is good for sending, withdrawing or other non-time sensitive transactions. This setting will most often result in a successful transaction." }, "editGasEducationModalIntro": { - "message": "The right gas amount to select depends on the type of transaction and how important it is." + "message": "Selecting the right gas fee depends on the type of transaction and how important it is to you." }, "editGasEducationModalTitle": { "message": "How to choose?" @@ -675,7 +697,7 @@ "message": "Max priority fee is higher than necessary. You may pay more than needed." }, "editGasMaxPriorityFeeLow": { - "message": "Max priority fee extremely low for network conditions" + "message": "Max priority fee is low for current network conditions" }, "editGasMaxPriorityFeeTooltip": { "message": "Max priority fee (aka “miner tip”) goes directly to miners and incentivizes them to prioritize your transaction. You’ll most often pay your max setting" @@ -714,9 +736,8 @@ "editGasTooLowTooltip": { "message": "Your max fee or max priority fee may be low for current market conditions. We don't know when (or if) your transaction will be processed. " }, - "editGasTotalBannerSubtitle": { - "message": "Up to $1 ($2)", - "display": "$1 represents a fiat value" + "editGasTooLowWarningTooltip": { + "message": "This lowers your maximum fee but if network traffic increases your transaction may be delayed or fail." }, "editNonceField": { "message": "Edit Nonce" @@ -727,6 +748,9 @@ "editPermission": { "message": "Edit Permission" }, + "enableFromSettings": { + "message": " Enable it from Settings." + }, "encryptionPublicKeyNotice": { "message": "$1 would like your public encryption key. By consenting, this site will be able to compose encrypted messages to you.", "description": "$1 is the web3 site name" @@ -850,6 +874,12 @@ "expandView": { "message": "Expand view" }, + "experimental": { + "message": "Experimental" + }, + "experimentalSettingsDescription": { + "message": "Token detection & more" + }, "exportPrivateKey": { "message": "Export Private Key" }, @@ -869,6 +899,9 @@ "failureMessage": { "message": "Something went wrong, and we were unable to complete the action" }, + "fakeTokenWarning": { + "message": "Anyone can create a token, including creating fake versions of existing tokens. Learn more about $1" + }, "fast": { "message": "Fast" }, @@ -1048,6 +1081,9 @@ "importAccount": { "message": "Import Account" }, + "importAccountError": { + "message": "Error importing account." + }, "importAccountLinkText": { "message": "import using Secret Recovery Phrase" }, @@ -1055,7 +1091,7 @@ "message": " Imported accounts will not be associated with your originally created MetaMask account Secret Recovery Phrase. Learn more about imported accounts " }, "importAccountSeedPhrase": { - "message": "Import an account with Secret Recovery Phrase" + "message": "Import a wallet with Secret Recovery Phrase" }, "importAccountText": { "message": "or $1", @@ -1067,6 +1103,12 @@ "importTokenWarning": { "message": "Anyone can create a token with any name, including fake versions of existing tokens. Add and trade at your own risk!" }, + "importTokens": { + "message": "import tokens" + }, + "importTokensCamelCase": { + "message": "Import Tokens" + }, "importWallet": { "message": "Import wallet" }, @@ -1168,6 +1210,9 @@ "learnMore": { "message": "Learn more" }, + "learnScamRisk": { + "message": "scams and security risks." + }, "ledgerAccountRestriction": { "message": "You need to make use your last account before you can add a new one." }, @@ -1204,8 +1249,8 @@ "letsGoSetUp": { "message": "Yes, let’s get set up!" }, - "likeToAddTokens": { - "message": "Would you like to add these tokens?" + "likeToImportTokens": { + "message": "Would you like to import these tokens?" }, "links": { "message": "Links" @@ -1236,7 +1281,7 @@ }, "makeSureNoOneWatching": { "message": "Make sure no one is watching your screen", - "description": "Warning to users to be care while creating and saving their new seed phrase" + "description": "Warning to users to be care while creating and saving their new Secret Recovery Phrase" }, "max": { "message": "Max" @@ -1313,8 +1358,11 @@ "message": "verify the network details", "description": "Serves as link text for the 'mismatchedChain' key. This text will be embedded inside the translation for that key." }, + "missingToken": { + "message": "Don't see your token?" + }, "mobileSyncWarning": { - "message": "The 'Sync with extension' feature is temporarily disabled. If you want to use your extension wallet on MetaMask mobile, then on your mobile app: go back to the wallet setup options and select the 'Import with Secret Recovery Phrase' option. Use your extension wallet’s secret phrase to then import your wallet into mobile." + "message": "⚠️ Proceeding will display a secret QR code that allows access to your accounts. Do not share it with anyone. Support staff will never ask you for it." }, "mustSelectOne": { "message": "Must select at least 1 token." @@ -1332,9 +1380,15 @@ "message": "Need help? Contact $1", "description": "$1 represents `needHelpLinkText`, the text which goes in the help link" }, + "needHelpFeedback": { + "message": "Share your Feedback" + }, "needHelpLinkText": { "message": "MetaMask Support" }, + "needHelpSubmitTicket": { + "message": "Submit a Ticket" + }, "needImportFile": { "message": "You must select a file to import.", "description": "User is important an account and needs to add a file to continue" @@ -1550,9 +1604,6 @@ "onlyAddTrustedNetworks": { "message": "A malicious network provider can lie about the state of the blockchain and record your network activity. Only add custom networks you trust." }, - "onlyAvailableOnMainnet": { - "message": "Only available on mainnet" - }, "onlyConnectTrust": { "message": "Only connect with sites you trust." }, @@ -1653,7 +1704,7 @@ "message": "Queued" }, "readdToken": { - "message": "You can add this token back in the future by going to “Add token” in your accounts options menu." + "message": "You can add this token back in the future by going to “Import token” in your accounts options menu." }, "receive": { "message": "Receive" @@ -1691,6 +1742,9 @@ "recoveryPhraseReminderTitle": { "message": "Protect your funds" }, + "refreshList": { + "message": "Refresh list" + }, "reject": { "message": "Reject" }, @@ -1801,16 +1855,16 @@ "message": "Search Tokens" }, "secretBackupPhrase": { - "message": "Secret Backup Phrase" + "message": "Secret Recovery Phrase" }, "secretBackupPhraseDescription": { - "message": "Your secret backup phrase makes it easy to back up and restore your account." + "message": "Your Secret Recovery Phrase makes it easy to back up and restore your account." }, "secretBackupPhraseWarning": { - "message": "WARNING: Never disclose your backup phrase. Anyone with this phrase can take your Ether forever." + "message": "WARNING: Never disclose your Secret Recovery Phrase. Anyone with this phrase can take your Ether forever." }, "secretPhrase": { - "message": "Enter your secret phrase here to restore your vault." + "message": "Only the first account on this wallet will auto load. After completing this process, to add additional accounts, click the drop down menu, then select Create Account." }, "secureWallet": { "message": "Secure Wallet" @@ -1840,28 +1894,28 @@ "message": "Store in a bank vault." }, "seedPhraseIntroSidebarCopyOne": { - "message": "Your recovery phrase is the “master key” to your wallet and funds." + "message": "Your Secret Recovery Phrase is the “master key” to your wallet and funds." }, "seedPhraseIntroSidebarCopyThree": { - "message": "If someone asks for your recovery phrase, they are most likely trying to scam you." + "message": "If someone asks for your Secret Recovery Phrase, they are most likely trying to scam you." }, "seedPhraseIntroSidebarCopyTwo": { - "message": "Never, ever share your recovery phrase, even with MetaMask!" + "message": "Never, ever share your Secret Recovery Phrase, even with MetaMask!" }, "seedPhraseIntroSidebarTitleOne": { - "message": "What is a recovery phrase?" + "message": "What is a Secret Recovery Phrase?" }, "seedPhraseIntroSidebarTitleThree": { - "message": "Should I share my recovery phrase?" + "message": "Should I share my Secret Recovery Phrase?" }, "seedPhraseIntroSidebarTitleTwo": { - "message": "How do I save my recovery phrase?" + "message": "How do I save my Secret Recovery Phrase?" }, "seedPhraseIntroTitle": { "message": "Secure your wallet" }, "seedPhraseIntroTitleCopy": { - "message": "Before getting started, watch this short video to learn about your recovery phrase and how to keep your wallet safe." + "message": "Before getting started, watch this short video to learn about your Secret Recovery Phrase and how to keep your wallet safe." }, "seedPhrasePlaceholder": { "message": "Separate each word with a single space" @@ -1925,11 +1979,15 @@ "message": "Send $1", "description": "Symbol of the specified token" }, + "sendTo": { + "message": "Send to" + }, "sendTokens": { "message": "Send Tokens" }, - "sentEther": { - "message": "sent ether" + "sendingNativeAsset": { + "message": "Sending $1", + "description": "$1 represents the native currency symbol for the current network (e.g. ETH or BNB)" }, "separateEachWord": { "message": "Separate each word with a single space" @@ -1983,7 +2041,7 @@ "message": "Sign" }, "signNotice": { - "message": "Signing this message can have \ndangerous side effects. Only sign messages from \nsites you fully trust with your entire account.\n This dangerous method will be removed in a future version. " + "message": "Signing this message can be dangerous. This signature could potentially perform any operation on your account's behalf, including granting complete control of your account and all of its assets to the requesting site. Only sign this message if you know what you're doing or completely trust the requesting site." }, "signatureRequest": { "message": "Signature Request" @@ -2125,10 +2183,6 @@ "message": "No tokens available matching $1", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "Checking $1", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapConfirmWithHwWallet": { "message": "Confirm with your hardware wallet" }, @@ -2144,6 +2198,9 @@ "swapDecentralizedExchange": { "message": "Decentralized exchange" }, + "swapDirectContract": { + "message": "Direct contract" + }, "swapEditLimit": { "message": "Edit limit" }, @@ -2171,6 +2228,9 @@ "swapFailedErrorTitle": { "message": "Swap failed" }, + "swapFetchingQuotes": { + "message": "Fetching quotes" + }, "swapFetchingQuotesErrorDescription": { "message": "Hmmm... something went wrong. Try again, or if errors persist, contact customer support." }, @@ -2180,9 +2240,6 @@ "swapFetchingTokens": { "message": "Fetching tokens..." }, - "swapFinalizing": { - "message": "Finalizing..." - }, "swapFromTo": { "message": "The swap of $1 to $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" @@ -2443,6 +2500,9 @@ "symbolBetweenZeroTwelve": { "message": "Symbol must be 11 characters or fewer." }, + "syncFailed": { + "message": "Sync failed" + }, "syncInProgress": { "message": "Sync in progress" }, @@ -2513,6 +2573,9 @@ "tokenDecimalFetchFailed": { "message": "Token decimal required." }, + "tokenDetectionAnnouncement": { + "message": "New! Improved token detection is available on Ethereum Mainnet as an experimental feature. $1" + }, "tokenSymbol": { "message": "Token Symbol" }, @@ -2537,9 +2600,8 @@ "transactionCreated": { "message": "Transaction created with a value of $1 at $2." }, - "transactionDetailDappGasHeading": { - "message": "$1 suggested gas fee", - "description": "$1 represents a dapp origin" + "transactionDetailDappGasMoreInfo": { + "message": "Site suggested" }, "transactionDetailDappGasTooltip": { "message": "Edit to use MetaMask's recommended gas fee based on the latest block." @@ -2554,7 +2616,7 @@ "message": "Gas fees are set by the network and fluctuate based on network traffic and transaction complexity." }, "transactionDetailGasTooltipIntro": { - "message": "Gas fees are paid to crypto miners who process transactions on the Ethereum network. MetaMask does not profit from gas fees." + "message": "Gas fees are paid to crypto miners who process transactions on the $1 network. MetaMask does not profit from gas fees." }, "transactionDetailGasTotalSubtitle": { "message": "Amount + gas fee" @@ -2575,10 +2637,13 @@ "message": "Transaction Fee" }, "transactionHistoryBaseFee": { - "message": "Base fee (GWEI)" + "message": "Base Fee (GWEI)" + }, + "transactionHistoryMaxFeePerGas": { + "message": "Max Fee Per Gas" }, "transactionHistoryPriorityFee": { - "message": "Priority fee (GWEI)" + "message": "Priority Fee (GWEI)" }, "transactionHistoryTotalGasFee": { "message": "Total Gas Fee" @@ -2676,6 +2741,12 @@ "usePhishingDetectionDescription": { "message": "Display a warning for phishing domains targeting Ethereum users" }, + "useTokenDetection": { + "message": "Use Token Detection" + }, + "useTokenDetectionDescription": { + "message": "We use third-party APIs to detect and display new tokens sent to your wallet. Turn off if you don’t want MetaMask to pull data from those services." + }, "usedByClients": { "message": "Used by a variety of different clients" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 33fb6d0e5..b799d2d59 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -79,9 +79,6 @@ "addNetwork": { "message": "Agregar red" }, - "addRecipient": { - "message": "Agregar destinatario" - }, "addSuggestedTokens": { "message": "Agregar tokens sugeridos" }, @@ -94,9 +91,6 @@ "addToken": { "message": "Agregar token" }, - "addTokens": { - "message": "Agregar tokens" - }, "advanced": { "message": "Avanzado" }, @@ -1028,7 +1022,7 @@ "letsGoSetUp": { "message": "Sí, vamos a establecer la configuración." }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "¿Le gustaría agregar estos tokens?" }, "links": { @@ -1343,9 +1337,6 @@ "onlyAddTrustedNetworks": { "message": "Un proveedor de red malintencionado puede mentir sobre el estado de la cadena de bloques y registrar su actividad de red. Agregue solo redes personalizadas de confianza." }, - "onlyAvailableOnMainnet": { - "message": "Solo disponible en la red principal" - }, "onlyConnectTrust": { "message": "Conéctese solo con sitios de confianza." }, @@ -1694,9 +1685,6 @@ "sendTokens": { "message": "Enviar tokens" }, - "sentEther": { - "message": "enviar ether" - }, "separateEachWord": { "message": "Separar cada palabra con un solo espacio" }, @@ -1876,10 +1864,6 @@ "message": "No hay tokens disponibles que coincidan con $1", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "Comprobando $1", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapConfirmWithHwWallet": { "message": "Confirmar con la cartera de hardware" }, @@ -1931,9 +1915,6 @@ "swapFetchingTokens": { "message": "Capturando tokens…" }, - "swapFinalizing": { - "message": "Finalizando…" - }, "swapFromTo": { "message": "El canje de $1 por $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index 33fb6d0e5..b799d2d59 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -79,9 +79,6 @@ "addNetwork": { "message": "Agregar red" }, - "addRecipient": { - "message": "Agregar destinatario" - }, "addSuggestedTokens": { "message": "Agregar tokens sugeridos" }, @@ -94,9 +91,6 @@ "addToken": { "message": "Agregar token" }, - "addTokens": { - "message": "Agregar tokens" - }, "advanced": { "message": "Avanzado" }, @@ -1028,7 +1022,7 @@ "letsGoSetUp": { "message": "Sí, vamos a establecer la configuración." }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "¿Le gustaría agregar estos tokens?" }, "links": { @@ -1343,9 +1337,6 @@ "onlyAddTrustedNetworks": { "message": "Un proveedor de red malintencionado puede mentir sobre el estado de la cadena de bloques y registrar su actividad de red. Agregue solo redes personalizadas de confianza." }, - "onlyAvailableOnMainnet": { - "message": "Solo disponible en la red principal" - }, "onlyConnectTrust": { "message": "Conéctese solo con sitios de confianza." }, @@ -1694,9 +1685,6 @@ "sendTokens": { "message": "Enviar tokens" }, - "sentEther": { - "message": "enviar ether" - }, "separateEachWord": { "message": "Separar cada palabra con un solo espacio" }, @@ -1876,10 +1864,6 @@ "message": "No hay tokens disponibles que coincidan con $1", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "Comprobando $1", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapConfirmWithHwWallet": { "message": "Confirmar con la cartera de hardware" }, @@ -1931,9 +1915,6 @@ "swapFetchingTokens": { "message": "Capturando tokens…" }, - "swapFinalizing": { - "message": "Finalizando…" - }, "swapFromTo": { "message": "El canje de $1 por $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" diff --git a/app/_locales/et/messages.json b/app/_locales/et/messages.json index 58d69b71e..f44cca3d0 100644 --- a/app/_locales/et/messages.json +++ b/app/_locales/et/messages.json @@ -38,9 +38,6 @@ "addNetwork": { "message": "Lisage võrk" }, - "addRecipient": { - "message": "Lisa saaja" - }, "addSuggestedTokens": { "message": "Lisa soovitatud lube" }, @@ -53,9 +50,6 @@ "addToken": { "message": "Lisage luba" }, - "addTokens": { - "message": "Lisa lube" - }, "advanced": { "message": "Täpsemad" }, @@ -539,7 +533,7 @@ "letsGoSetUp": { "message": "Jah, hakkame pihta!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Kas soovite need load lisada?" }, "links": { @@ -885,9 +879,6 @@ "sendTokens": { "message": "Saada lube" }, - "sentEther": { - "message": "saadetud eeter" - }, "separateEachWord": { "message": "Eraldage iga sõna ühe tühikuga" }, diff --git a/app/_locales/fa/messages.json b/app/_locales/fa/messages.json index 3d95df38b..96b9ece48 100644 --- a/app/_locales/fa/messages.json +++ b/app/_locales/fa/messages.json @@ -38,9 +38,6 @@ "addNetwork": { "message": "اضافه شبکه" }, - "addRecipient": { - "message": "اضافه کردن دریافت کننده" - }, "addSuggestedTokens": { "message": "اضافه رمزیاب های پیشنهاد شده" }, @@ -53,9 +50,6 @@ "addToken": { "message": "یک رمز یاب اضافه کنید" }, - "addTokens": { - "message": "اضافه رمزیاب ها" - }, "advanced": { "message": "پیشرفته" }, @@ -276,7 +270,7 @@ "message": "اعشاریه ها باید حد اقل 0، و بیشتر از 36 نباشند." }, "defaultNetwork": { - "message": "شبکه خودکار برای معاملات Ether عبارت است از Main Net." + "message": "شبکه خودکار برای معاملات Ether عبارت است از Mainnet." }, "delete": { "message": "حذف" @@ -543,7 +537,7 @@ "letsGoSetUp": { "message": "بلی، درست شد!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "آیا میخواهید این رمزیاب ها را اضافه نمایید؟" }, "links": { @@ -895,9 +889,6 @@ "sendTokens": { "message": "رمزیاب ها را ارسال کنید" }, - "sentEther": { - "message": "ایتر ارسال شد" - }, "separateEachWord": { "message": "هر کلمه را با یک فاصله واحد جدا سازید" }, diff --git a/app/_locales/fi/messages.json b/app/_locales/fi/messages.json index 9df9c20d5..2baae1b63 100644 --- a/app/_locales/fi/messages.json +++ b/app/_locales/fi/messages.json @@ -38,9 +38,6 @@ "addNetwork": { "message": "Lisää verkko" }, - "addRecipient": { - "message": "Lisää vastaanottaja" - }, "addSuggestedTokens": { "message": "Lisää ehdotetut käyttötunnukset" }, @@ -53,9 +50,6 @@ "addToken": { "message": "Lisää tietue" }, - "addTokens": { - "message": "Lisää poletteja" - }, "advanced": { "message": "Lisäasetukset" }, @@ -276,7 +270,7 @@ "message": "Desimaalien on oltava vähintään 0 ja korkeintaan 36." }, "defaultNetwork": { - "message": "Oletusverkko Ether-tapahtumille on Main Net." + "message": "Oletusverkko Ether-tapahtumille on Mainnet." }, "delete": { "message": "Poista" @@ -543,7 +537,7 @@ "letsGoSetUp": { "message": "Kyllä, valmistaudutaan!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Haluaisitko lisätä nämä poletit?" }, "links": { @@ -892,9 +886,6 @@ "sendTokens": { "message": "Lähetä tietueita" }, - "sentEther": { - "message": "lähetä etheriä" - }, "separateEachWord": { "message": "Erottele sanat toisistaan yhdellä välilyönnillä" }, diff --git a/app/_locales/fil/messages.json b/app/_locales/fil/messages.json index 91c808bfc..a472c8bd0 100644 --- a/app/_locales/fil/messages.json +++ b/app/_locales/fil/messages.json @@ -35,9 +35,6 @@ "addNetwork": { "message": "Magdagdag ng Network" }, - "addRecipient": { - "message": "Magdagdag ng Recipient" - }, "addSuggestedTokens": { "message": "Magdagdag ng Mga Iminungkahing Token" }, @@ -50,9 +47,6 @@ "addToken": { "message": "Magdagdag ng Token" }, - "addTokens": { - "message": "Magdagdag ng Mga Token" - }, "advancedOptions": { "message": "Mga Advanced na Opsyon" }, @@ -252,7 +246,7 @@ "message": "Ang mga decimal ay hindi dapat bumaba sa 0, at hindi lumampas sa 36." }, "defaultNetwork": { - "message": "Ang default na network para sa mga transaksyon ng Ether ay Main Net." + "message": "Ang default na network para sa mga transaksyon ng Ether ay Mainnet." }, "delete": { "message": "I-delete" @@ -493,7 +487,7 @@ "letsGoSetUp": { "message": "Oo, i-set up natin ito!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Gusto mo bang idagdag ang mga token na ito?" }, "links": { @@ -807,9 +801,6 @@ "sendTokens": { "message": "Magpadala ng Mga Token" }, - "sentEther": { - "message": "nagpadala ng ether" - }, "separateEachWord": { "message": "Paghiwa-hiwalayin ang bawat salita gamit ang isang space" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 2f8e52fed..f43a8f73f 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -38,9 +38,6 @@ "addNetwork": { "message": "Ajouter un réseau" }, - "addRecipient": { - "message": "Ajouter destinataire" - }, "addSuggestedTokens": { "message": "Ajouter les jetons suggérés" }, @@ -53,9 +50,6 @@ "addToken": { "message": "Ajouter un jeton" }, - "addTokens": { - "message": "Ajouter des jetons" - }, "advanced": { "message": "Paramètres avancés" }, @@ -537,7 +531,7 @@ "letsGoSetUp": { "message": "Oui, passons à la configuration !" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Souhaitez-vous ajouter ces jetons ?" }, "links": { @@ -657,7 +651,7 @@ "message": "de" }, "off": { - "message": "Déconnecté" + "message": "Désactivé" }, "on": { "message": "Activé" @@ -877,9 +871,6 @@ "sendTokens": { "message": "Envoyer des jetons" }, - "sentEther": { - "message": "Ether envoyé" - }, "separateEachWord": { "message": "Separez chaque mot avec un espace simple" }, @@ -1113,7 +1104,7 @@ "message": "Voir contact" }, "viewOnCustomBlockExplorer": { - "message": "Afficher à $1" + "message": "Afficher sur $1" }, "viewOnEtherscan": { "message": "Voir sur Etherscan" diff --git a/app/_locales/he/messages.json b/app/_locales/he/messages.json index a0d2e9686..3ddc4e4fb 100644 --- a/app/_locales/he/messages.json +++ b/app/_locales/he/messages.json @@ -38,9 +38,6 @@ "addNetwork": { "message": "הוסף/י רשת" }, - "addRecipient": { - "message": "הוסף נמען" - }, "addSuggestedTokens": { "message": "הוסף/י אסימונים מוצעים" }, @@ -53,9 +50,6 @@ "addToken": { "message": "הוסף/י אסימון" }, - "addTokens": { - "message": "הוסף סמלים" - }, "advanced": { "message": "מתקדם" }, @@ -276,7 +270,7 @@ "message": "מספרים עשרוניים חייבים להיות לפחות 0 ולא מעל 36." }, "defaultNetwork": { - "message": "רשת ברירת המחדל לעסקאות Ether היא Main Net." + "message": "רשת ברירת המחדל לעסקאות Ether היא Mainnet." }, "delete": { "message": "מחיקה" @@ -543,7 +537,7 @@ "letsGoSetUp": { "message": "כן, בוא נתקין!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "האם ברצונך להוסיף טוקנים אלה?" }, "links": { @@ -889,9 +883,6 @@ "sendTokens": { "message": "שלח טוקנים" }, - "sentEther": { - "message": "את'ר שנשלח" - }, "separateEachWord": { "message": "יש להפריד כל מילה עם רווח יחיד" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 31736f438..de8052516 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -79,9 +79,6 @@ "addNetwork": { "message": "नेटवर्क जोड़ें" }, - "addRecipient": { - "message": "प्राप्तकर्ता को जोड़ें" - }, "addSuggestedTokens": { "message": "सुझाए गए टोकन जोड़ें" }, @@ -94,9 +91,6 @@ "addToken": { "message": "टोकन जोड़ें" }, - "addTokens": { - "message": "टोकन जोड़ें" - }, "advanced": { "message": "उन्नत" }, @@ -519,7 +513,7 @@ "message": "अनुरोध डिक्रिप्ट करें" }, "defaultNetwork": { - "message": "Ether के लेनदेन के लिए डिफ़ॉल्ट नेटवर्क Main Net है।" + "message": "Ether के लेनदेन के लिए डिफ़ॉल्ट नेटवर्क Mainnet है।" }, "delete": { "message": "हटाएँ" @@ -1028,7 +1022,7 @@ "letsGoSetUp": { "message": "हाँ, आइए सेट करते हैं!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "क्या आप इन टोकन को जोड़ना चाहते हैं?" }, "links": { @@ -1343,9 +1337,6 @@ "onlyAddTrustedNetworks": { "message": "एक दुर्भावनापूर्ण नेटवर्क प्रदाता ब्लॉकचेन की स्थिति के बारे में झूठ बोल सकता है और आपकी नेटवर्क गतिविधि को रिकॉर्ड कर सकता है। केवल उन कस्टम नेटवर्क को जोड़ें, जिन पर आप भरोसा करते हैं।" }, - "onlyAvailableOnMainnet": { - "message": "केवल मेननेट पर उपलब्ध है" - }, "onlyConnectTrust": { "message": "केवल उन साइटों से कनेक्ट करें, जिन पर आप भरोसा करते हैं।" }, @@ -1694,9 +1685,6 @@ "sendTokens": { "message": "टोकन भेजें" }, - "sentEther": { - "message": "Ether भेजा गया" - }, "separateEachWord": { "message": "प्रत्येक शब्द को एक रिक्ति से अलग करें" }, @@ -1876,10 +1864,6 @@ "message": "$1 के मिलान वाले कोई भी टोकन उपलब्ध नहीं हैं", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "$1 की जाँच की जा रही है", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapConfirmWithHwWallet": { "message": "अपने हार्डवेयर वॉलेट से पुष्टि करें" }, @@ -1931,9 +1915,6 @@ "swapFetchingTokens": { "message": "टोकन प्राप्त किए जा रहे हैं..." }, - "swapFinalizing": { - "message": "अंतिम रूप दिया जा रहा है..." - }, "swapFromTo": { "message": "$1 से $2 का स्वैप", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" diff --git a/app/_locales/hn/messages.json b/app/_locales/hn/messages.json index d8aeceb8e..73056bb06 100644 --- a/app/_locales/hn/messages.json +++ b/app/_locales/hn/messages.json @@ -11,9 +11,6 @@ "addToken": { "message": "टोकन जोड़ें" }, - "addTokens": { - "message": "टोकनों को जोड़ें" - }, "amount": { "message": "राशि" }, @@ -194,7 +191,7 @@ "kovan": { "message": "कोवान टेस्ट नेटवर्क" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "क्या आप इन टोकनों को जोड़ना चाहते हैं?" }, "loading": { diff --git a/app/_locales/hr/messages.json b/app/_locales/hr/messages.json index 44ca7e1db..656fd84f9 100644 --- a/app/_locales/hr/messages.json +++ b/app/_locales/hr/messages.json @@ -38,9 +38,6 @@ "addNetwork": { "message": "Dodaj mrežu" }, - "addRecipient": { - "message": "Dodaj primatelja" - }, "addSuggestedTokens": { "message": "Dodaj predložene tokene" }, @@ -53,9 +50,6 @@ "addToken": { "message": "Dodaj token" }, - "addTokens": { - "message": "Dodaj tokene" - }, "advanced": { "message": "Napredno" }, @@ -539,7 +533,7 @@ "letsGoSetUp": { "message": "Da, obavimo postavljanje!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Želite li dodati ove tokene?" }, "links": { @@ -888,9 +882,6 @@ "sendTokens": { "message": "Pošalji tokene" }, - "sentEther": { - "message": "pošalji ether" - }, "separateEachWord": { "message": "Odvojite pojedinačne riječi jednim razmakom" }, diff --git a/app/_locales/ht/messages.json b/app/_locales/ht/messages.json index 78cc96a2a..6cab69c61 100644 --- a/app/_locales/ht/messages.json +++ b/app/_locales/ht/messages.json @@ -29,9 +29,6 @@ "addToken": { "message": "Ajoute Token" }, - "addTokens": { - "message": "Ajoute Token" - }, "amount": { "message": "Kantite lajan" }, @@ -311,7 +308,7 @@ "ledgerAccountRestriction": { "message": "Ou bezwen sèvi ak dènye kont ou anvan ou ka ajoute yon nouvo." }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Èske ou ta renmen ajoute sa nan tokens?" }, "links": { @@ -555,9 +552,6 @@ "sendTokens": { "message": "Voye Tokens" }, - "sentEther": { - "message": "Voye ether" - }, "separateEachWord": { "message": "Separe chak mo ak yon sèl espas" }, diff --git a/app/_locales/hu/messages.json b/app/_locales/hu/messages.json index f7ea54d5b..7e8cec14b 100644 --- a/app/_locales/hu/messages.json +++ b/app/_locales/hu/messages.json @@ -38,9 +38,6 @@ "addNetwork": { "message": "Hálózat hozzáadása" }, - "addRecipient": { - "message": "Címzett hozzáadása" - }, "addSuggestedTokens": { "message": "Javasolt tokenek hozzáadása" }, @@ -53,9 +50,6 @@ "addToken": { "message": "Token hozzáadása" }, - "addTokens": { - "message": "Érmék hozzáadása" - }, "advanced": { "message": "Speciális" }, @@ -276,7 +270,7 @@ "message": "A tizedesjegyek száma 0 és 36 között legyen . " }, "defaultNetwork": { - "message": "Az Ether tranzakciók alapértelmezett hálózata a Main Net." + "message": "Az Ether tranzakciók alapértelmezett hálózata a Mainnet." }, "delete": { "message": "Törlés" @@ -539,7 +533,7 @@ "letsGoSetUp": { "message": "Igen, hozzuk létre!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Hozzá szeretné adni ezeket az érméket?" }, "links": { @@ -888,9 +882,6 @@ "sendTokens": { "message": "Token küldése" }, - "sentEther": { - "message": "elküldött ether" - }, "separateEachWord": { "message": "Minden egyes szavat szóközzel válasszon el" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 0c1b5a0c8..80f539ad7 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -79,9 +79,6 @@ "addNetwork": { "message": "Tambahkan Jaringan" }, - "addRecipient": { - "message": "Tambahkan Penerima" - }, "addSuggestedTokens": { "message": "Tambahkan Token yang Disarankan" }, @@ -94,9 +91,6 @@ "addToken": { "message": "Tambahkan Token" }, - "addTokens": { - "message": "Tambahkan Token" - }, "advanced": { "message": "Tingkat Lanjut" }, @@ -1028,7 +1022,7 @@ "letsGoSetUp": { "message": "Ya, mari siap-siap!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Apakah Anda ingin menambahkan token ini?" }, "links": { @@ -1343,9 +1337,6 @@ "onlyAddTrustedNetworks": { "message": "Penyedia jaringan jahat dapat berbohong tentang status blockchain dan merekam aktivitas jaringan Anda. Hanya tambahkan jaringan kustom yang Anda percayai." }, - "onlyAvailableOnMainnet": { - "message": "Hanya tersedia di mainnet" - }, "onlyConnectTrust": { "message": "Hanya hubungkan ke situs yang Anda percayai." }, @@ -1694,9 +1685,6 @@ "sendTokens": { "message": "Kirim Token" }, - "sentEther": { - "message": "ether terkirim" - }, "separateEachWord": { "message": "Pisahkan setiap kata dengan satu spasi" }, @@ -1876,10 +1864,6 @@ "message": "Tidak ada token yang cocok yang tersedia $1", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "Memeriksa $1", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapConfirmWithHwWallet": { "message": "Konfirmasikan dengan dompet perangkat keras Anda" }, @@ -1931,9 +1915,6 @@ "swapFetchingTokens": { "message": "Mengambil token..." }, - "swapFinalizing": { - "message": "Menyelesaikan..." - }, "swapFromTo": { "message": "Penukaran dari $1 ke $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index f3923d480..57122608d 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -52,9 +52,6 @@ "addNetwork": { "message": "Aggiungi Rete" }, - "addRecipient": { - "message": "Aggiungi destinatario" - }, "addSuggestedTokens": { "message": "Aggiungi Token Suggeriti" }, @@ -67,9 +64,6 @@ "addToken": { "message": "Aggiungi Token" }, - "addTokens": { - "message": "Aggiungi token" - }, "advanced": { "message": "Avanzate" }, @@ -878,7 +872,7 @@ "letsGoSetUp": { "message": "Si, iniziamo!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Vorresti aggiungere questi token?" }, "links": { @@ -1096,9 +1090,6 @@ "onlyAddTrustedNetworks": { "message": "Una rete malevola può mentire sullo stato della blockchain e registrare le tue azioni. Aggiungi solo reti fidate." }, - "onlyAvailableOnMainnet": { - "message": "Disponibile solo nella rete principale" - }, "onlyConnectTrust": { "message": "Connettiti solo con siti di cui ti fidi." }, @@ -1374,9 +1365,6 @@ "sendTokens": { "message": "Invia Tokens" }, - "sentEther": { - "message": "ether inviati" - }, "separateEachWord": { "message": "Separa ogni parola con un solo spazio" }, @@ -1528,10 +1516,6 @@ "message": "Non ci sono token disponibile con questo nome $1", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "Verificando $1", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapCustom": { "message": "personalizza" }, @@ -1570,9 +1554,6 @@ "swapFetchingTokens": { "message": "Recuperando i token..." }, - "swapFinalizing": { - "message": "Finalizzando..." - }, "swapLowSlippageError": { "message": "La transazione può fallire, il massimo slippage è troppo basso." }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index bc3b961d2..e6f295cc6 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -79,9 +79,6 @@ "addNetwork": { "message": "ネットワークの追加" }, - "addRecipient": { - "message": "受信者の追加" - }, "addSuggestedTokens": { "message": "推奨されたトークンの追加" }, @@ -94,9 +91,6 @@ "addToken": { "message": "トークンの追加" }, - "addTokens": { - "message": "トークンの追加" - }, "advanced": { "message": "詳細" }, @@ -1028,7 +1022,7 @@ "letsGoSetUp": { "message": "さあセットアップしましょう!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "これらのトークンを追加しますか?" }, "links": { @@ -1343,9 +1337,6 @@ "onlyAddTrustedNetworks": { "message": "悪意のあるネットワーク プロバイダは、ブロックチェーンの状態を偽り、お客様のネットワーク行動を記録することがあります。信頼するカスタム ネットワークのみを追加してください。" }, - "onlyAvailableOnMainnet": { - "message": "メインネットのみで使用可能" - }, "onlyConnectTrust": { "message": "信頼するサイトにのみ接続します。" }, @@ -1694,9 +1685,6 @@ "sendTokens": { "message": "トークンの送信" }, - "sentEther": { - "message": "Ether の送金" - }, "separateEachWord": { "message": "単語ごとにスペースを 1 つ置いて分離します" }, @@ -1876,10 +1864,6 @@ "message": "$1 と一致するトークンがありません", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "$1 をチェック中", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapConfirmWithHwWallet": { "message": "ハードウェア ウォレットで確認する" }, @@ -1931,9 +1915,6 @@ "swapFetchingTokens": { "message": "トークンを取り出し中..." }, - "swapFinalizing": { - "message": "終了中..." - }, "swapFromTo": { "message": "$1 から $2 のスワップ", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" diff --git a/app/_locales/kn/messages.json b/app/_locales/kn/messages.json index d5c178c3e..7aad7aed4 100644 --- a/app/_locales/kn/messages.json +++ b/app/_locales/kn/messages.json @@ -38,9 +38,6 @@ "addNetwork": { "message": "ನೆಟ್‌ವರ್ಕ್ ಸೇರಿಸಿ" }, - "addRecipient": { - "message": "ಸ್ವೀಕೃತಿದಾರರನ್ನು ಸೇರಿಸಿ" - }, "addSuggestedTokens": { "message": "ಸೂಚಿಸಲಾದ ಟೋಕನ್‌ಗಳನ್ನು ಸೇರಿಸಿ" }, @@ -53,9 +50,6 @@ "addToken": { "message": "ಟೋಕನ್ ಸೇರಿಸಿ" }, - "addTokens": { - "message": "ಟೋಕನ್‌ಗಳನ್ನು ಸೇರಿಸಿ" - }, "advanced": { "message": "ಸುಧಾರಿತ" }, @@ -543,7 +537,7 @@ "letsGoSetUp": { "message": "ಹೌದು, ಹೊಂದಿಸೋಣ!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "ನೀವು ಈ ಟೋಕನ್‌ಗಳನ್ನು ಸೇರಿಸಲು ಬಯಸುತ್ತೀರಾ?" }, "links": { @@ -895,9 +889,6 @@ "sendTokens": { "message": "ಟೋಕನ್‌ಗಳನ್ನು ಕಳುಹಿಸಿ" }, - "sentEther": { - "message": "ಕಳುಹಿಸಲಾದ ಎಥರ್" - }, "separateEachWord": { "message": "ಒಂದು ಸ್ಪೇಸ್ ಮೂಲಕ ಪ್ರತಿ ಪದವನ್ನು ಬೇರ್ಪಡಿಸಿ" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index d8e78ce2b..d62801f6a 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -79,9 +79,6 @@ "addNetwork": { "message": "네트워크 추가" }, - "addRecipient": { - "message": "수신인 추가" - }, "addSuggestedTokens": { "message": "추천 토큰 추가" }, @@ -94,9 +91,6 @@ "addToken": { "message": "토큰 추가" }, - "addTokens": { - "message": "토큰 추가" - }, "advanced": { "message": "고급" }, @@ -1028,7 +1022,7 @@ "letsGoSetUp": { "message": "설정을 시작하죠!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "이 토큰을 추가하시겠어요?" }, "links": { @@ -1343,9 +1337,6 @@ "onlyAddTrustedNetworks": { "message": "악성 네트워크 공급업체는 블록체인 상태를 거짓으로 보고하고 네트워크 활동을 기록할 수 있습니다. 신뢰하는 맞춤형 네트워크만 추가하세요." }, - "onlyAvailableOnMainnet": { - "message": "메인넷에서만 사용 가능" - }, "onlyConnectTrust": { "message": "신뢰하는 사이트만 연결하세요." }, @@ -1694,9 +1685,6 @@ "sendTokens": { "message": "토큰 보내기" }, - "sentEther": { - "message": "Ether 보냄" - }, "separateEachWord": { "message": "공백 한 칸으로 각 단어를 구분하세요." }, @@ -1876,10 +1864,6 @@ "message": "$1와(과) 일치하는 토큰이 없습니다.", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "$1 확인 중", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapConfirmWithHwWallet": { "message": "하드웨어 지갑으로 확인합니다." }, @@ -1931,9 +1915,6 @@ "swapFetchingTokens": { "message": "토큰 가져오는 중..." }, - "swapFinalizing": { - "message": "마무리 중..." - }, "swapFromTo": { "message": "$1을(를) $2(으)로 스왑", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" diff --git a/app/_locales/lt/messages.json b/app/_locales/lt/messages.json index 4daca63f5..f315161d8 100644 --- a/app/_locales/lt/messages.json +++ b/app/_locales/lt/messages.json @@ -38,9 +38,6 @@ "addNetwork": { "message": "Pridėti tinklą" }, - "addRecipient": { - "message": "Pridėti gavėją" - }, "addSuggestedTokens": { "message": "Pridėti siūlomų žetonų" }, @@ -53,9 +50,6 @@ "addToken": { "message": "Pridėti žetoną" }, - "addTokens": { - "message": "Pridėti žetonų" - }, "advanced": { "message": "Išplėstiniai" }, @@ -543,7 +537,7 @@ "letsGoSetUp": { "message": "Taip, pradėkime!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Ar norėtumėte pridėti šiuos žetonus?" }, "links": { @@ -895,9 +889,6 @@ "sendTokens": { "message": "Siųsti žetonus" }, - "sentEther": { - "message": "siųsti eterių" - }, "separateEachWord": { "message": "Kiekvieną žodį atskirkite viengubu tarpu" }, diff --git a/app/_locales/lv/messages.json b/app/_locales/lv/messages.json index e2dc7dccc..3fdcc90a0 100644 --- a/app/_locales/lv/messages.json +++ b/app/_locales/lv/messages.json @@ -38,9 +38,6 @@ "addNetwork": { "message": "Pievienot tīklu" }, - "addRecipient": { - "message": "Pievienot saņēmēju" - }, "addSuggestedTokens": { "message": "Pievienot ieteiktos marķierus" }, @@ -53,9 +50,6 @@ "addToken": { "message": "Pievienot žetonu" }, - "addTokens": { - "message": "Pievienot marķierus" - }, "advanced": { "message": "Papildu" }, @@ -276,7 +270,7 @@ "message": "Daļskaitļiem jābūt diapazonā no 0 līdz 36." }, "defaultNetwork": { - "message": "Galvenais Ether darījumu tīkls ir Main Net." + "message": "Galvenais Ether darījumu tīkls ir Mainnet." }, "delete": { "message": "Dzēst" @@ -539,7 +533,7 @@ "letsGoSetUp": { "message": "Jā, sāksim iestatīšanu!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Vai vēlaties pievienot šos marķierus?" }, "links": { @@ -891,9 +885,6 @@ "sendTokens": { "message": "Nosūtīt marķierus" }, - "sentEther": { - "message": "nosūtītie ether" - }, "separateEachWord": { "message": "Atdaliet katru vārdu ar vienu atstarpi" }, diff --git a/app/_locales/ms/messages.json b/app/_locales/ms/messages.json index 340cecf2f..38f440b04 100644 --- a/app/_locales/ms/messages.json +++ b/app/_locales/ms/messages.json @@ -38,9 +38,6 @@ "addNetwork": { "message": "Tambah Rangkaian" }, - "addRecipient": { - "message": "Tambah Penerima" - }, "addSuggestedTokens": { "message": "Tambah Token yang Disyorkan" }, @@ -53,9 +50,6 @@ "addToken": { "message": "Tambah Token" }, - "addTokens": { - "message": "Tambah Token" - }, "advanced": { "message": "Lanjutan" }, @@ -529,7 +523,7 @@ "letsGoSetUp": { "message": "Ya, mari sediakannya!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Adakah anda ingin menambah token ini?" }, "links": { @@ -872,9 +866,6 @@ "sendTokens": { "message": "Hantar Token" }, - "sentEther": { - "message": "menghantar ether" - }, "separateEachWord": { "message": "Pisahkan setiap perkataan dengan ruang tunggal" }, diff --git a/app/_locales/nl/messages.json b/app/_locales/nl/messages.json index e70f716af..f5daf1b8b 100644 --- a/app/_locales/nl/messages.json +++ b/app/_locales/nl/messages.json @@ -8,9 +8,6 @@ "addToken": { "message": "Voeg token toe" }, - "addTokens": { - "message": "Tokens toevoegen" - }, "amount": { "message": "Bedrag" }, @@ -80,7 +77,7 @@ "message": "Decimalen moeten minimaal 0 en niet meer dan 36 zijn." }, "defaultNetwork": { - "message": "Het standaardnetwerk voor Ether-transacties is Main Net." + "message": "Het standaardnetwerk voor Ether-transacties is Mainnet." }, "depositEther": { "message": "Stort Ether" @@ -188,7 +185,7 @@ "kovan": { "message": "Kovan-testnetwerk" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Wil je deze tokens toevoegen?" }, "loading": { @@ -201,7 +198,7 @@ "message": "Uitloggen" }, "mainnet": { - "message": "Main Netwerk" + "message": "Mainnetwerk" }, "message": { "message": "Bericht" diff --git a/app/_locales/no/messages.json b/app/_locales/no/messages.json index a62885187..c63a23962 100644 --- a/app/_locales/no/messages.json +++ b/app/_locales/no/messages.json @@ -38,9 +38,6 @@ "addNetwork": { "message": "Legg til nettverk" }, - "addRecipient": { - "message": "Legg til mottaker " - }, "addSuggestedTokens": { "message": "Legg til foreslåtte tokener " }, @@ -53,9 +50,6 @@ "addToken": { "message": "Legg til token " }, - "addTokens": { - "message": "Legg til sjetonger" - }, "advanced": { "message": "Avansert" }, @@ -273,7 +267,7 @@ "message": "Desimaler må være minst 0, og ikke flere enn 36." }, "defaultNetwork": { - "message": "Standardnettverket for Ether-transaksjoner er Main Net." + "message": "Standardnettverket for Ether-transaksjoner er Mainnet." }, "delete": { "message": "Slett" @@ -530,7 +524,7 @@ "letsGoSetUp": { "message": "Ja, la oss komme i gang!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Ønsker du å legge til disse tokenene" }, "links": { @@ -876,9 +870,6 @@ "sendTokens": { "message": "Send tokener" }, - "sentEther": { - "message": "sendt ether" - }, "separateEachWord": { "message": "Del hvert ord med et enkelt mellomrom " }, diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index 96637d9bf..b4c123481 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -79,9 +79,6 @@ "addNetwork": { "message": "Magdagdag ng Network" }, - "addRecipient": { - "message": "Magdagdag ng Recipient" - }, "addSuggestedTokens": { "message": "Magdagdag ng Mga Iminumungkahing Token" }, @@ -94,9 +91,6 @@ "addToken": { "message": "Magdagdag ng Token" }, - "addTokens": { - "message": "Magdagdag ng Mga Token" - }, "advanced": { "message": "Advanced" }, @@ -519,7 +513,7 @@ "message": "I-decrypt ang request" }, "defaultNetwork": { - "message": "Ang default na network para sa mga transaksyon ng Ether ay ang Main Net." + "message": "Ang default na network para sa mga transaksyon ng Ether ay ang Mainnet." }, "delete": { "message": "I-delete" @@ -1028,7 +1022,7 @@ "letsGoSetUp": { "message": "Sige, simulan na nating mag-set up!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Gusto mo bang idagdag ang mga token na ito?" }, "links": { @@ -1343,9 +1337,6 @@ "onlyAddTrustedNetworks": { "message": "Magagawa ng nakakapinsalang network provider na magsinungaling tungkol sa status ng blockchain at itala ang aktibidad ng iyong network. Magdagdag lang ng mga custom na network na pinagkakatiwalaan mo." }, - "onlyAvailableOnMainnet": { - "message": "Available lang sa mainnet" - }, "onlyConnectTrust": { "message": "Kumonekta lang sa mga site na pinagkakatiwalaan mo." }, @@ -1694,9 +1685,6 @@ "sendTokens": { "message": "Magpadala ng Mga Token" }, - "sentEther": { - "message": "nagpadala ng ether" - }, "separateEachWord": { "message": "Paghiwa-hiwalayin ang bawat salita gamit ang isang space" }, @@ -1876,10 +1864,6 @@ "message": "Walang available na token na tumutugma sa $1", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "Sinusuri ang $1", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapConfirmWithHwWallet": { "message": "Kumpirmahin ang iyong hardware wallet" }, @@ -1931,9 +1915,6 @@ "swapFetchingTokens": { "message": "Kinukuha ang mga token..." }, - "swapFinalizing": { - "message": "Isinasapinal..." - }, "swapFromTo": { "message": "Ang pag-swap ng $1 sa $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" diff --git a/app/_locales/pl/messages.json b/app/_locales/pl/messages.json index 903a977e0..4c7b7383d 100644 --- a/app/_locales/pl/messages.json +++ b/app/_locales/pl/messages.json @@ -38,9 +38,6 @@ "addNetwork": { "message": "Dodaj sieć" }, - "addRecipient": { - "message": "Dodaj odbiorcę" - }, "addSuggestedTokens": { "message": "Dodaj sugerowane tokeny." }, @@ -53,9 +50,6 @@ "addToken": { "message": "Dodaj token" }, - "addTokens": { - "message": "Dodaj tokeny" - }, "advanced": { "message": "Zaawansowane" }, @@ -276,7 +270,7 @@ "message": "Liczb po przecinku musi być co najmniej 0 i nie więcej niż 36." }, "defaultNetwork": { - "message": "Domyślna sieć dla Eteru to Main Net." + "message": "Domyślna sieć dla Eteru to Mainnet." }, "delete": { "message": "Usuń" @@ -543,7 +537,7 @@ "letsGoSetUp": { "message": "Tak, zacznijmy od początku!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Czy chcesz dodać te tokeny?" }, "links": { @@ -889,9 +883,6 @@ "sendTokens": { "message": "Wyślij tokeny" }, - "sentEther": { - "message": "wyślij eter" - }, "separateEachWord": { "message": "Oddziel słowa pojedynczą spacją" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 1d0676ee1..283ebdfb2 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -11,9 +11,6 @@ "addToken": { "message": "Adicionar Token" }, - "addTokens": { - "message": "Adicionar Tokens" - }, "amount": { "message": "Valor" }, @@ -83,7 +80,7 @@ "message": "Decimais devem ser no mínimo 0 e não passar de 36." }, "defaultNetwork": { - "message": "A rede pré definida para transações em Ether é a Main Net." + "message": "A rede pré definida para transações em Ether é a Mainnet." }, "depositEther": { "message": "Depositar Ether" @@ -194,7 +191,7 @@ "kovan": { "message": "Rede de Teste Kovan" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Gostaria de adicionar estes tokens?" }, "loading": { diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index 344179b45..79879c23f 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -79,9 +79,6 @@ "addNetwork": { "message": "Adicionar rede" }, - "addRecipient": { - "message": "Adicionar destinatário" - }, "addSuggestedTokens": { "message": "Adicionar tokens sugeridos" }, @@ -94,9 +91,6 @@ "addToken": { "message": "Adicionar token" }, - "addTokens": { - "message": "Adicionar tokens" - }, "advanced": { "message": "Avançadas" }, @@ -1028,7 +1022,7 @@ "letsGoSetUp": { "message": "Sim, vamos fazer a configuração!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Você gostaria de adicionar esses tokens?" }, "links": { @@ -1343,9 +1337,6 @@ "onlyAddTrustedNetworks": { "message": "Um provedor de rede mal-intencionado pode mentir sobre o estado do blockchain e gravar a atividade da sua rede. Adicione somente as redes em que você confia." }, - "onlyAvailableOnMainnet": { - "message": "Disponível somente na mainnet" - }, "onlyConnectTrust": { "message": "Conecte-se somente com sites em quem você confia." }, @@ -1694,9 +1685,6 @@ "sendTokens": { "message": "Enviar tokens" }, - "sentEther": { - "message": "ether enviado" - }, "separateEachWord": { "message": "Separe cada palavra com um único espaço" }, @@ -1876,10 +1864,6 @@ "message": "Nenhum token disponível correspondente a $1", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "Verificando $1", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapConfirmWithHwWallet": { "message": "Confirme com sua carteira de hardware" }, @@ -1931,9 +1915,6 @@ "swapFetchingTokens": { "message": "Fetch dos tokens..." }, - "swapFinalizing": { - "message": "Finalizando..." - }, "swapFromTo": { "message": "O swap de $1 para $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" diff --git a/app/_locales/ro/messages.json b/app/_locales/ro/messages.json index ec7270acf..9e934ab69 100644 --- a/app/_locales/ro/messages.json +++ b/app/_locales/ro/messages.json @@ -38,9 +38,6 @@ "addNetwork": { "message": "Adăugați rețea" }, - "addRecipient": { - "message": "Adăugați destinatarul" - }, "addSuggestedTokens": { "message": "Adăugați indicativele sugerate" }, @@ -53,9 +50,6 @@ "addToken": { "message": "Adăugare simbol" }, - "addTokens": { - "message": "Adăugați token-uri" - }, "advanced": { "message": "Avansate" }, @@ -276,7 +270,7 @@ "message": "Zecimalele trebuie să fie cel puțin 0, dar nu peste 36." }, "defaultNetwork": { - "message": "Rețeaua implicită pentru tranzacțiile cu Ether este Main Net." + "message": "Rețeaua implicită pentru tranzacțiile cu Ether este Mainnet." }, "delete": { "message": "Șterge" @@ -533,7 +527,7 @@ "letsGoSetUp": { "message": "Da, hai să configurăm!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Adăugați aceste indicative?" }, "links": { @@ -882,9 +876,6 @@ "sendTokens": { "message": "Trimiteți indicative" }, - "sentEther": { - "message": "trimiteți ether" - }, "separateEachWord": { "message": "Despărțiți fiecare cuvânt cu un spațiu" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 353a543a3..4d7457124 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -79,9 +79,6 @@ "addNetwork": { "message": "Добавить сеть" }, - "addRecipient": { - "message": "Добавить получателя" - }, "addSuggestedTokens": { "message": "Добавить предложенные токены" }, @@ -94,9 +91,6 @@ "addToken": { "message": "Добавить токен" }, - "addTokens": { - "message": "Добавить токены" - }, "advanced": { "message": "Дополнительно" }, @@ -519,7 +513,7 @@ "message": "Расшифровать запрос" }, "defaultNetwork": { - "message": "Сетью по умолчанию для транзакций Ether является Main Net." + "message": "Сетью по умолчанию для транзакций Ether является Mainnet." }, "delete": { "message": "Удалить" @@ -1028,7 +1022,7 @@ "letsGoSetUp": { "message": "Да, давайте настроим!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Вы хотели бы добавить эти токены?" }, "links": { @@ -1343,9 +1337,6 @@ "onlyAddTrustedNetworks": { "message": "Вредоносный сетевой провайдер может дезинформировать о состоянии блокчейна и записывать ваши действия в сети. Добавляйте только те пользовательские сети, которым доверяете." }, - "onlyAvailableOnMainnet": { - "message": "Доступно только в mainnet" - }, "onlyConnectTrust": { "message": "Подключайтесь только к сайтам, которым доверяете." }, @@ -1555,7 +1546,7 @@ "message": "Новый URL RPC" }, "save": { - "message": "«»Сохранить" + "message": "Сохранить" }, "saveAsCsvFile": { "message": "Сохранить как файл CSV" @@ -1694,9 +1685,6 @@ "sendTokens": { "message": "Отправить токены" }, - "sentEther": { - "message": "отправленный Ether" - }, "separateEachWord": { "message": "Отделяйте каждое слово одним пробелом" }, @@ -1876,10 +1864,6 @@ "message": "Нет доступных токенов соответствующих $1", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "Проверка $1", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapConfirmWithHwWallet": { "message": "Подтвердить с помощью аппаратного кошелька" }, @@ -1931,9 +1915,6 @@ "swapFetchingTokens": { "message": "Получение токенов..." }, - "swapFinalizing": { - "message": "Завершение..." - }, "swapFromTo": { "message": "Своп $1 на $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" diff --git a/app/_locales/sk/messages.json b/app/_locales/sk/messages.json index 5f273558e..0a5af86c0 100644 --- a/app/_locales/sk/messages.json +++ b/app/_locales/sk/messages.json @@ -38,9 +38,6 @@ "addNetwork": { "message": "Pridať sieť" }, - "addRecipient": { - "message": "Pridať príjemcu" - }, "addSuggestedTokens": { "message": "Pridať navrhované tokeny" }, @@ -53,9 +50,6 @@ "addToken": { "message": "Přidat token" }, - "addTokens": { - "message": "Přidat tokeny" - }, "advanced": { "message": "Rozšírené" }, @@ -270,7 +264,7 @@ "message": "Desetinných míst musí být od 0 do 36." }, "defaultNetwork": { - "message": "Výchozí síť pro Etherové transakce je Main Net." + "message": "Výchozí síť pro Etherové transakce je Mainnet." }, "delete": { "message": "Odstrániť" @@ -527,7 +521,7 @@ "letsGoSetUp": { "message": "Áno, poďme to nastaviť!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Chcete přidat tyto tokeny?" }, "links": { @@ -858,9 +852,6 @@ "sendTokens": { "message": "Odeslat tokeny" }, - "sentEther": { - "message": "poslaný ether" - }, "separateEachWord": { "message": "Každé slovo oddeľte jednou medzerou" }, diff --git a/app/_locales/sl/messages.json b/app/_locales/sl/messages.json index 726dbb591..2aa7c40d3 100644 --- a/app/_locales/sl/messages.json +++ b/app/_locales/sl/messages.json @@ -38,9 +38,6 @@ "addNetwork": { "message": "Dodaj omrežje" }, - "addRecipient": { - "message": "Dodaj prejemnika" - }, "addSuggestedTokens": { "message": "Dodaj priporočene žetone" }, @@ -53,9 +50,6 @@ "addToken": { "message": "Dodaj žeton" }, - "addTokens": { - "message": "Dodaj žetone" - }, "advanced": { "message": "Napredno" }, @@ -537,7 +531,7 @@ "letsGoSetUp": { "message": "Lotimo se nastavitev!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Želite dodati te žetone?" }, "links": { @@ -880,9 +874,6 @@ "sendTokens": { "message": "Pošlji žetone" }, - "sentEther": { - "message": "poslani ether" - }, "separateEachWord": { "message": "Vsako besedo ločite z enim presledkom" }, diff --git a/app/_locales/sr/messages.json b/app/_locales/sr/messages.json index 37a43945d..5a21f3df1 100644 --- a/app/_locales/sr/messages.json +++ b/app/_locales/sr/messages.json @@ -38,9 +38,6 @@ "addNetwork": { "message": "Dodajte mrežu" }, - "addRecipient": { - "message": "Dodaj primaoca" - }, "addSuggestedTokens": { "message": "Dodajte sugerisane tokene" }, @@ -53,9 +50,6 @@ "addToken": { "message": "Dodaj token" }, - "addTokens": { - "message": "Dodaj tokene" - }, "advanced": { "message": "Напредне опције" }, @@ -273,7 +267,7 @@ "message": "Decimalni broj mora biti najmanje 0, a ne veći od 36." }, "defaultNetwork": { - "message": "Podrazumevana mreža za Ether transakcije je Main Net." + "message": "Podrazumevana mreža za Ether transakcije je Mainnet." }, "delete": { "message": "Избриши" @@ -540,7 +534,7 @@ "letsGoSetUp": { "message": "Da, hajde da sve podesimo!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Želite li da dodate ove tokene?" }, "links": { @@ -886,9 +880,6 @@ "sendTokens": { "message": "Pošalji tokene" }, - "sentEther": { - "message": "ether je poslat" - }, "separateEachWord": { "message": "Razdvojite svaku reč jednim mestom razmaka" }, diff --git a/app/_locales/sv/messages.json b/app/_locales/sv/messages.json index c6e35a7a5..790027eda 100644 --- a/app/_locales/sv/messages.json +++ b/app/_locales/sv/messages.json @@ -38,9 +38,6 @@ "addNetwork": { "message": "Lägg till nätverk" }, - "addRecipient": { - "message": "Lägg till mottagare" - }, "addSuggestedTokens": { "message": "Lägg till föreslagna tokens" }, @@ -53,9 +50,6 @@ "addToken": { "message": "Lägg till token" }, - "addTokens": { - "message": "Lägg till tokens" - }, "advanced": { "message": "Avancerat" }, @@ -270,7 +264,7 @@ "message": "Decimalerna måste vara minst 0 och inte över 36." }, "defaultNetwork": { - "message": "Standardnätverket för Ether-transaktioner är Main Net." + "message": "Standardnätverket för Ether-transaktioner är Mainnet." }, "delete": { "message": "Radera" @@ -533,7 +527,7 @@ "letsGoSetUp": { "message": "Ja, kör igång!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Vill du lägga till dessa tokens?" }, "links": { @@ -879,9 +873,6 @@ "sendTokens": { "message": "Skicka tokens" }, - "sentEther": { - "message": "skickat ether" - }, "separateEachWord": { "message": "Lägg in ett mellanslag mellan varje ord" }, diff --git a/app/_locales/sw/messages.json b/app/_locales/sw/messages.json index f2c3a4fa4..79c583c7b 100644 --- a/app/_locales/sw/messages.json +++ b/app/_locales/sw/messages.json @@ -38,9 +38,6 @@ "addNetwork": { "message": "Ongeza Mtandao" }, - "addRecipient": { - "message": "Ongeza Mpokeaji" - }, "addSuggestedTokens": { "message": "Ongeza Vianzio Vilivyopendekezwa" }, @@ -53,9 +50,6 @@ "addToken": { "message": "Ongeza Kianzio" }, - "addTokens": { - "message": "Ongeza Vianzio" - }, "advanced": { "message": "Mipangilio ya kina" }, @@ -270,7 +264,7 @@ "message": "Desimali zinapaswa kuwa angalau 0, na si zaidi ya 36." }, "defaultNetwork": { - "message": "Mtandao chaguomsingi wa miamala ya Ether ni Main Net." + "message": "Mtandao chaguomsingi wa miamala ya Ether ni Mainnet." }, "delete": { "message": "Futa" @@ -530,7 +524,7 @@ "letsGoSetUp": { "message": "Ndiyo, hebu tuweke mipangilio!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Je, ungependa kuongeza vianzio hivi?" }, "links": { @@ -873,9 +867,6 @@ "sendTokens": { "message": "Tuma Vianzio" }, - "sentEther": { - "message": "ether iliyotumwa" - }, "separateEachWord": { "message": "Tenganisha kila neno kwa nafasi moja" }, diff --git a/app/_locales/ta/messages.json b/app/_locales/ta/messages.json index 425b104c1..be36f14c0 100644 --- a/app/_locales/ta/messages.json +++ b/app/_locales/ta/messages.json @@ -14,9 +14,6 @@ "addToken": { "message": "டோக்கனைச் சேர்" }, - "addTokens": { - "message": "டோக்கன்களைச் சேர்" - }, "advanced": { "message": "மேம்பட்டவை" }, @@ -251,7 +248,7 @@ "learnMore": { "message": "மேலும் அறிக" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "இந்த டோக்கன்களைச் சேர்க்க விரும்புகிறீர்களா?" }, "links": { diff --git a/app/_locales/th/messages.json b/app/_locales/th/messages.json index 3c193a838..43e90d04c 100644 --- a/app/_locales/th/messages.json +++ b/app/_locales/th/messages.json @@ -14,9 +14,6 @@ "addToken": { "message": "เพิ่มโทเค็น" }, - "addTokens": { - "message": "เพิ่มหลายโทเค็น" - }, "amount": { "message": "จำนวน" }, @@ -122,7 +119,7 @@ "message": "จำนวนต้องมากกว่า 0 และไม่เกิน 36" }, "defaultNetwork": { - "message": "ค่าเริ่มต้นของเครือข่ายสำหรับทำรายการธุรกรรมอีเธอร์คือ Main Net" + "message": "ค่าเริ่มต้นของเครือข่ายสำหรับทำรายการธุรกรรมอีเธอร์คือ Mainnet" }, "delete": { "message": "ลบ" @@ -269,7 +266,7 @@ "learnMore": { "message": "เรียนรู้เพิ่มเติม" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "คุณต้องการเพิ่มโทเค็นเหล่านี้หรือไม่?" }, "loading": { @@ -282,7 +279,7 @@ "message": "ออกจากระบบ" }, "mainnet": { - "message": "เครือข่าย Main Net" + "message": "เครือข่าย Mainnet" }, "message": { "message": "ข้อความ" diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 523b97d57..c65591572 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -52,9 +52,6 @@ "addNetwork": { "message": "Magdagdag ng Network" }, - "addRecipient": { - "message": "Magdagdag ng Recipient" - }, "addSuggestedTokens": { "message": "Magdagdag ng Mga Iminumungkahing Token" }, @@ -67,9 +64,6 @@ "addToken": { "message": "Magdagdag ng Token" }, - "addTokens": { - "message": "Magdagdag ng Mga Token" - }, "advanced": { "message": "Advanced" }, @@ -441,7 +435,7 @@ "message": "I-decrypt ang request" }, "defaultNetwork": { - "message": "Ang default na network para sa mga transaksyon ng Ether ay ang Main Net." + "message": "Ang default na network para sa mga transaksyon ng Ether ay ang Mainnet." }, "delete": { "message": "I-delete" @@ -872,7 +866,7 @@ "letsGoSetUp": { "message": "Sige, simulan na nating mag-set up!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Gusto mo bang idagdag ang mga token na ito?" }, "links": { @@ -1087,9 +1081,6 @@ "message": "Isasara ng \"$1\" ang tab na ito at ididirekta ka pabalik sa $2", "description": "Return the user to the site that initiated onboarding" }, - "onlyAvailableOnMainnet": { - "message": "Available lang sa mainnet" - }, "onlyConnectTrust": { "message": "Kumonekta lang sa mga site na pinagkakatiwalaan mo." }, @@ -1365,9 +1356,6 @@ "sendTokens": { "message": "Magpadala ng Mga Token" }, - "sentEther": { - "message": "nagpadala ng ether" - }, "separateEachWord": { "message": "Paghiwa-hiwalayin ang bawat salita gamit ang espasyo" }, @@ -1516,10 +1504,6 @@ "message": "Walang available na token na tumutugma sa $1", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "Sinusuri ang $1", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapCustom": { "message": "custom" }, @@ -1558,9 +1542,6 @@ "swapFetchingTokens": { "message": "Kinukuha ang mga token..." }, - "swapFinalizing": { - "message": "Isinasapinal..." - }, "swapLowSlippageError": { "message": "Maaaring hindi magtagumpay ang transaksyon, masyadong mababa ang max na slippage." }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 3c4bdebf7..15723d6ed 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -11,9 +11,6 @@ "addToken": { "message": "Jeton ekle" }, - "addTokens": { - "message": "Jetonlar ekle" - }, "amount": { "message": "Tutar" }, @@ -104,7 +101,7 @@ "message": "Ondalıklar en azından 0 olmalı ve 36'dan büyük olmamalı." }, "defaultNetwork": { - "message": "Ether işlemleri için varsayılan ağ Main Net." + "message": "Ether işlemleri için varsayılan ağ Mainnet." }, "depositEther": { "message": "Ether yatır" @@ -221,7 +218,7 @@ "learnMore": { "message": "Daha fazla bilgi." }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Bu jetonlara adres eklemek ister misiniz?" }, "links": { @@ -322,7 +319,7 @@ "message": "Gelecekte Bu jetonu hesap seçenekleri menüsünde “Jeton ekle”'ye giderek geri ekleyebilirsiniz." }, "reject": { - "message": "Reddetmek" + "message": "Reddet" }, "rejected": { "message": "Rededildi" diff --git a/app/_locales/uk/messages.json b/app/_locales/uk/messages.json index 6beb7c861..a9d372722 100644 --- a/app/_locales/uk/messages.json +++ b/app/_locales/uk/messages.json @@ -38,9 +38,6 @@ "addNetwork": { "message": "Додати мережу" }, - "addRecipient": { - "message": "Додати отримувача" - }, "addSuggestedTokens": { "message": "Додати рекомендовані токени" }, @@ -53,9 +50,6 @@ "addToken": { "message": "Додати токен" }, - "addTokens": { - "message": "Додати токени" - }, "advanced": { "message": "Розширені" }, @@ -276,7 +270,7 @@ "message": "Кількість розрядів після коми: від 0 до 36." }, "defaultNetwork": { - "message": "Мережа для транзакцій з Ether за замовчуванням - Main Net." + "message": "Мережа для транзакцій з Ether за замовчуванням - Mainnet." }, "delete": { "message": "Видалити" @@ -543,7 +537,7 @@ "letsGoSetUp": { "message": "Так, давайте налаштуємо!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Ви б хотіли додати ці токени?" }, "links": { @@ -895,9 +889,6 @@ "sendTokens": { "message": "Надіслати токени" }, - "sentEther": { - "message": "надісланий ефір" - }, "separateEachWord": { "message": "Відділіть кожне слово одним пробілом" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index c6f1af5a3..1553f11ee 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -79,9 +79,6 @@ "addNetwork": { "message": "Thêm mạng" }, - "addRecipient": { - "message": "Thêm người nhận" - }, "addSuggestedTokens": { "message": "Thêm token được đề xuất" }, @@ -94,9 +91,6 @@ "addToken": { "message": "Thêm token" }, - "addTokens": { - "message": "Thêm token" - }, "advanced": { "message": "Nâng cao" }, @@ -1028,7 +1022,7 @@ "letsGoSetUp": { "message": "Có, hãy thiết lập!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "Bạn có muốn thêm những token này không?" }, "links": { @@ -1343,9 +1337,6 @@ "onlyAddTrustedNetworks": { "message": "Một nhà cung cấp mạng độc hại có thể nói dối về trạng thái của chuỗi khối và ghi lại hoạt động của bạn trên mạng. Chỉ thêm các mạng tùy chỉnh mà bạn tin tưởng." }, - "onlyAvailableOnMainnet": { - "message": "Chỉ có trên mạng chính thức" - }, "onlyConnectTrust": { "message": "Chỉ kết nối với các trang web mà bạn tin tưởng." }, @@ -1694,9 +1685,6 @@ "sendTokens": { "message": "Gửi token" }, - "sentEther": { - "message": "đã gửi ether" - }, "separateEachWord": { "message": "Phân tách mỗi từ bằng một dấu cách" }, @@ -1876,10 +1864,6 @@ "message": "Không có token nào khớp với $1", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "Đang kiểm tra $1", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapConfirmWithHwWallet": { "message": "Xác nhận ví cứng của bạn" }, @@ -1931,9 +1915,6 @@ "swapFetchingTokens": { "message": "Đang tìm nạp token..." }, - "swapFinalizing": { - "message": "Đang hoàn tất..." - }, "swapFromTo": { "message": "Giao dịch hoán đổi $1 sang $2", "description": "Tells a user that they need to confirm on their hardware wallet a swap of 2 tokens. $1 is a source token and $2 is a destination token" diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index e9de843e4..1a83650d4 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -52,9 +52,6 @@ "addNetwork": { "message": "添加网络" }, - "addRecipient": { - "message": "添加接收方" - }, "addSuggestedTokens": { "message": "添加推荐代币" }, @@ -67,9 +64,6 @@ "addToken": { "message": "添加代币" }, - "addTokens": { - "message": "添加代币" - }, "advanced": { "message": "高级" }, @@ -875,7 +869,7 @@ "letsGoSetUp": { "message": "第一次,立即开始设置!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "您想添加这些代币吗?" }, "links": { @@ -1090,9 +1084,6 @@ "message": "“$1”会关闭此标签,直接回到 $2", "description": "Return the user to the site that initiated onboarding" }, - "onlyAvailableOnMainnet": { - "message": "仅在主网(mainnet)上提供" - }, "onlyConnectTrust": { "message": "只连接您信任的网站。" }, @@ -1368,9 +1359,6 @@ "sendTokens": { "message": "发送代币" }, - "sentEther": { - "message": "发送 Ether" - }, "separateEachWord": { "message": "用空格分隔每个单词" }, @@ -1522,10 +1510,6 @@ "message": "没有匹配的代币符合 $1", "description": "Tells the user that a given search string does not match any tokens in our token lists. $1 can be any string of text" }, - "swapCheckingQuote": { - "message": "正在检查 $1", - "description": "Shown to the user during quote loading. $1 is the name of an aggregator. The message indicates that metamask is currently checking if that aggregator has a trade/quote for their requested swap." - }, "swapCustom": { "message": "自定义" }, @@ -1564,9 +1548,6 @@ "swapFetchingTokens": { "message": "获取代币中……" }, - "swapFinalizing": { - "message": "确定中……" - }, "swapLowSlippageError": { "message": "交易可能失败,最大滑点过低。" }, diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index 208b184a1..045826588 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -41,9 +41,6 @@ "addNetwork": { "message": "新增網路" }, - "addRecipient": { - "message": "新增接收人" - }, "addSuggestedTokens": { "message": "加入建議的代幣" }, @@ -56,9 +53,6 @@ "addToken": { "message": "加入代幣" }, - "addTokens": { - "message": "加入代幣" - }, "advanced": { "message": "進階" }, @@ -552,7 +546,7 @@ "letsGoSetUp": { "message": "好,我們開始吧!" }, - "likeToAddTokens": { + "likeToImportTokens": { "message": "確定要加入代幣?" }, "links": { @@ -883,9 +877,6 @@ "sendTokens": { "message": "發送代幣" }, - "sentEther": { - "message": "發送以太幣" - }, "separateEachWord": { "message": "單詞之間請以空白間隔" }, diff --git a/app/build-types/beta/beta-mascot.json b/app/build-types/beta/beta-mascot.json new file mode 100644 index 000000000..5f922a57e --- /dev/null +++ b/app/build-types/beta/beta-mascot.json @@ -0,0 +1,337 @@ +{ + "chunks": [ + { + "color": [0, 0, 0], + "faces": [ + [11, 12, 13], + [36, 15, 37], + [37, 38, 36], + [31, 39, 22], + [22, 21, 31], + [31, 15, 36], + [36, 39, 31], + [64, 65, 66], + [75, 69, 26], + [26, 80, 75], + [75, 80, 38], + [38, 37, 75], + [38, 80, 39], + [39, 36, 38], + [39, 80, 26], + [26, 22, 39] + ] + }, + { + "color": [236, 229, 220], + "faces": [ + [19, 20, 21], + [21, 22, 19], + [20, 19, 23], + [23, 24, 20], + [23, 25, 24], + [19, 22, 26], + [26, 27, 19], + [23, 28, 29], + [23, 29, 30], + [25, 23, 30], + [21, 20, 24], + [24, 31, 21], + [24, 25, 30], + [29, 51, 52], + [52, 30, 29], + [27, 26, 69], + [69, 70, 27], + [70, 71, 72], + [72, 27, 70], + [72, 71, 73], + [51, 74, 72], + [52, 51, 72], + [73, 52, 72], + [69, 71, 70], + [71, 69, 75], + [52, 73, 71], + [19, 27, 74], + [74, 28, 19], + [51, 29, 28], + [28, 74, 51], + [74, 27, 72], + [28, 23, 19] + ] + }, + { + "color": [119, 228, 171], + "faces": [ + [5, 4, 35], + [57, 59, 79] + ] + }, + { + "color": [80, 157, 116], + "faces": [ + [4, 5, 2], + [2, 5, 6], + [57, 56, 55], + [58, 59, 55], + [2, 1, 4], + [55, 59, 57] + ] + }, + { + "color": [67, 127, 95], + "faces": [ + [0, 1, 2], + [2, 3, 0], + [6, 3, 2], + [7, 8, 9], + [10, 3, 6], + [10, 50, 7], + [7, 3, 10], + [7, 9, 3], + [49, 0, 9], + [3, 9, 0], + [53, 54, 55], + [55, 56, 53], + [55, 54, 58], + [60, 61, 62], + [63, 58, 54], + [63, 60, 89], + [60, 63, 54], + [60, 54, 61], + [88, 61, 53], + [54, 53, 61] + ] + }, + { + "color": [119, 228, 207], + "faces": [ + [59, 5, 35], + [35, 79, 59] + ] + }, + { + "color": [163, 230, 235], + "faces": [ + [14, 15, 11], + [11, 16, 14], + [16, 13, 12], + [17, 33, 10], + [17, 18, 34], + [34, 33, 17], + [11, 15, 31], + [18, 12, 11], + [41, 64, 37], + [64, 41, 40], + [66, 65, 40], + [67, 63, 77], + [67, 77, 76], + [76, 68, 67], + [75, 37, 64], + [68, 64, 66] + ] + }, + { + "color": [204, 237, 236], + "faces": [ + [10, 6, 17], + [31, 18, 11], + [14, 16, 40], + [40, 41, 14], + [63, 67, 58], + [64, 68, 75], + [14, 41, 37], + [37, 15, 14], + [5, 59, 40], + [40, 16, 5] + ] + }, + { + "color": [207, 248, 247], + "faces": [ + [6, 5, 16], + [16, 17, 6], + [12, 17, 16], + [58, 67, 40], + [40, 59, 58], + [40, 67, 66] + ] + }, + { + "color": [127, 185, 228], + "faces": [ + [33, 34, 24], + [71, 76, 77] + ] + }, + { + "color": [119, 200, 228], + "faces": [ + [31, 24, 18], + [24, 34, 18], + [35, 4, 42], + [4, 1, 42], + [42, 43, 44], + [44, 35, 42], + [45, 43, 42], + [42, 10, 45], + [30, 32, 24], + [30, 33, 32], + [33, 30, 10], + [44, 43, 46], + [43, 45, 47], + [47, 46, 43], + [48, 47, 45], + [45, 30, 48], + [30, 45, 10], + [49, 42, 0], + [8, 7, 42], + [50, 42, 7], + [50, 10, 42], + [1, 0, 42], + [42, 9, 8], + [42, 49, 9], + [75, 68, 71], + [71, 68, 76], + [79, 81, 57], + [57, 81, 56], + [82, 79, 35], + [35, 44, 82], + [81, 79, 82], + [82, 83, 81], + [84, 63, 81], + [81, 83, 84], + [44, 46, 85], + [85, 82, 44], + [71, 78, 52], + [52, 78, 77], + [77, 63, 52], + [82, 85, 83], + [83, 85, 86], + [86, 84, 83], + [87, 52, 84], + [84, 86, 87], + [52, 63, 84], + [88, 53, 81], + [62, 81, 60], + [89, 60, 81], + [89, 81, 63], + [56, 81, 53], + [81, 62, 61], + [81, 61, 88], + [48, 87, 86], + [86, 47, 48], + [47, 86, 85], + [85, 46, 47], + [48, 30, 52], + [52, 87, 48] + ] + }, + { + "color": [95, 167, 211], + "faces": [ + [24, 32, 33], + [77, 78, 71] + ] + }, + { + "color": [119, 222, 228], + "faces": [ + [17, 12, 18], + [13, 16, 11], + [67, 68, 66], + [65, 64, 40] + ] + } + ], + "positions": [ + [111.024597, 52.604599, 46.225899], + [114.025002, 87.673302, 58.9818], + [66.192001, 80.898003, 55.394299], + [72.113297, 35.491798, 30.871401], + [97.804497, 116.560997, 73.978798], + [16.7623, 58.010899, 58.078201], + [52.608898, 30.3641, 42.556099], + [106.881401, 31.945499, 46.9133], + [113.484596, 38.6049, 49.121498], + [108.6633, 43.2332, 46.315399], + [101.216599, 15.9822, 46.308201], + [16.6605, -16.2883, 93.618698], + [40.775002, -10.2288, 85.276398], + [23.926901, -2.5103, 86.736504], + [11.1691, -7.0037, 99.377602], + [9.5692, -34.393902, 141.671997], + [12.596, 7.1655, 88.740997], + [61.180901, 8.8142, 76.996803], + [39.719501, -28.927099, 88.963799], + [13.7962, -68.575699, 132.057007], + [15.2674, -62.32, 129.688004], + [14.8446, -52.6096, 140.113007], + [12.8917, -49.771599, 144.740997], + [35.604198, -71.758003, 81.063904], + [47.462502, -68.606102, 63.369701], + [38.2486, -64.730202, 38.909901], + [-12.8917, -49.771599, 144.740997], + [-13.7962, -68.575699, 132.057007], + [17.802099, -71.758003, 81.063904], + [19.1243, -69.0168, 49.420101], + [38.2486, -66.275597, 17.776199], + [12.8928, -36.703499, 141.671997], + [109.283997, -93.589897, 27.824301], + [122.117996, -36.8894, 35.025002], + [67.7668, -30.197001, 78.417801], + [33.180698, 101.851997, 25.3186], + [9.4063, -35.589802, 150.722], + [-9.5692, -34.393902, 141.671997], + [-9.4063, -35.589802, 150.722], + [11.4565, -37.899399, 150.722], + [-12.596, 7.1655, 88.740997], + [-11.1691, -7.0037, 99.377602], + [70.236504, 62.836201, -3.9475], + [47.263401, 54.293999, -27.414801], + [28.7302, 91.731102, -24.972601], + [69.167603, 6.5862, -12.7757], + [28.7302, 49.1003, -48.3596], + [31.903, 5.692, -47.821999], + [35.075802, -34.432899, -16.280899], + [115.284103, 48.681499, 48.684101], + [110.842796, 28.4821, 49.176201], + [-19.1243, -69.0168, 49.420101], + [-38.2486, -66.275597, 17.776199], + [-111.024597, 52.604599, 46.225899], + [-72.113297, 35.491798, 30.871401], + [-66.192001, 80.898003, 55.394299], + [-114.025002, 87.673302, 58.9818], + [-97.804497, 116.560997, 73.978798], + [-52.608898, 30.3641, 42.556099], + [-16.7623, 58.010899, 58.078201], + [-106.881401, 31.945499, 46.9133], + [-108.6633, 43.2332, 46.315399], + [-113.484596, 38.6049, 49.121498], + [-101.216599, 15.9822, 46.308201], + [-16.6605, -16.2883, 93.618698], + [-23.926901, -2.5103, 86.736504], + [-40.775002, -10.2288, 85.276398], + [-61.180901, 8.8142, 76.996803], + [-39.719501, -28.927099, 88.963799], + [-14.8446, -52.6096, 140.113007], + [-15.2674, -62.32, 129.688004], + [-47.462502, -68.606102, 63.369701], + [-35.604198, -71.758003, 81.063904], + [-38.2486, -64.730202, 38.909901], + [-17.802099, -71.758003, 81.063904], + [-12.8928, -36.703499, 141.671997], + [-67.7668, -30.197001, 78.417801], + [-122.117996, -36.8894, 35.025002], + [-109.283997, -93.589897, 27.824301], + [-33.180698, 101.851997, 25.3186], + [-11.4565, -37.899399, 150.722], + [-70.236504, 62.836201, -3.9475], + [-28.7302, 91.731102, -24.972601], + [-47.263401, 54.293999, -27.414801], + [-69.167603, 6.5862, -12.7757], + [-28.7302, 49.1003, -48.3596], + [-31.903, 5.692, -47.821999], + [-35.075802, -34.432899, -16.280899], + [-115.284103, 48.681499, 48.684101], + [-110.842796, 28.4821, 49.176201] + ] +} diff --git a/app/build-types/beta/icon-128.png b/app/build-types/beta/icon-128.png new file mode 100644 index 000000000..97762ff99 Binary files /dev/null and b/app/build-types/beta/icon-128.png differ diff --git a/app/build-types/beta/icon-16.png b/app/build-types/beta/icon-16.png new file mode 100644 index 000000000..216b4ad06 Binary files /dev/null and b/app/build-types/beta/icon-16.png differ diff --git a/app/build-types/beta/icon-19.png b/app/build-types/beta/icon-19.png new file mode 100644 index 000000000..f7da09c5e Binary files /dev/null and b/app/build-types/beta/icon-19.png differ diff --git a/app/build-types/beta/icon-32.png b/app/build-types/beta/icon-32.png new file mode 100644 index 000000000..fb2a55a57 Binary files /dev/null and b/app/build-types/beta/icon-32.png differ diff --git a/app/build-types/beta/icon-38.png b/app/build-types/beta/icon-38.png new file mode 100644 index 000000000..e9449d4d0 Binary files /dev/null and b/app/build-types/beta/icon-38.png differ diff --git a/app/build-types/beta/icon-48.png b/app/build-types/beta/icon-48.png new file mode 100644 index 000000000..0fdfeb25c Binary files /dev/null and b/app/build-types/beta/icon-48.png differ diff --git a/app/build-types/beta/icon-512.png b/app/build-types/beta/icon-512.png new file mode 100644 index 000000000..09690ab1b Binary files /dev/null and b/app/build-types/beta/icon-512.png differ diff --git a/app/build-types/beta/icon-64.png b/app/build-types/beta/icon-64.png new file mode 100644 index 000000000..b60b7d5d5 Binary files /dev/null and b/app/build-types/beta/icon-64.png differ diff --git a/app/build-types/beta/info-logo.png b/app/build-types/beta/info-logo.png new file mode 100644 index 000000000..97762ff99 Binary files /dev/null and b/app/build-types/beta/info-logo.png differ diff --git a/app/build-types/beta/logo/metamask-fox.svg b/app/build-types/beta/logo/metamask-fox.svg new file mode 100644 index 000000000..53698aefd --- /dev/null +++ b/app/build-types/beta/logo/metamask-fox.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/build-types/beta/logo/metamask-logo-horizontal-dark.svg b/app/build-types/beta/logo/metamask-logo-horizontal-dark.svg new file mode 100644 index 000000000..3155a5149 --- /dev/null +++ b/app/build-types/beta/logo/metamask-logo-horizontal-dark.svg @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/build-types/beta/logo/metamask-logo-horizontal.svg b/app/build-types/beta/logo/metamask-logo-horizontal.svg new file mode 100644 index 000000000..43a44eb0e --- /dev/null +++ b/app/build-types/beta/logo/metamask-logo-horizontal.svg @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/images/mascot.json b/app/images/mascot.json new file mode 100644 index 000000000..319c10fb4 --- /dev/null +++ b/app/images/mascot.json @@ -0,0 +1,322 @@ +{ + "chunks": [ + { + "color": [246, 133, 27], + "faces": [ + [17, 33, 10], + [17, 18, 34], + [34, 33, 17], + [10, 6, 17], + [11, 15, 31], + [31, 18, 11], + [18, 12, 11], + [14, 16, 40], + [40, 41, 14], + [59, 5, 35], + [35, 79, 59], + [67, 63, 77], + [67, 77, 76], + [76, 68, 67], + [63, 67, 58], + [64, 68, 75], + [75, 37, 64], + [68, 64, 66], + [14, 41, 37], + [37, 15, 14], + [5, 59, 40], + [40, 16, 5] + ] + }, + { + "color": [228, 118, 27], + "faces": [ + [31, 24, 18], + [6, 5, 16], + [16, 17, 6], + [24, 32, 33], + [33, 34, 24], + [5, 4, 35], + [75, 68, 71], + [58, 67, 40], + [40, 59, 58], + [71, 76, 77], + [77, 78, 71] + ] + }, + { + "color": [118, 61, 22], + "faces": [ + [0, 1, 2], + [2, 3, 0], + [4, 5, 2], + [6, 3, 2], + [2, 5, 6], + [7, 8, 9], + [10, 3, 6], + [10, 50, 7], + [7, 3, 10], + [7, 9, 3], + [49, 0, 9], + [3, 9, 0], + [53, 54, 55], + [55, 56, 53], + [57, 56, 55], + [58, 59, 55], + [55, 54, 58], + [60, 61, 62], + [63, 58, 54], + [63, 60, 89], + [60, 63, 54], + [60, 54, 61], + [88, 61, 53], + [54, 53, 61], + [2, 1, 4], + [55, 59, 57] + ] + }, + { + "color": [22, 22, 22], + "faces": [ + [36, 15, 37], + [37, 38, 36], + [31, 39, 22], + [22, 21, 31], + [31, 15, 36], + [36, 39, 31], + [75, 69, 26], + [26, 80, 75], + [75, 80, 38], + [38, 37, 75], + [38, 80, 39], + [39, 36, 38], + [39, 80, 26], + [26, 22, 39] + ] + }, + { + "color": [215, 193, 179], + "faces": [ + [21, 20, 24], + [24, 31, 21], + [69, 71, 70], + [71, 69, 75] + ] + }, + { + "color": [192, 173, 158], + "faces": [ + [19, 20, 21], + [21, 22, 19], + [20, 19, 23], + [23, 24, 20], + [23, 25, 24], + [19, 22, 26], + [26, 27, 19], + [23, 28, 29], + [23, 29, 30], + [25, 23, 30], + [29, 51, 52], + [52, 30, 29], + [27, 26, 69], + [69, 70, 27], + [70, 71, 72], + [72, 27, 70], + [72, 71, 73], + [51, 74, 72], + [52, 51, 72], + [73, 52, 72], + [19, 27, 74], + [74, 28, 19], + [51, 29, 28], + [28, 74, 51], + [74, 27, 72], + [28, 23, 19] + ] + }, + { + "color": [205, 97, 22], + "faces": [ + [24, 34, 18], + [16, 13, 12], + [12, 17, 16], + [13, 16, 11], + [71, 68, 76], + [40, 67, 66], + [66, 65, 40], + [65, 64, 40] + ] + }, + { + "color": [35, 52, 71], + "faces": [ + [11, 12, 13], + [64, 65, 66] + ] + }, + { + "color": [228, 117, 31], + "faces": [ + [14, 15, 11], + [11, 16, 14], + [17, 12, 18], + [41, 64, 37], + [67, 68, 66] + ] + }, + { + "color": [226, 118, 27], + "faces": [ + [35, 4, 42], + [4, 1, 42], + [42, 43, 44], + [44, 35, 42], + [45, 43, 42], + [42, 10, 45], + [30, 32, 24], + [24, 25, 30], + [30, 33, 32], + [33, 30, 10], + [44, 43, 46], + [43, 45, 47], + [47, 46, 43], + [48, 47, 45], + [45, 30, 48], + [30, 45, 10], + [49, 42, 0], + [8, 7, 42], + [50, 42, 7], + [50, 10, 42], + [1, 0, 42], + [42, 9, 8], + [42, 49, 9], + [64, 41, 40], + [57, 59, 79], + [79, 81, 57], + [57, 81, 56], + [82, 79, 35], + [35, 44, 82], + [81, 79, 82], + [82, 83, 81], + [84, 63, 81], + [81, 83, 84], + [44, 46, 85], + [85, 82, 44], + [52, 73, 71], + [71, 78, 52], + [52, 78, 77], + [77, 63, 52], + [82, 85, 83], + [83, 85, 86], + [86, 84, 83], + [87, 52, 84], + [84, 86, 87], + [52, 63, 84], + [88, 53, 81], + [62, 81, 60], + [89, 60, 81], + [89, 81, 63], + [56, 81, 53], + [81, 62, 61], + [81, 61, 88], + [48, 87, 86], + [86, 47, 48], + [47, 86, 85], + [85, 46, 47], + [48, 30, 52], + [52, 87, 48] + ] + } + ], + "positions": [ + [111.0246, 52.6046, 46.2259], + [114.025, 87.6733, 58.9818], + [66.192, 80.898, 55.3943], + [72.1133, 35.4918, 30.8714], + [97.8045, 116.561, 73.9788], + [16.7623, 58.0109, 58.0782], + [52.6089, 30.3641, 42.5561], + [106.8814, 31.9455, 46.9133], + [113.4846, 38.6049, 49.1215], + [108.6633, 43.2332, 46.3154], + [101.2166, 15.9822, 46.3082], + [16.6605, -16.2883, 93.6187], + [40.775, -10.2288, 85.2764], + [23.9269, -2.5103, 86.7365], + [11.1691, -7.0037, 99.3776], + [9.5692, -34.3939, 141.672], + [12.596, 7.1655, 88.741], + [61.1809, 8.8142, 76.9968], + [39.7195, -28.9271, 88.9638], + [13.7962, -68.5757, 132.057], + [15.2674, -62.32, 129.688], + [14.8446, -52.6096, 140.113], + [12.8917, -49.7716, 144.741], + [35.6042, -71.758, 81.0639], + [47.4625, -68.6061, 63.3697], + [38.2486, -64.7302, 38.9099], + [-12.8917, -49.7716, 144.741], + [-13.7962, -68.5757, 132.057], + [17.8021, -71.758, 81.0639], + [19.1243, -69.0168, 49.4201], + [38.2486, -66.2756, 17.7762], + [12.8928, -36.7035, 141.672], + [109.284, -93.5899, 27.8243], + [122.118, -36.8894, 35.025], + [67.7668, -30.197, 78.4178], + [33.1807, 101.852, 25.3186], + [9.4063, -35.5898, 150.722], + [-9.5692, -34.3939, 141.672], + [-9.4063, -35.5898, 150.722], + [11.4565, -37.8994, 150.722], + [-12.596, 7.1655, 88.741], + [-11.1691, -7.0037, 99.3776], + [70.2365, 62.8362, -3.9475], + [47.2634, 54.294, -27.4148], + [28.7302, 91.7311, -24.9726], + [69.1676, 6.5862, -12.7757], + [28.7302, 49.1003, -48.3596], + [31.903, 5.692, -47.822], + [35.0758, -34.4329, -16.2809], + [115.2841, 48.6815, 48.6841], + [110.8428, 28.4821, 49.1762], + [-19.1243, -69.0168, 49.4201], + [-38.2486, -66.2756, 17.7762], + [-111.0246, 52.6046, 46.2259], + [-72.1133, 35.4918, 30.8714], + [-66.192, 80.898, 55.3943], + [-114.025, 87.6733, 58.9818], + [-97.8045, 116.561, 73.9788], + [-52.6089, 30.3641, 42.5561], + [-16.7623, 58.0109, 58.0782], + [-106.8814, 31.9455, 46.9133], + [-108.6633, 43.2332, 46.3154], + [-113.4846, 38.6049, 49.1215], + [-101.2166, 15.9822, 46.3082], + [-16.6605, -16.2883, 93.6187], + [-23.9269, -2.5103, 86.7365], + [-40.775, -10.2288, 85.2764], + [-61.1809, 8.8142, 76.9968], + [-39.7195, -28.9271, 88.9638], + [-14.8446, -52.6096, 140.113], + [-15.2674, -62.32, 129.688], + [-47.4625, -68.6061, 63.3697], + [-35.6042, -71.758, 81.0639], + [-38.2486, -64.7302, 38.9099], + [-17.8021, -71.758, 81.0639], + [-12.8928, -36.7035, 141.672], + [-67.7668, -30.197, 78.4178], + [-122.118, -36.8894, 35.025], + [-109.284, -93.5899, 27.8243], + [-33.1807, 101.852, 25.3186], + [-11.4565, -37.8994, 150.722], + [-70.2365, 62.8362, -3.9475], + [-28.7302, 91.7311, -24.9726], + [-47.2634, 54.294, -27.4148], + [-69.1676, 6.5862, -12.7757], + [-28.7302, 49.1003, -48.3596], + [-31.903, 5.692, -47.822], + [-35.0758, -34.4329, -16.2809], + [-115.2841, 48.6815, 48.6841], + [-110.8428, 28.4821, 49.1762] + ] +} diff --git a/app/images/videos/recovery-onboarding/subtitles-en.vtt b/app/images/videos/recovery-onboarding/subtitles/en.vtt similarity index 100% rename from app/images/videos/recovery-onboarding/subtitles-en.vtt rename to app/images/videos/recovery-onboarding/subtitles/en.vtt diff --git a/app/images/videos/recovery-onboarding/subtitles/es.vtt b/app/images/videos/recovery-onboarding/subtitles/es.vtt new file mode 100644 index 000000000..2f56d35be --- /dev/null +++ b/app/images/videos/recovery-onboarding/subtitles/es.vtt @@ -0,0 +1,115 @@ +WEBVTT + +1 +00:00:00.780 --> 00:00:04.580 +MetaMask es una nueva forma de conectarse +a sitios y aplicaciones. + +2 +00:00:04.580 --> 00:00:08.860 +En los sitios web tradicionales, una base de datos +o un banco central es responsable de controlar y + +3 +00:00:08.860 --> 00:00:10.179 +recuperar sus cuentas. + +4 +00:00:10.179 --> 00:00:15.050 +Pero con MetaMask, todo el control lo tiene +el titular de la clave maestra. + +5 +00:00:15.050 --> 00:00:18.460 +La persona que tenga esta clave controlará las cuentas. + +6 +00:00:18.460 --> 00:00:21.110 +La “clave maestra” +es su frase secreta de recuperación. + +7 +00:00:21.110 --> 00:00:26.070 +Esta frase está compuesta por 12 palabras y se crea +la primera vez que se configura MetaMask; le permite + +8 +00:00:26.070 --> 00:00:30.120 +recuperar su cartera y los fondos en caso de que +alguna vez pierda su clave de acceso. + +9 +00:00:30.120 --> 00:00:33.451 +Es fundamental que proteja +su cartera + +10 +00:00:33.451 --> 00:00:37.510 +guardando la frase secreta de recuperación +en un lugar sumamente seguro y secreto. + +11 +00:00:37.510 --> 00:00:41.429 +Si alguna persona llegara a encontrarla, accederá +a la “clave maestra” de su cartera y podrá + +12 +00:00:41.429 --> 00:00:45.190 +ingresar a todos sus fondos y tomarlos libremente. + +13 +00:00:45.190 --> 00:00:50.109 +Para proteger su cartera en MetaMask, +guarde en un lugar seguro su frase secreta de recuperación. + +14 +00:00:50.109 --> 00:00:54.930 +Puede anotarla, esconderla en algún lugar, +guardarla en una caja de seguridad + +15 +00:00:54.930 --> 00:00:57.729 +o utilizar un administrador seguro de contraseñas. + +16 +00:00:57.729 --> 00:01:01.050 +Inclusive, algunos usuarios graban +la frase en una placa metálica. + +17 +00:01:01.050 --> 00:01:04.440 +Si llegara a perder su frase secreta de recuperación, +ninguna persona, ni siquiera el equipo de MetaMask, podrá ayudarlo + +18 +00:01:04.440 --> 00:01:07.820 +a recuperar +su cartera. + +19 +00:01:07.820 --> 00:01:12.072 +Si aún no ha anotado ni guardado en un lugar seguro su +frase secreta de recuperación, + +20 +00:01:12.072 --> 00:01:15.492 +hágalo ahora mismo. Lo esperamos. + +21 +00:01:15.500 --> 00:01:20.780 +Y recuerde no compartir nunca su +frase secreta de recuperación con nadie; ni siquiera con nosotros. + +22 +00:01:20.780 --> 00:01:24.910 +Si alguien se la pide alguna vez, +será con intenciones de estafarlo. + +23 +00:01:24.910 --> 00:01:26.250 +¡Eso es todo! + +24 +00:01:26.250 --> 00:01:31.020 +Ahora ya sabe qué es una frase secreta de recuperación +y qué debe hacer para mantener protegida su cartera. diff --git a/app/images/videos/recovery-onboarding/subtitles/hi.vtt b/app/images/videos/recovery-onboarding/subtitles/hi.vtt new file mode 100644 index 000000000..f82c69566 --- /dev/null +++ b/app/images/videos/recovery-onboarding/subtitles/hi.vtt @@ -0,0 +1,115 @@ +WEBVTT + +1 +00:00:00.780 --> 00:00:04.580 +MetaMask साइटों और एप्लिकेशन से +जुड़ने का एक नया तरीका है। + +2 +00:00:04.580 --> 00:00:08.860 +पारंपरिक वेबसाइटों पर, आपके खातों को नियंत्रित करने और +पुनर्प्राप्त करने के लिए एक केंद्रीय डेटाबेस या + +3 +00:00:08.860 --> 00:00:10.179 +बैंक ज़िम्मेदार होता है। + +4 +00:00:10.179 --> 00:00:15.050 +लेकिन MetaMask पर, सारी शक्ति +मास्टर कुंजी के धारक की होती है। + +5 +00:00:15.050 --> 00:00:18.460 +जो भी कुंजी रखता है, वह खातों को नियंत्रित करता है। + +6 +00:00:18.460 --> 00:00:21.110 +आपका गुप्त रिकवरी फ्रेज़ +आपकी "मास्टर कुंजी" है। + +7 +00:00:21.110 --> 00:00:26.070 +यह 12 शब्दों की एक सीरीज़ होती है, जो +आपके द्वारा पहली बार MetaMask सेट करने पर जेनरेट होती है, जिससे + +8 +00:00:26.070 --> 00:00:30.120 +आप कभी भी एक्सेस खोने पर अपने वॉलेट और धन को +पुनर्प्राप्त कर सकते हैं। + +9 +00:00:30.120 --> 00:00:33.451 +यह महत्वपूर्ण है कि आप +अपने गुप्त रिकवरी फ्रेज़ को + +10 +00:00:33.451 --> 00:00:37.510 +बहुत सुरक्षित और बहुत गुप्त रखकर +अपने वॉलेट को सुरक्षित रखें। + +11 +00:00:37.510 --> 00:00:41.429 +अगर किसी को भी इसकी सुविधा का एक्सेस मिल जाता है, तो +उनके पास आपके वॉलेट की "मास्टर कुंजी" होगी और + +12 +00:00:41.429 --> 00:00:45.190 +वे आपके सारे धन को आसानी से एक्सेस कर सकते हैं। + +13 +00:00:45.190 --> 00:00:50.109 +अपने MetaMask वॉलेट को सुरक्षित करने के लिए आप अपने +गुप्त रिकवरी फ्रेज़ को सुरक्षित रूप से सहेजना चाहेंगे। + +14 +00:00:50.109 --> 00:00:54.930 +आप इसे लिख सकते हैं, इसे कहीं छुपा सकते हैं, +इसे सेफ़ डिपोज़िट बॉक्स में रख सकते हैं + +15 +00:00:54.930 --> 00:00:57.729 +या सुरक्षित पासवर्ड मैनेजर का उपयोग कर सकते हैं। + +16 +00:00:57.729 --> 00:01:01.050 +कुछ उपयोगकर्ता अपने +फ्रेज़ को धातु की प्लेट पर भी उकेर कर रखते हैं! + +17 +00:01:01.050 --> 00:01:04.440 +यदि आप अपना गुप्त रिकवरी फ्रेज़ खो देते हैं, तो +कोई भी, यहां तक कि MetaMask की टीम भी, + +18 +00:01:04.440 --> 00:01:07.820 +आपके वॉलेट को पुनर्प्राप्त करने में आपकी +सहायता नहीं कर सकती है। + +19 +00:01:07.820 --> 00:01:12.072 +यदि आपने अपना गुप्त रिकवरी फ्रेज़ +लिखा नहीं है और इसे कहीं सुरक्षित संग्रहीत नहीं किया है, + +20 +00:01:12.072 --> 00:01:15.492 +तो अभी करें। हम इंतजार करेंगे। + +21 +00:01:15.500 --> 00:01:20.780 +और याद रखें, कभी भी अपना गुप्त रिकवरी फ्रेज़ +किसी के साथ साझा न करें: हमसे भी नहीं। + +22 +00:01:20.780 --> 00:01:24.910 +यदि कोई आपसे कभी भी इसे मांगता है, तो +वे आपके साथ धोखाधड़ी करने की कोशिश कर सकते हैं। + +23 +00:01:24.910 --> 00:01:26.250 +बस इतना ही! + +24 +00:01:26.250 --> 00:01:31.020 +अब आपको पता चल गया है कि गुप्त रिकवरी फ्रेज़ क्या है +और अपने वॉलेट को कैसे सकुशल और सुरक्षित रखा जाए। diff --git a/app/images/videos/recovery-onboarding/subtitles/id.vtt b/app/images/videos/recovery-onboarding/subtitles/id.vtt new file mode 100644 index 000000000..94761496b --- /dev/null +++ b/app/images/videos/recovery-onboarding/subtitles/id.vtt @@ -0,0 +1,115 @@ +WEBVTT + +1 +00:00:00.780 --> 00:00:04.580 +MetaMask adalah cara baru untuk terhubung +ke situs dan aplikasi. + +2 +00:00:04.580 --> 00:00:08.860 +Di situs web tradisional, database sentral +atau bank bertanggung jawab untuk mengontrol dan + +3 +00:00:08.860 --> 00:00:10.179 +memulihkan akun Anda. + +4 +00:00:10.179 --> 00:00:15.050 +Tetapi di MetaMask, semua kuasa milik +pemegang kunci induk. + +5 +00:00:15.050 --> 00:00:18.460 +Siapa pun yang memegang kunci tersebut, akan mengontrol akun. + +6 +00:00:18.460 --> 00:00:21.110 +Frasa pemulihan rahasia +adalah "kunci induk" Anda. + +7 +00:00:21.110 --> 00:00:26.070 +Ini adalah rangkaian 12 kata yang dibuat +saat Anda menyiapkan MetaMask pertama kali, yang memungkinkan + +8 +00:00:26.070 --> 00:00:30.120 +Anda memulihkan dompet dan dana jika Anda +kehilangan akses. + +9 +00:00:30.120 --> 00:00:33.451 +Penting agar Anda mengamankan +dompet Anda dengan menjaga + +10 +00:00:33.451 --> 00:00:37.510 +frasa pemulihan rahasia +Anda dengan sangat aman dan sangat rahasia. + +11 +00:00:37.510 --> 00:00:41.429 +Jika seseorang mendapatkan aksesnya, mereka akan memiliki +"kunci induk" ke dompet Anda dan dapat + +12 +00:00:41.429 --> 00:00:45.190 +mengakses secara bebas dan mengambil semua dana Anda. + +13 +00:00:45.190 --> 00:00:50.109 +Untuk mengamankan dompet MetaMask, Anda pasti ingin +menyimpan frasa pemulihan rahasia Anda secara aman. + +14 +00:00:50.109 --> 00:00:54.930 +Anda dapat menuliskannya, menyembunyikannya di suatu tempat, +menempatkannya di kotak deposit yang aman + +15 +00:00:54.930 --> 00:00:57.729 +atau menggunakan pengelola kata sandi yang aman. + +16 +00:00:57.729 --> 00:01:01.050 +Beberapa pengguna bahkan mengukir frasa +mereka pada pelat logam! + +17 +00:01:01.050 --> 00:01:04.440 +Tidak ada seorang pun, bahkan tidak juga tim +di MetaMask, dapat membantu Anda + +18 +00:01:04.440 --> 00:01:07.820 +memulihkan dompet Anda jika Anda menghilangkan +frasa pemulihan rahasia Anda. + +19 +00:01:07.820 --> 00:01:12.072 +Jika belum menuliskan frasa pemulihan rahasia Anda +dan menyimpannya di suatu tempat yang aman, + +20 +00:01:12.072 --> 00:01:15.492 +lakukan sekarang. Kami akan menunggu. + +21 +00:01:15.500 --> 00:01:20.780 +Dan ingat, jangan membagikan frasa pemulihan rahasia +Anda kepada siapa pun: bahkan tidak kepada kami. + +22 +00:01:20.780 --> 00:01:24.910 +Jika ada yang menanyakannya, +mereka akan mencoba menipu Anda. + +23 +00:01:24.910 --> 00:01:26.250 +Begitulah! + +24 +00:01:26.250 --> 00:01:31.020 +Sekarang, Anda tahu apa itu frasa pemulihan rahasia +dan cara menjaga dompet Anda tetap aman. diff --git a/app/images/videos/recovery-onboarding/subtitles/ja.vtt b/app/images/videos/recovery-onboarding/subtitles/ja.vtt new file mode 100644 index 000000000..e99b3b7a4 --- /dev/null +++ b/app/images/videos/recovery-onboarding/subtitles/ja.vtt @@ -0,0 +1,115 @@ +WEBVTT + +1 +00:00:00.780 --> 00:00:04.580 +MetaMask は +をサイトとアプリケーションにつなぐ新たな方法です。 + +2 +00:00:04.580 --> 00:00:08.860 +従来のウェブサイト上では、中央データベース +または銀行がアカウントの制御と + +3 +00:00:08.860 --> 00:00:10.179 +回復の責任を負います。 + +4 +00:00:10.179 --> 00:00:15.050 +しかし、MetaMask 上では、全ての権限は +マスターキーの保持者に属します。 + +5 +00:00:15.050 --> 00:00:18.460 +当該のキーの保持者が、アカウントを制御します。 + +6 +00:00:18.460 --> 00:00:21.110 +あなたのシークレット リカバリー フレーズ +があなたの「マスターキー」です。 + +7 +00:00:21.110 --> 00:00:26.070 +これは一連の 12 の単語で +あなたが最初に MetaMask を設定した際に自動生成され、これにより + +8 +00:00:26.070 --> 00:00:30.120 +あなたは万が一アクセス出来なくなった場合に +ウォレットと資金を復元できます。 + +9 +00:00:30.120 --> 00:00:33.451 +ウォレットの安全性を確保することは非常に重要 +であり、あなたの + +10 +00:00:33.451 --> 00:00:37.510 +シークレット リカバリー フレーズ +を非常に安全かつ秘密に保つことで実現します。 + +11 +00:00:37.510 --> 00:00:41.429 +誰かがそれにアクセスすれば、彼らは +あなたのウォレットの「マスターキー」を得て、 + +12 +00:00:41.429 --> 00:00:45.190 +あなたの資金に自由にアクセスして全てを奪えます。 + +13 +00:00:45.190 --> 00:00:50.109 +MetaMask ウォレットの安全性を確保するため +あなたは シークレット リカバリー フレーズを安全に保存したくなるでしょう。 + +14 +00:00:50.109 --> 00:00:54.930 +それを書き留めたり、どこかへ隠したり、 +セーフティボックスに入れたり + +15 +00:00:54.930 --> 00:00:57.729 +または安全確保のためのパスワードマネジャーを使用できます。 + +16 +00:00:57.729 --> 00:01:01.050 +自分たちの +フレーズをメタルプレートに彫るユーザーさえいます! + +17 +00:01:01.050 --> 00:01:04.440 +何者も、 +MetaMask のチームのメンバーですら、あなたが + +18 +00:01:04.440 --> 00:01:07.820 +シークレット リカバリー フレーズを無くしたら +あなたのウォレットを復元する手助けはできません。 + +19 +00:01:07.820 --> 00:01:12.072 +あなたがシークレット リカバリー +フレーズを書き留め安全な場所に保管していないのならば、 + +20 +00:01:12.072 --> 00:01:15.492 +ぜひ今それを実行してください。お待ちしております。 + +21 +00:01:15.500 --> 00:01:20.780 +さらに、あなたのシークレット リカバリー +フレーズを誰とも決して共有しないことを忘れないでください。私たちでさえも。 + +22 +00:01:20.780 --> 00:01:24.910 +それを尋ねる者がいたら、 +彼らはあなたを騙そうとしているのです。 + +23 +00:01:24.910 --> 00:01:26.250 +以上です! + +24 +00:01:26.250 --> 00:01:31.020 +これでシークレット リカバリ フレーズ +が何であるか、あなたのウォレットと資金の安全を確保する方法が判りました。 diff --git a/app/images/videos/recovery-onboarding/subtitles/ko.vtt b/app/images/videos/recovery-onboarding/subtitles/ko.vtt new file mode 100644 index 000000000..68c04d1ae --- /dev/null +++ b/app/images/videos/recovery-onboarding/subtitles/ko.vtt @@ -0,0 +1,115 @@ +WEBVTT + +1 +00:00:00.780 --> 00:00:04.580 +MetaMask는 사이트와 애플리케이션에 +연결할 수 있는 새로운 방법입니다. + +2 +00:00:04.580 --> 00:00:08.860 +전통적인 웹사이트에서는 중앙 데이터베이스 +또는 은행에게 계정을 제어 및 + +3 +00:00:08.860 --> 00:00:10.179 +복구할 책임이 있습니다. + +4 +00:00:10.179 --> 00:00:15.050 +하지만 MetaMask에서는 모든 권한이 +마스터 키의 소유자에게 있습니다. + +5 +00:00:15.050 --> 00:00:18.460 +키를 보유하고 있는 사람은 계정을 제어합니다. + +6 +00:00:18.460 --> 00:00:21.110 +계정 시드 구문은 +"마스터 키"입니다. + +7 +00:00:21.110 --> 00:00:26.070 +먼저 MetaMask를 설정하면, 일련의 +12단어가 생성되어, + +8 +00:00:26.070 --> 00:00:30.120 +접근 권한을 상실했을 때 지갑과 +자금을 복구할 수 있습니다. + +9 +00:00:30.120 --> 00:00:33.451 +계정 시드 구문을 +안전하게 비밀을 + +10 +00:00:33.451 --> 00:00:37.510 +유지하여 지갑을 안전하게 +지키는 것이 중요합니다. + +11 +00:00:37.510 --> 00:00:41.429 +계정 시드 구문에 액세스하는 사람에게는 +지갑에 대한 "마스터 키"가 있어 자유롭게 + +12 +00:00:41.429 --> 00:00:45.190 +액세스하여 모든 자금을 가져갈 수 있습니다. + +13 +00:00:45.190 --> 00:00:50.109 +MetaMask 지갑을 안전하게 보호하려면, 계정 시드 +구문을 저장할 수 있습니다. + +14 +00:00:50.109 --> 00:00:54.930 +계정 시드 구문을 적어서 어딘가에 숨겨두거나 +대여 금고에 두거나 + +15 +00:00:54.930 --> 00:00:57.729 +보안 암호 관리자를 사용할 수 있습니다. + +16 +00:00:57.729 --> 00:01:01.050 +일부 사용자는 자신의 구문을 +금속판에 새겨두기도 합니다! + +17 +00:01:01.050 --> 00:01:04.440 +계정 시드 구문을 잊으면, +MetaMask의 팀이라고 + +18 +00:01:04.440 --> 00:01:07.820 +해도 지갑을 +복구할 수 없습니다. + +19 +00:01:07.820 --> 00:01:12.072 +게정 시드 구문을 적어두지 +않으면, 안전한 장소에 + +20 +00:01:12.072 --> 00:01:15.492 +보관하십시오. 기다리겠습니다. + +21 +00:01:15.500 --> 00:01:20.780 +다른 사람과 계정 시드 구문을 +고유하면 안 됩니다. 당사하고도 공유하지 마십시오. + +22 +00:01:20.780 --> 00:01:24.910 +계정 시드 구문을 요청하는 사람은 +사기를 치려고 하는 것입니다. + +23 +00:01:24.910 --> 00:01:26.250 +이제 다 됐습니다. + +24 +00:01:26.250 --> 00:01:31.020 +이제 여러분은 계정 시드 구문이 무엇이고 +지갑을 안전하게 보관하는 방법을 알고 있습니다. diff --git a/app/images/videos/recovery-onboarding/subtitles/pt.vtt b/app/images/videos/recovery-onboarding/subtitles/pt.vtt new file mode 100644 index 000000000..bebef88e1 --- /dev/null +++ b/app/images/videos/recovery-onboarding/subtitles/pt.vtt @@ -0,0 +1,115 @@ +WEBVTT + +1 +00:00:00.780 --> 00:00:04.580 +O MetaMask é um novo jeito de se conectar +a sites e aplicativos. + +2 +00:00:04.580 --> 00:00:08.860 +Em websites tradicionais, um banco ou base de dados central +é responsável por controlar e + +3 +00:00:08.860 --> 00:00:10.179 +recuperar as suas contas. + +4 +00:00:10.179 --> 00:00:15.050 +Mas, no MetaMask, todo o poder pertence +ao titular de uma chave-mestra. + +5 +00:00:15.050 --> 00:00:18.460 +Quem quer que detenha a chave controla as contas. + +6 +00:00:18.460 --> 00:00:21.110 +A sua frase de recuperação secreta +é a sua "chave-mestra". + +7 +00:00:21.110 --> 00:00:26.070 +É uma série de 12 palavras que são geradas +quando você configura o MetaMask na primeira vez, o que permite que + +8 +00:00:26.070 --> 00:00:30.120 +você recupere a sua carteira e recursos, caso você +venha a perder o acesso. + +9 +00:00:30.120 --> 00:00:33.451 +É importante que você mantenha protegida +a sua carteira ao manter a sua + +10 +00:00:33.451 --> 00:00:37.510 +frase de recuperação secreta +muito segura e muito secreta. + +11 +00:00:37.510 --> 00:00:41.429 +Caso alguém obtenha acesso a ela, essa pessoa terá +a "chave-mestra" para a sua carteira e poderá + +12 +00:00:41.429 --> 00:00:45.190 +acessá-la livremente e tome todos os seus recursos. + +13 +00:00:45.190 --> 00:00:50.109 +A fim de proteger a sua carteira MetaMask, você desejará +manter em segurança a sua frase de recuperação secreta. + +14 +00:00:50.109 --> 00:00:54.930 +Você pode escrevê-la, escondê-la em algum lugar, +colocá-la em um cofre + +15 +00:00:54.930 --> 00:00:57.729 +ou usar um gerenciador de senhas seguras. + +16 +00:00:57.729 --> 00:01:01.050 +Alguns usuários até mesmo gravam sua +frase em uma placa de metal! + +17 +00:01:01.050 --> 00:01:04.440 +Ninguém, nem mesmo a equipe +na MetaMask, pode lhe ajudar + +18 +00:01:04.440 --> 00:01:07.820 +a recuperar a sua carteira, caso você perca +a sua frase de recuperação secreta. + +19 +00:01:07.820 --> 00:01:12.072 +Caso você não tenha escrito a sua frase de recuperação +secreta e a tenha armazenado em algum lugar seguro, + +20 +00:01:12.072 --> 00:01:15.492 +faça isso agora. Iremos aguardar. + +21 +00:01:15.500 --> 00:01:20.780 +E lembre-se de jamais compartilhar a sua frase de recuperação +secreta com ninguém: nem mesmo conosco. + +22 +00:01:20.780 --> 00:01:24.910 +Caso alguém venha a lhe pedir a sua frase de recuperação secreta, +essa pessoa está tentando dar um golpe em você. + +23 +00:01:24.910 --> 00:01:26.250 +É isso! + +24 +00:01:26.250 --> 00:01:31.020 +Agora, você sabe o que é uma frase de recuperação secreta +e como manter a sua carteira protegida e segura. diff --git a/app/images/videos/recovery-onboarding/subtitles/ru.vtt b/app/images/videos/recovery-onboarding/subtitles/ru.vtt new file mode 100644 index 000000000..46c165a6a --- /dev/null +++ b/app/images/videos/recovery-onboarding/subtitles/ru.vtt @@ -0,0 +1,115 @@ +WEBVTT + +1 +00:00:00.780 --> 00:00:04.580 +MetaMask — это новый способ подключения +к сайтам и приложениям. + +2 +00:00:04.580 --> 00:00:08.860 +На традиционных сайтах центральная база данных +или банк несет ответственность за контроль и + +3 +00:00:08.860 --> 00:00:10.179 +восстановление ваших счетов. + +4 +00:00:10.179 --> 00:00:15.050 +На MetaMask все полномочия находятся +в руках владельца мастер-ключа. + +5 +00:00:15.050 --> 00:00:18.460 +Тот, в чьих руках находится ключ, контролирует счета. + +6 +00:00:18.460 --> 00:00:21.110 +Ваша секретная фраза восстановления +— это ваш «мастер-ключ». + +7 +00:00:21.110 --> 00:00:26.070 +Это набор из 12 слов, которые генерируются +при первой настройке MetaMask, он позволяет + +8 +00:00:26.070 --> 00:00:30.120 +вам восстанавливать ваш кошелек и средства, если вы +теряете к ним доступ. + +9 +00:00:30.120 --> 00:00:33.451 +Важно, чтобы вы обезопасили +свой кошелек, храня вашу + +10 +00:00:33.451 --> 00:00:37.510 +секретную фразу восстановления +в очень надежном и тайном месте. + +11 +00:00:37.510 --> 00:00:41.429 +Если кто-то получит доступ к ней, у этого человека окажется в руках +«мастер-ключ» от вашего кошелька, и он сможет + +12 +00:00:41.429 --> 00:00:45.190 +распоряжаться им и завладеть всеми вашими средствами. + +13 +00:00:45.190 --> 00:00:50.109 +Чтобы обезопасить ваш кошелек MetaMask, +сохраните секретную фразу восстановления в безопасном месте. + +14 +00:00:50.109 --> 00:00:54.930 +Вы можете записать ее, спрятать ее где-то, +положить ее в банковский сейф + +15 +00:00:54.930 --> 00:00:57.729 +или воспользоваться безопасным диспетчером паролей. + +16 +00:00:57.729 --> 00:01:01.050 +Некоторые пользователи даже гравируют свою +фразу на металлической пластине! + +17 +00:01:01.050 --> 00:01:04.440 +Никто, даже команда +MetaMask, не сможет помочь вам + +18 +00:01:04.440 --> 00:01:07.820 +восстановить ваш кошелек, если вы потеряете +вашу секретную фразу восстановления. + +19 +00:01:07.820 --> 00:01:12.072 +Если вы еще не записали секретную фразу +восстановления и не поместили ее в надежное место, + +20 +00:01:12.072 --> 00:01:15.492 +сделайте это сейчас. Мы подождем. + +21 +00:01:15.500 --> 00:01:20.780 +И помните, никогда не сообщайте свою секретную фразу +восстановления никому: даже нам. + +22 +00:01:20.780 --> 00:01:24.910 +Если кто-нибудь когда-либо спросит у вас ее, +этот человек пытается вас обмануть. + +23 +00:01:24.910 --> 00:01:26.250 +Вот и все! + +24 +00:01:26.250 --> 00:01:31.020 +Теперь вы знаете, что такое секретная фраза восстановления +и как обезопасить ваш кошелек. diff --git a/app/images/videos/recovery-onboarding/subtitles/tl.vtt b/app/images/videos/recovery-onboarding/subtitles/tl.vtt new file mode 100644 index 000000000..cfb3ebca7 --- /dev/null +++ b/app/images/videos/recovery-onboarding/subtitles/tl.vtt @@ -0,0 +1,115 @@ +WEBVTT + +1 +00:00:00.780 --> 00:00:04.580 +Ang MetaMask ay isang bagong paraan para kumonekta +sa mga site at application. + +2 +00:00:04.580 --> 00:00:08.860 +Sa mga tradisyonal na website, ang isang central database +o bangko ang magiging responsable sa pagkontrol at + +3 +00:00:08.860 --> 00:00:10.179 +pag-recover ng iyong mga account. + +4 +00:00:10.179 --> 00:00:15.050 +Pero sa MetaMask, ang lahat ng kakayahan ay nasa +may hawak ng master key. + +5 +00:00:15.050 --> 00:00:18.460 +Kung sino man ang may hawak ng key, siya ang magkokontrol sa mga account. + +6 +00:00:18.460 --> 00:00:21.110 +Ang iyong lihim na recovery phrase +ay ang iyong "master key". + +7 +00:00:21.110 --> 00:00:26.070 +Isa itong 12 salita na nagagawa +sa unang pagkakataong i-set up mo ang MetaMask, na magbibigay-daan sa iyo + +8 +00:00:26.070 --> 00:00:30.120 +na maibalik ang iyong wallet at pera kung sakaling +mawalan ka ng access. + +9 +00:00:30.120 --> 00:00:33.451 +Mahalagang i-secure +ang iyong wallet sa pamamagitan ng pagpapanatiling sobrang ligtas at walang nakakaalam ng iyong + +10 +00:00:33.451 --> 00:00:37.510 +lihim na recovery phrase +. + +11 +00:00:37.510 --> 00:00:41.429 +Kung may ibang taong makaka-access nito, makukuha nila +ang "master key" sa iyong wallet at + +12 +00:00:41.429 --> 00:00:45.190 +madali nilang maa-access at makukuha ang lahat ng pera mo. + +13 +00:00:45.190 --> 00:00:50.109 +Para ma-secure ang iyong MetaMask wallet, +ligtas na i-save ang iyong lihim na recovery phrase. + +14 +00:00:50.109 --> 00:00:54.930 +Puwede mo itong isulat, itago, +ilagay sa isang safe deposit box + +15 +00:00:54.930 --> 00:00:57.729 +o kaya ay gumamit ng ligtas na password manager. + +16 +00:00:57.729 --> 00:01:01.050 +Ang ilang user nga ay inuukit pa ang kanilang +phrase sa isang metal plate! + +17 +00:01:01.050 --> 00:01:04.440 +Walang sinuman, maging ang team +sa MetaMask, ang makakatulong sa iyong + +18 +00:01:04.440 --> 00:01:07.820 +maibalik ang wallet mo kung maiwawala mo +iyong lihim na recovery phrase. + +19 +00:01:07.820 --> 00:01:12.072 +Kung hindi mo pa naisusulat ang iyong lihim na recovery +phrase at hindi pa naitatago sa ligtas na lugar, + +20 +00:01:12.072 --> 00:01:15.492 +gawin mo na ngayon. Hihintayin ka namin. + +21 +00:01:15.500 --> 00:01:20.780 +At tandaan, huwag kailanman ipaalam sa iba ang iyong lihim na recovery +phrase: maging sa amin. + +22 +00:01:20.780 --> 00:01:24.910 +Kung may magtatanong man sa iyo, +sinusubukan ka nilang i-scam. + +23 +00:01:24.910 --> 00:01:26.250 +´Yun lang! + +24 +00:01:26.250 --> 00:01:31.020 +Ngayon ay alam mo na kung ano ang lihim na recovery phrase +at kung paano mapapanatiling ligtas ang iyong wallet. diff --git a/app/images/videos/recovery-onboarding/subtitles/vi.vtt b/app/images/videos/recovery-onboarding/subtitles/vi.vtt new file mode 100644 index 000000000..a05e97d85 --- /dev/null +++ b/app/images/videos/recovery-onboarding/subtitles/vi.vtt @@ -0,0 +1,115 @@ +WEBVTT + +1 +00:00:00.780 --> 00:00:04.580 +MetaMask là cách thức mới để kết nối +với các trang web và ứng dụng. + +2 +00:00:04.580 --> 00:00:08.860 +Trên các trang web truyền thống, một cơ sở dữ liệu trung tâm +hay ngân hàng sẽ chịu trách nhiệm kiểm soát và + +3 +00:00:08.860 --> 00:00:10.179 +khôi phục các tài khoản của bạn. + +4 +00:00:10.179 --> 00:00:15.050 +Tuy nhiên, trên MetaMask, toàn bộ quyền sẽ thuộc về +người nắm giữ khóa chính. + +5 +00:00:15.050 --> 00:00:18.460 +Người có khóa chính sẽ kiểm soát được tài khoản. + +6 +00:00:18.460 --> 00:00:21.110 +Cụm mật khẩu khôi phục bí mật +là “khóa chính” của bạn. + +7 +00:00:21.110 --> 00:00:26.070 +Đây là chuỗi gồm 12 từ được tạo +vào lần đầu tiên bạn thiết lập MetaMask, chuỗi này cho phép + +8 +00:00:26.070 --> 00:00:30.120 +bạn khôi phục ví và tiền của mình nếu +bạn bị mất quyền truy cập. + +9 +00:00:30.120 --> 00:00:33.451 +Bạn cần phải bảo vệ an toàn cho +ví của mình bằng cách lưu giữ + +10 +00:00:33.451 --> 00:00:37.510 +cụm mật khẩu khôi phục bí mật +thật an toàn và bí mật. + +11 +00:00:37.510 --> 00:00:41.429 +Nếu ai đó có được cụm mật khẩu khôi phục bí mật của bạn thì người đó sẽ có +“khóa chính” cho ví của bạn và có thể + +12 +00:00:41.429 --> 00:00:45.190 +tự do truy cập và lấy toàn bộ tiền của bạn. + +13 +00:00:45.190 --> 00:00:50.109 +Để bảo vệ an toàn cho ví MetaMask, bạn cần +lưu giữ cụm mật khẩu khôi phục bí mật một cách an toàn. + +14 +00:00:50.109 --> 00:00:54.930 +Bạn có thể chép lại và giấu ở một nơi nào đó, +cất trong hộp ký gửi an toàn + +15 +00:00:54.930 --> 00:00:57.729 +hoặc dùng một trình quản lý mật khẩu an toàn. + +16 +00:00:57.729 --> 00:01:01.050 +Một số người dùng thậm chí còn khắc +cụm mật khẩu của họ lên một tấm kim loại! + +17 +00:01:01.050 --> 00:01:04.440 +Không một ai, kể cả đội ngũ +tại MetaMask, có thể giúp bạn + +18 +00:01:04.440 --> 00:01:07.820 +khôi phục lại ví nếu bạn đánh mất +cụm mật khẩu khôi phục bí mật của mình. + +19 +00:01:07.820 --> 00:01:12.072 +Nếu chưa ghi lại cụm mật khẩu khôi phục bí mật +của mình và lưu giữ ở nơi an toàn, + +20 +00:01:12.072 --> 00:01:15.492 +thì bạn hãy thực hiện ngay bây giờ. Chúng tôi sẽ chờ bạn. + +21 +00:01:15.500 --> 00:01:20.780 +Và đừng bao giờ chia sẻ cụm mật khẩu khôi phục +bí mật với bất kỳ ai: kể cả chúng tôi. + +22 +00:01:20.780 --> 00:01:24.910 +Nếu ai đó hỏi bạn cụm mật khẩu khôi phục bí mật, +thì họ đang cố gắng lừa đảo bạn. + +23 +00:01:24.910 --> 00:01:26.250 +Xin hãy ghi nhớ! + +24 +00:01:26.250 --> 00:01:31.020 +Bây giờ bạn đã biết cụm mật khẩu khôi phục bí mật +là gì và cách bảo vệ ví của bạn an toàn và bảo mật. diff --git a/app/manifest/_beta_modifications.json b/app/manifest/_beta_modifications.json new file mode 100644 index 000000000..a505eb7f4 --- /dev/null +++ b/app/manifest/_beta_modifications.json @@ -0,0 +1,27 @@ +{ + "browser_action": { + "default_icon": { + "16": "images/icon-16.png", + "19": "images/icon-19.png", + "32": "images/icon-32.png", + "38": "images/icon-38.png", + "64": "images/icon-64.png", + "128": "images/icon-128.png", + "512": "images/icon-512.png" + }, + "default_title": "MetaMask Beta" + }, + "icons": { + "16": "images/icon-16.png", + "19": "images/icon-19.png", + "32": "images/icon-32.png", + "38": "images/icon-38.png", + "48": "images/icon-48.png", + "64": "images/icon-64.png", + "128": "images/icon-128.png", + "512": "images/icon-512.png" + }, + "name": "__MSG_appName__ Beta", + "short_name": "__MSG_appName__ Beta", + "version": "" +} diff --git a/app/manifest/chrome.json b/app/manifest/chrome.json index 281c847a4..e4bb01cdd 100644 --- a/app/manifest/chrome.json +++ b/app/manifest/chrome.json @@ -3,5 +3,5 @@ "matches": ["https://metamask.io/*"], "ids": ["*"] }, - "minimum_chrome_version": "63" + "minimum_chrome_version": "66" } diff --git a/app/phishing.html b/app/phishing.html index 5460447b9..dd180fb77 100644 --- a/app/phishing.html +++ b/app/phishing.html @@ -51,7 +51,7 @@ Ethereum Phishing Detector. Domains on these warning lists may include outright malicious websites and legitimate websites that have been compromised by a malicious actor.

-

To read more about this site please search for the domain on CryptoScamDB.

+

To read more about this site please search for the domain on CryptoScamDB.

Note that this warning list is compiled on a voluntary basis. This list may be inaccurate or incomplete. Just because a domain does not appear on this list is not an implicit guarantee of that domain's safety. @@ -60,7 +60,7 @@

If you think this domain is incorrectly flagged or if a blocked legitimate website has resolved its security issues, - please file an issue. + please file an issue.

diff --git a/app/scripts/background.js b/app/scripts/background.js index a8ff096f6..d8d99150f 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -2,9 +2,6 @@ * @file The entry point for the web extension singleton process. */ -// polyfills -import 'abortcontroller-polyfill/dist/polyfill-patch-fetch'; - import endOfStream from 'end-of-stream'; import pump from 'pump'; import debounce from 'debounce-stream'; diff --git a/app/scripts/controllers/detect-tokens.js b/app/scripts/controllers/detect-tokens.js index c9ea77f2d..246114657 100644 --- a/app/scripts/controllers/detect-tokens.js +++ b/app/scripts/controllers/detect-tokens.js @@ -1,10 +1,9 @@ import Web3 from 'web3'; -import contracts from '@metamask/contract-metadata'; 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'; +import { isEqualCaseInsensitive } from '../../../ui/helpers/utils/util'; // By default, poll every 3 minutes const DEFAULT_INTERVAL = MINUTE * 3; @@ -24,57 +23,37 @@ export default class DetectTokensController { preferences, network, keyringMemStore, + tokenList, + tokensController, } = {}) { + this.tokensController = tokensController; this.preferences = preferences; this.interval = interval; this.network = network; this.keyringMemStore = keyringMemStore; - } + this.tokenList = tokenList; + this.selectedAddress = this.preferences?.store.getState().selectedAddress; + this.tokenAddresses = this.tokensController?.state.tokens.map((token) => { + return token.address; + }); + this.hiddenTokens = this.tokensController?.state.ignoredTokens; - /** - * For each token in @metamask/contract-metadata, find check selectedAddress balance. - */ - async detectNewTokens() { - if (!this.isActive) { - return; - } - if (this._network.store.getState().provider.chainId !== MAINNET_CHAIN_ID) { - return; - } - - const tokensToDetect = []; - this.web3.setProvider(this._network._provider); - for (const contractAddress in contracts) { + preferences?.store.subscribe(({ selectedAddress, useTokenDetection }) => { if ( - contracts[contractAddress].erc20 && - !this.tokenAddresses.includes(contractAddress.toLowerCase()) && - !this.hiddenTokens.includes(contractAddress.toLowerCase()) + this.selectedAddress !== selectedAddress || + this.useTokenDetection !== useTokenDetection ) { - tokensToDetect.push(contractAddress); - } - } - - let result; - try { - result = await this._getTokenBalances(tokensToDetect); - } catch (error) { - warn( - `MetaMask - DetectTokensController single call balance fetch failed`, - error, - ); - return; - } - - tokensToDetect.forEach((tokenAddress, index) => { - const balance = result[index]; - if (balance && !balance.isZero()) { - this._preferences.addToken( - tokenAddress, - contracts[tokenAddress].symbol, - contracts[tokenAddress].decimals, - ); + this.selectedAddress = selectedAddress; + this.useTokenDetection = useTokenDetection; + this.restartTokenDetection(); } }); + tokensController?.subscribe(({ tokens = [], ignoredTokens = [] }) => { + this.tokenAddresses = tokens.map((token) => { + return token.address; + }); + this.hiddenTokens = ignoredTokens; + }); } async _getTokenBalances(tokens) { @@ -91,6 +70,66 @@ export default class DetectTokensController { }); } + /** + * For each token in the tokenlist provided by the TokenListController, check selectedAddress balance. + */ + async detectNewTokens() { + if (!this.isActive) { + return; + } + + const { tokenList } = this._tokenList.state; + if (Object.keys(tokenList).length === 0) { + return; + } + + const tokensToDetect = []; + this.web3.setProvider(this._network._provider); + for (const tokenAddress in tokenList) { + if ( + !this.tokenAddresses.find((address) => + isEqualCaseInsensitive(address, tokenAddress), + ) && + !this.hiddenTokens.find((address) => + isEqualCaseInsensitive(address, tokenAddress), + ) + ) { + tokensToDetect.push(tokenAddress); + } + } + const sliceOfTokensToDetect = [ + tokensToDetect.slice(0, 1000), + tokensToDetect.slice(1000, tokensToDetect.length - 1), + ]; + for (const tokensSlice of sliceOfTokensToDetect) { + let result; + try { + result = await this._getTokenBalances(tokensSlice); + } catch (error) { + warn( + `MetaMask - DetectTokensController single call balance fetch failed`, + error, + ); + return; + } + + const tokensWithBalance = tokensSlice.filter((_, index) => { + const balance = result[index]; + return balance && !balance.isZero(); + }); + + await Promise.all( + tokensWithBalance.map((tokenAddress) => { + return this.tokensController.addToken( + tokenAddress, + tokenList[tokenAddress].symbol, + tokenList[tokenAddress].decimals, + ); + }), + ); + } + } + /** * Restart token detection polling period and call detectNewTokens * in case of address change or user session initialization. @@ -118,34 +157,6 @@ export default class DetectTokensController { }, interval); } - /** - * In setter when selectedAddress is changed, detectNewTokens and restart polling - * @type {Object} - */ - set preferences(preferences) { - if (!preferences) { - return; - } - this._preferences = preferences; - const currentTokens = preferences.store.getState().tokens; - this.tokenAddresses = currentTokens - ? currentTokens.map((token) => token.address) - : []; - this.hiddenTokens = preferences.store.getState().hiddenTokens; - preferences.store.subscribe(({ tokens = [], hiddenTokens = [] }) => { - this.tokenAddresses = tokens.map((token) => { - return token.address; - }); - this.hiddenTokens = hiddenTokens; - }); - preferences.store.subscribe(({ selectedAddress }) => { - if (this.selectedAddress !== selectedAddress) { - this.selectedAddress = selectedAddress; - this.restartTokenDetection(); - } - }); - } - /** * @type {Object} */ @@ -176,6 +187,16 @@ export default class DetectTokensController { }); } + /** + * @type {Object} + */ + set tokenList(tokenList) { + if (!tokenList) { + return; + } + this._tokenList = tokenList; + } + /** * Internal isActive state * @type {Object} diff --git a/app/scripts/controllers/detect-tokens.test.js b/app/scripts/controllers/detect-tokens.test.js index 7bd788271..4a739aff1 100644 --- a/app/scripts/controllers/detect-tokens.test.js +++ b/app/scripts/controllers/detect-tokens.test.js @@ -1,17 +1,23 @@ import { strict as assert } from 'assert'; import sinon from 'sinon'; +import nock from 'nock'; import { ObservableStore } from '@metamask/obs-store'; -import contracts from '@metamask/contract-metadata'; import BigNumber from 'bignumber.js'; - +import { + ControllerMessenger, + TokenListController, + TokensController, +} from '@metamask/controllers'; import { MAINNET, ROPSTEN } from '../../../shared/constants/network'; +import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils'; import DetectTokensController from './detect-tokens'; import NetworkController from './network'; import PreferencesController from './preferences'; describe('DetectTokensController', function () { + let tokenListController; const sandbox = sinon.createSandbox(); - let keyringMemStore, network, preferences, provider; + let keyringMemStore, network, preferences, provider, tokensController; const noop = () => undefined; @@ -26,6 +32,12 @@ describe('DetectTokensController', function () { network.initializeProvider(networkControllerProviderConfig); provider = network.getProviderAndBlockTracker().provider; preferences = new PreferencesController({ network, provider }); + tokensController = new TokensController({ + onPreferencesStateChange: preferences.store.subscribe.bind( + preferences.store, + ), + onNetworkStateChange: network.store.subscribe.bind(network.store), + }); preferences.setAddresses([ '0x7e57e2', '0xbc86727e770de68b1060c91f6bb6945c73e10388', @@ -34,8 +46,92 @@ describe('DetectTokensController', function () { .stub(network, 'getLatestBlock') .callsFake(() => Promise.resolve({})); sandbox - .stub(preferences, '_detectIsERC721') + .stub(tokensController, '_instantiateNewEthersProvider') + .returns(null); + sandbox + .stub(tokensController, '_detectIsERC721') .returns(Promise.resolve(false)); + nock('https://token-api.metaswap.codefi.network') + .get(`/tokens/1`) + .reply(200, [ + { + address: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', + symbol: 'SNX', + decimals: 18, + occurrences: 11, + aggregators: [ + 'paraswap', + 'pmm', + 'airswapLight', + 'zeroEx', + 'bancor', + 'coinGecko', + 'zapper', + 'kleros', + 'zerion', + 'cmc', + 'oneInch', + ], + name: 'Synthetix', + iconUrl: 'https://airswap-token-images.s3.amazonaws.com/SNX.png', + }, + { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + symbol: 'LINK', + decimals: 18, + occurrences: 11, + aggregators: [ + 'paraswap', + 'pmm', + 'airswapLight', + 'zeroEx', + 'bancor', + 'coinGecko', + 'zapper', + 'kleros', + 'zerion', + 'cmc', + 'oneInch', + ], + name: 'Chainlink', + iconUrl: 'https://s3.amazonaws.com/airswap-token-images/LINK.png', + }, + { + address: '0x1f573d6fb3f13d689ff844b4ce37794d79a7ff1c', + symbol: 'BNT', + decimals: 18, + occurrences: 11, + aggregators: [ + 'paraswap', + 'pmm', + 'airswapLight', + 'zeroEx', + 'bancor', + 'coinGecko', + 'zapper', + 'kleros', + 'zerion', + 'cmc', + 'oneInch', + ], + name: 'Bancor', + iconUrl: 'https://s3.amazonaws.com/airswap-token-images/BNT.png', + }, + ]) + .get(`/tokens/3`) + .reply(200, { error: 'ChainId 3 is not supported' }) + .persist(); + const tokenListMessenger = new ControllerMessenger().getRestricted({ + name: 'TokenListController', + }); + tokenListController = new TokenListController({ + chainId: '1', + useStaticTokenList: false, + onNetworkStateChange: sinon.spy(), + onPreferencesStateChange: sinon.spy(), + messenger: tokenListMessenger, + }); + await tokenListController.start(); }); after(function () { @@ -56,6 +152,8 @@ describe('DetectTokensController', function () { preferences, network, keyringMemStore, + tokenList: tokenListController, + tokensController, }); controller.isOpen = true; controller.isUnlocked = true; @@ -75,10 +173,23 @@ describe('DetectTokensController', function () { it('should not check tokens while on test network', async function () { sandbox.useFakeTimers(); network.setProviderType(ROPSTEN); + const tokenListMessengerRopsten = new ControllerMessenger().getRestricted({ + name: 'TokenListController', + }); + tokenListController = new TokenListController({ + chainId: '3', + useStaticTokenList: false, + onNetworkStateChange: sinon.spy(), + onPreferencesStateChange: sinon.spy(), + messenger: tokenListMessengerRopsten, + }); + await tokenListController.start(); const controller = new DetectTokensController({ preferences, network, keyringMemStore, + tokenList: tokenListController, + tokensController, }); controller.isOpen = true; controller.isUnlocked = true; @@ -96,24 +207,30 @@ describe('DetectTokensController', function () { preferences, network, keyringMemStore, + tokenList: tokenListController, + tokensController, }); controller.isOpen = true; controller.isUnlocked = true; - const contractAddresses = Object.keys(contracts); - const erc20ContractAddresses = contractAddresses.filter( - (contractAddress) => contracts[contractAddress].erc20 === true, - ); + const { tokenList } = tokenListController.state; + const erc20ContractAddresses = Object.keys(tokenList); const existingTokenAddress = erc20ContractAddresses[0]; - const existingToken = contracts[existingTokenAddress]; - await preferences.addToken( + const existingToken = tokenList[existingTokenAddress]; + await tokensController.addToken( existingTokenAddress, existingToken.symbol, existingToken.decimals, ); const tokenAddressToSkip = erc20ContractAddresses[1]; + const tokenToSkip = tokenList[tokenAddressToSkip]; + await tokensController.addToken( + tokenAddressToSkip, + tokenToSkip.symbol, + tokenToSkip.decimals, + ); sandbox .stub(controller, '_getTokenBalances') @@ -123,15 +240,15 @@ describe('DetectTokensController', function () { ), ); - await preferences.removeToken(tokenAddressToSkip); - + await tokensController.removeAndIgnoreToken(tokenAddressToSkip); await controller.detectNewTokens(); - assert.deepEqual(preferences.store.getState().tokens, [ + assert.deepEqual(tokensController.state.tokens, [ { - address: existingTokenAddress.toLowerCase(), + address: toChecksumHexAddress(existingTokenAddress), decimals: existingToken.decimals, symbol: existingToken.symbol, + image: undefined, isERC721: false, }, ]); @@ -144,34 +261,34 @@ describe('DetectTokensController', function () { preferences, network, keyringMemStore, + tokenList: tokenListController, + tokensController, }); controller.isOpen = true; controller.isUnlocked = true; - const contractAddresses = Object.keys(contracts); - const erc20ContractAddresses = contractAddresses.filter( - (contractAddress) => contracts[contractAddress].erc20 === true, - ); + const { tokenList } = tokenListController.state; + const erc20ContractAddresses = Object.keys(tokenList); const existingTokenAddress = erc20ContractAddresses[0]; - const existingToken = contracts[existingTokenAddress]; - await preferences.addToken( + const existingToken = tokenList[existingTokenAddress]; + await tokensController.addToken( existingTokenAddress, existingToken.symbol, existingToken.decimals, ); const tokenAddressToAdd = erc20ContractAddresses[1]; - const tokenToAdd = contracts[tokenAddressToAdd]; + const tokenToAdd = tokenList[tokenAddressToAdd]; - const contractAddresssesToDetect = contractAddresses.filter( + const contractAddressesToDetect = erc20ContractAddresses.filter( (address) => address !== existingTokenAddress, ); - const indexOfTokenToAdd = contractAddresssesToDetect.indexOf( + const indexOfTokenToAdd = contractAddressesToDetect.indexOf( tokenAddressToAdd, ); + const balances = new Array(contractAddressesToDetect.length); - const balances = new Array(contractAddresssesToDetect.length); balances[indexOfTokenToAdd] = new BigNumber(10); sandbox @@ -179,18 +296,19 @@ describe('DetectTokensController', function () { .returns(Promise.resolve(balances)); await controller.detectNewTokens(); - - assert.deepEqual(preferences.store.getState().tokens, [ + assert.deepEqual(tokensController.state.tokens, [ { - address: existingTokenAddress.toLowerCase(), + address: toChecksumHexAddress(existingTokenAddress), decimals: existingToken.decimals, symbol: existingToken.symbol, isERC721: false, + image: undefined, }, { - address: tokenAddressToAdd.toLowerCase(), + address: toChecksumHexAddress(tokenAddressToAdd), decimals: tokenToAdd.decimals, symbol: tokenToAdd.symbol, + image: undefined, isERC721: false, }, ]); @@ -203,34 +321,34 @@ describe('DetectTokensController', function () { preferences, network, keyringMemStore, + tokenList: tokenListController, + tokensController, }); controller.isOpen = true; controller.isUnlocked = true; - const contractAddresses = Object.keys(contracts); - const erc20ContractAddresses = contractAddresses.filter( - (contractAddress) => contracts[contractAddress].erc20 === true, - ); + const { tokenList } = tokenListController.state; + const erc20ContractAddresses = Object.keys(tokenList); const existingTokenAddress = erc20ContractAddresses[0]; - const existingToken = contracts[existingTokenAddress]; - await preferences.addToken( + const existingToken = tokenList[existingTokenAddress]; + await tokensController.addToken( existingTokenAddress, existingToken.symbol, existingToken.decimals, ); const tokenAddressToAdd = erc20ContractAddresses[1]; - const tokenToAdd = contracts[tokenAddressToAdd]; + const tokenToAdd = tokenList[tokenAddressToAdd]; - const contractAddresssesToDetect = contractAddresses.filter( + const contractAddressesToDetect = erc20ContractAddresses.filter( (address) => address !== existingTokenAddress, ); - const indexOfTokenToAdd = contractAddresssesToDetect.indexOf( + const indexOfTokenToAdd = contractAddressesToDetect.indexOf( tokenAddressToAdd, ); - const balances = new Array(contractAddresssesToDetect.length); + const balances = new Array(contractAddressesToDetect.length); balances[indexOfTokenToAdd] = new BigNumber(10); sandbox @@ -239,17 +357,19 @@ describe('DetectTokensController', function () { await controller.detectNewTokens(); - assert.deepEqual(preferences.store.getState().tokens, [ + assert.deepEqual(tokensController.state.tokens, [ { - address: existingTokenAddress.toLowerCase(), + address: toChecksumHexAddress(existingTokenAddress), decimals: existingToken.decimals, symbol: existingToken.symbol, + image: undefined, isERC721: false, }, { - address: tokenAddressToAdd.toLowerCase(), + address: toChecksumHexAddress(tokenAddressToAdd), decimals: tokenToAdd.decimals, symbol: tokenToAdd.symbol, + image: undefined, isERC721: false, }, ]); @@ -261,6 +381,8 @@ describe('DetectTokensController', function () { preferences, network, keyringMemStore, + tokenList: tokenListController, + tokensController, }); controller.isOpen = true; controller.isUnlocked = true; @@ -277,6 +399,8 @@ describe('DetectTokensController', function () { preferences, network, keyringMemStore, + tokenList: tokenListController, + tokensController, }); controller.isOpen = true; controller.selectedAddress = '0x0'; @@ -292,6 +416,8 @@ describe('DetectTokensController', function () { preferences, network, keyringMemStore, + tokenList: tokenListController, + tokensController, }); controller.isOpen = true; controller.isUnlocked = false; @@ -307,6 +433,7 @@ describe('DetectTokensController', function () { preferences, network, keyringMemStore, + tokensController, }); // trigger state update from preferences controller await preferences.setSelectedAddress( diff --git a/app/scripts/controllers/network/util.test.js b/app/scripts/controllers/network/util.test.js index 251812741..f8e03ff0f 100644 --- a/app/scripts/controllers/network/util.test.js +++ b/app/scripts/controllers/network/util.test.js @@ -1,5 +1,8 @@ import { strict as assert } from 'assert'; -import { TRANSACTION_STATUSES } from '../../../../shared/constants/transaction'; +import { + TRANSACTION_STATUSES, + TRANSACTION_TYPES, +} from '../../../../shared/constants/transaction'; import { formatTxMetaForRpcResult } from './util'; describe('network utils', function () { @@ -16,7 +19,7 @@ describe('network utils', function () { gas: '0x7b0d', nonce: '0x4b', }, - type: 'sentEther', + type: TRANSACTION_TYPES.SIMPLE_SEND, origin: 'other', chainId: '0x3', time: 1624408066355, @@ -63,7 +66,7 @@ describe('network utils', function () { gas: '0x7b0d', nonce: '0x4b', }, - type: 'sentEther', + type: TRANSACTION_TYPES.SIMPLE_SEND, origin: 'other', chainId: '0x3', time: 1624408066355, diff --git a/app/scripts/controllers/permissions/enums.js b/app/scripts/controllers/permissions/enums.js index 447040bed..4901140a1 100644 --- a/app/scripts/controllers/permissions/enums.js +++ b/app/scripts/controllers/permissions/enums.js @@ -40,6 +40,7 @@ export const SAFE_METHODS = [ 'eth_coinbase', 'eth_decrypt', 'eth_estimateGas', + 'eth_feeHistory', 'eth_gasPrice', 'eth_getBalance', 'eth_getBlockByHash', diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index bd2c91a09..60607d602 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -1,22 +1,12 @@ import { strict as assert } from 'assert'; import { ObservableStore } from '@metamask/obs-store'; -import { ethErrors } from 'eth-rpc-errors'; import { normalize as normalizeAddress } from 'eth-sig-util'; import { ethers } from 'ethers'; import log from 'loglevel'; -import abiERC721 from 'human-standard-collectible-abi'; -import contractsMap from '@metamask/contract-metadata'; -import { LISTED_CONTRACT_ADDRESSES } from '../../../shared/constants/tokens'; import { NETWORK_TYPE_TO_ID_MAP } from '../../../shared/constants/network'; import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils'; -import { - isValidHexAddress, - toChecksumHexAddress, -} from '../../../shared/modules/hexstring-utils'; import { NETWORK_EVENTS } from './network'; -const ERC721_INTERFACE_ID = '0x80ac58cd'; - export default class PreferencesController { /** * @@ -24,9 +14,6 @@ export default class PreferencesController { * @param {Object} opts - Overrides the defaults for the initial state of this.store * @property {Object} store The stored object containing a users preferences, stored in local storage * @property {Array} store.frequentRpcList A list of custom rpcs to provide the user - * @property {Array} store.tokens The tokens the user wants display in their token lists - * @property {Object} store.accountTokens The tokens stored per account and then per network type - * @property {Object} store.assetImages Contains assets objects related to assets added * @property {boolean} store.useBlockie The users preference for blockie identicons within the UI * @property {boolean} store.useNonceField The users preference for nonce field within the UI * @property {Object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the @@ -41,17 +28,14 @@ export default class PreferencesController { constructor(opts = {}) { const initState = { frequentRpcListDetail: [], - accountTokens: {}, - accountHiddenTokens: {}, - assetImages: {}, - tokens: [], - hiddenTokens: [], - suggestedTokens: {}, useBlockie: false, useNonceField: false, usePhishDetect: true, dismissSeedBackUpReminder: false, - useStaticTokenList: false, + + // set to true means the dynamic list from the API is being used + // set to false will be using the static list from contract-metadata + useTokenDetection: false, // WARNING: Do not use feature flags for security-sensitive things. // Feature flag toggling is available in the global namespace @@ -87,12 +71,6 @@ export default class PreferencesController { this.openPopup = opts.openPopup; this.migrateAddressBookState = opts.migrateAddressBookState; - this.network.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => { - const { tokens, hiddenTokens } = this._getTokenRelatedStates(); - this.ethersProvider = new ethers.providers.Web3Provider(opts.provider); - this._updateAccountTokens(tokens, this.getAssetImages(), hiddenTokens); - }); - this._subscribeToInfuraAvailability(); global.setPreference = (key, value) => { @@ -140,13 +118,13 @@ export default class PreferencesController { } /** - * Setter for the `useStaticTokenList` property + * Setter for the `useTokenDetection` property * * @param {boolean} val - Whether or not the user prefers to use the static token list or dynamic token list from the API * */ - setUseStaticTokenList(val) { - this.store.updateState({ useStaticTokenList: val }); + setUseTokenDetection(val) { + this.store.updateState({ useTokenDetection: val }); } /** @@ -159,14 +137,6 @@ export default class PreferencesController { this.store.updateState({ firstTimeFlowType: type }); } - getSuggestedTokens() { - return this.store.getState().suggestedTokens; - } - - getAssetImages() { - return this.store.getState().assetImages; - } - /** * Add new methodData to state, to avoid requesting this information again through Infura * @@ -179,24 +149,6 @@ export default class PreferencesController { this.store.updateState({ knownMethodData }); } - /** - * wallet_watchAsset request handler. - * - * @param {Object} req - The watchAsset JSON-RPC request object. - */ - async requestWatchAsset(req) { - const { type, options } = req.params; - - switch (type) { - case 'ERC20': - return await this._handleWatchAssetERC20(options); - default: - throw ethErrors.rpc.invalidParams( - `Asset of type "${type}" not supported.`, - ); - } - } - /** * Setter for the `currentLocale` property * @@ -223,25 +175,14 @@ export default class PreferencesController { */ setAddresses(addresses) { const oldIdentities = this.store.getState().identities; - const oldAccountTokens = this.store.getState().accountTokens; - const oldAccountHiddenTokens = this.store.getState().accountHiddenTokens; const identities = addresses.reduce((ids, address, index) => { const oldId = oldIdentities[address] || {}; ids[address] = { name: `Account ${index + 1}`, address, ...oldId }; return ids; }, {}); - const accountTokens = addresses.reduce((tokens, address) => { - const oldTokens = oldAccountTokens[address] || {}; - tokens[address] = oldTokens; - return tokens; - }, {}); - const accountHiddenTokens = addresses.reduce((hiddenTokens, address) => { - const oldHiddenTokens = oldAccountHiddenTokens[address] || {}; - hiddenTokens[address] = oldHiddenTokens; - return hiddenTokens; - }, {}); - this.store.updateState({ identities, accountTokens, accountHiddenTokens }); + + this.store.updateState({ identities }); } /** @@ -251,19 +192,13 @@ export default class PreferencesController { * @returns {string} the address that was removed */ removeAddress(address) { - const { - identities, - accountTokens, - accountHiddenTokens, - } = this.store.getState(); + const { identities } = this.store.getState(); if (!identities[address]) { throw new Error(`${address} can't be deleted cause it was not found`); } delete identities[address]; - delete accountTokens[address]; - delete accountHiddenTokens[address]; - this.store.updateState({ identities, accountTokens, accountHiddenTokens }); + this.store.updateState({ identities }); // If the selected account is no longer valid, // select an arbitrary other account: @@ -281,11 +216,7 @@ export default class PreferencesController { * */ addAddresses(addresses) { - const { - identities, - accountTokens, - accountHiddenTokens, - } = this.store.getState(); + const { identities } = this.store.getState(); addresses.forEach((address) => { // skip if already exists if (identities[address]) { @@ -294,11 +225,9 @@ export default class PreferencesController { // add missing identity const identityCount = Object.keys(identities).length; - accountTokens[address] = {}; - accountHiddenTokens[address] = {}; identities[address] = { name: `Account ${identityCount + 1}`, address }; }); - this.store.updateState({ identities, accountTokens, accountHiddenTokens }); + this.store.updateState({ identities }); } /** @@ -345,25 +274,16 @@ export default class PreferencesController { return selected; } - removeSuggestedTokens() { - return new Promise((resolve) => { - this.store.updateState({ suggestedTokens: {} }); - resolve({}); - }); - } - /** * Setter for the `selectedAddress` property * * @param {string} _address - A new hex address for an account - * @returns {Promise} Promise resolves with tokens * */ setSelectedAddress(_address) { const address = normalizeAddress(_address); - this._updateTokens(address); - const { identities, tokens } = this.store.getState(); + const { identities } = this.store.getState(); const selectedIdentity = identities[address]; if (!selectedIdentity) { throw new Error(`Identity for '${address} not found`); @@ -371,7 +291,6 @@ export default class PreferencesController { selectedIdentity.lastSelected = Date.now(); this.store.updateState({ identities, selectedAddress: address }); - return Promise.resolve(tokens); } /** @@ -384,99 +303,6 @@ export default class PreferencesController { return this.store.getState().selectedAddress; } - /** - * Contains data about tokens users add to their account. - * @typedef {Object} AddedToken - * @property {string} address - The hex address for the token contract. Will be all lower cased and hex-prefixed. - * @property {string} symbol - The symbol of the token, usually 3 or 4 capitalized letters - * {@link https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md#symbol} - * @property {boolean} decimals - The number of decimals the token uses. - * {@link https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md#decimals} - */ - - /** - * Adds a new token to the token array and removes it from the hiddenToken array, or updates the token if passed an address that already exists. - * Modifies the existing tokens array from the store. All objects in the tokens array array AddedToken objects. - * @see AddedToken {@link AddedToken} - * - * @param {string} rawAddress - Hex address of the token contract. May or may not be a checksum address. - * @param {string} symbol - The symbol of the token - * @param {number} decimals - The number of decimals the token uses. - * @returns {Promise} Promises the new array of AddedToken objects. - * - */ - async addToken(rawAddress, symbol, decimals, image) { - const address = normalizeAddress(rawAddress); - const newEntry = { address, symbol, decimals: Number(decimals) }; - const { tokens, hiddenTokens } = this.store.getState(); - const assetImages = this.getAssetImages(); - const updatedHiddenTokens = hiddenTokens.filter( - (tokenAddress) => tokenAddress !== rawAddress.toLowerCase(), - ); - const previousEntry = tokens.find((token) => { - return token.address === address; - }); - const previousIndex = tokens.indexOf(previousEntry); - - newEntry.isERC721 = await this._detectIsERC721(newEntry.address); - - if (previousEntry) { - tokens[previousIndex] = newEntry; - } else { - tokens.push(newEntry); - } - assetImages[address] = image; - this._updateAccountTokens(tokens, assetImages, updatedHiddenTokens); - return Promise.resolve(tokens); - } - - /** - * Adds isERC721 field to token object - * (Called when a user attempts to add tokens that were previously added which do not yet had isERC721 field) - * - * @param {string} tokenAddress - The contract address of the token requiring the isERC721 field added. - * @returns {Promise} The new token object with the added isERC721 field. - * - */ - async updateTokenType(tokenAddress) { - const { tokens } = this.store.getState(); - const tokenIndex = tokens.findIndex((token) => { - return token.address === tokenAddress; - }); - tokens[tokenIndex].isERC721 = await this._detectIsERC721(tokenAddress); - this.store.updateState({ tokens }); - return Promise.resolve(tokens[tokenIndex]); - } - - /** - * Removes a specified token from the tokens array and adds it to hiddenTokens array - * - * @param {string} rawAddress - Hex address of the token contract to remove. - * @returns {Promise} The new array of AddedToken objects - * - */ - removeToken(rawAddress) { - const { tokens, hiddenTokens } = this.store.getState(); - const assetImages = this.getAssetImages(); - const updatedTokens = tokens.filter( - (token) => token.address !== rawAddress, - ); - const updatedHiddenTokens = [...hiddenTokens, rawAddress.toLowerCase()]; - delete assetImages[rawAddress]; - this._updateAccountTokens(updatedTokens, assetImages, updatedHiddenTokens); - return Promise.resolve(updatedTokens); - } - - /** - * A getter for the `tokens` property - * - * @returns {Array} The current array of AddedToken objects - * - */ - getTokens() { - return this.store.getState().tokens; - } - /** * Sets a custom label for an account * @param {string} account - the account to set a label for @@ -767,189 +593,4 @@ export default class PreferencesController { this.store.updateState({ infuraBlocked: isBlocked }); } - - /** - * Updates `accountTokens`, `tokens`, `accountHiddenTokens` and `hiddenTokens` of current account and network according to it. - * - * @param {array} tokens - Array of tokens to be updated. - * @param {array} assetImages - Array of assets objects related to assets added - * @param {array} hiddenTokens - Array of tokens hidden by user - * - */ - _updateAccountTokens(tokens, assetImages, hiddenTokens) { - const { - accountTokens, - chainId, - selectedAddress, - accountHiddenTokens, - } = this._getTokenRelatedStates(); - accountTokens[selectedAddress][chainId] = tokens; - accountHiddenTokens[selectedAddress][chainId] = hiddenTokens; - this.store.updateState({ - accountTokens, - tokens, - assetImages, - accountHiddenTokens, - hiddenTokens, - }); - } - - /** - * Detects whether or not a token is ERC-721 compatible. - * - * @param {string} tokensAddress - the token contract address. - * - */ - async _detectIsERC721(tokenAddress) { - const checksumAddress = toChecksumHexAddress(tokenAddress); - // if this token is already in our contract metadata map we don't need - // to check against the contract - if (contractsMap[checksumAddress]?.erc721 === true) { - return Promise.resolve(true); - } - const tokenContract = await this._createEthersContract( - tokenAddress, - abiERC721, - this.ethersProvider, - ); - - return await tokenContract - .supportsInterface(ERC721_INTERFACE_ID) - .catch((error) => { - console.log('error', error); - log.debug(error); - return false; - }); - } - - async _createEthersContract(tokenAddress, abi, ethersProvider) { - const tokenContract = await new ethers.Contract( - tokenAddress, - abi, - ethersProvider, - ); - return tokenContract; - } - - /** - * Updates `tokens` and `hiddenTokens` of current account and network. - * - * @param {string} selectedAddress - Account address to be updated with. - * - */ - _updateTokens(selectedAddress) { - const { tokens, hiddenTokens } = this._getTokenRelatedStates( - selectedAddress, - ); - this.store.updateState({ tokens, hiddenTokens }); - } - - /** - * A getter for `tokens`, `accountTokens`, `hiddenTokens` and `accountHiddenTokens` related states. - * - * @param {string} [selectedAddress] - A new hex address for an account - * @returns {Object.} States to interact with tokens in `accountTokens` - * - */ - _getTokenRelatedStates(selectedAddress) { - const { accountTokens, accountHiddenTokens } = this.store.getState(); - if (!selectedAddress) { - // eslint-disable-next-line no-param-reassign - selectedAddress = this.store.getState().selectedAddress; - } - const chainId = this.network.getCurrentChainId(); - if (!(selectedAddress in accountTokens)) { - accountTokens[selectedAddress] = {}; - } - if (!(selectedAddress in accountHiddenTokens)) { - accountHiddenTokens[selectedAddress] = {}; - } - if (!(chainId in accountTokens[selectedAddress])) { - accountTokens[selectedAddress][chainId] = []; - } - if (!(chainId in accountHiddenTokens[selectedAddress])) { - accountHiddenTokens[selectedAddress][chainId] = []; - } - const tokens = accountTokens[selectedAddress][chainId]; - const hiddenTokens = accountHiddenTokens[selectedAddress][chainId]; - return { - tokens, - accountTokens, - hiddenTokens, - accountHiddenTokens, - chainId, - selectedAddress, - }; - } - - /** - * Handle the suggestion of an ERC20 asset through `watchAsset` - * * - * @param {Object} tokenMetadata - Token metadata - * - */ - async _handleWatchAssetERC20(tokenMetadata) { - this._validateERC20AssetParams(tokenMetadata); - - const address = normalizeAddress(tokenMetadata.address); - const { symbol, decimals, image } = tokenMetadata; - this._addSuggestedERC20Asset(address, symbol, decimals, image); - - await this.openPopup(); - const tokenAddresses = this.getTokens().filter( - (token) => token.address === address, - ); - return tokenAddresses.length > 0; - } - - /** - * Validates that the passed options for suggested token have all required properties. - * - * @param {Object} opts - The options object to validate - * @throws {string} Throw a custom error indicating that address, symbol and/or decimals - * doesn't fulfill requirements - * - */ - _validateERC20AssetParams({ address, symbol, decimals }) { - if (!address || !symbol || typeof decimals === 'undefined') { - throw ethErrors.rpc.invalidParams( - `Must specify address, symbol, and decimals.`, - ); - } - if (typeof symbol !== 'string') { - throw ethErrors.rpc.invalidParams(`Invalid symbol: not a string.`); - } - if (!(symbol.length > 0)) { - throw ethErrors.rpc.invalidParams( - `Invalid symbol "${symbol}": shorter than a character.`, - ); - } - if (!(symbol.length < 12)) { - throw ethErrors.rpc.invalidParams( - `Invalid symbol "${symbol}": longer than 11 characters.`, - ); - } - const numDecimals = parseInt(decimals, 10); - if (isNaN(numDecimals) || numDecimals > 36 || numDecimals < 0) { - throw ethErrors.rpc.invalidParams( - `Invalid decimals "${decimals}": must be 0 <= 36.`, - ); - } - if (!isValidHexAddress(address, { allowNonPrefixed: false })) { - throw ethErrors.rpc.invalidParams(`Invalid address "${address}".`); - } - } - - _addSuggestedERC20Asset(address, symbol, decimals, image) { - const newEntry = { - address, - symbol, - decimals, - image, - unlisted: !LISTED_CONTRACT_ADDRESSES.includes(address), - }; - const suggested = this.getSuggestedTokens(); - suggested[address] = newEntry; - this.store.updateState({ suggestedTokens: suggested }); - } } diff --git a/app/scripts/controllers/preferences.test.js b/app/scripts/controllers/preferences.test.js index f088199ed..51277bce2 100644 --- a/app/scripts/controllers/preferences.test.js +++ b/app/scripts/controllers/preferences.test.js @@ -1,11 +1,6 @@ import { strict as assert } from 'assert'; import sinon from 'sinon'; -import contractMaps from '@metamask/contract-metadata'; -import abiERC721 from 'human-standard-collectible-abi'; -import { - MAINNET_CHAIN_ID, - RINKEBY_CHAIN_ID, -} from '../../../shared/constants/network'; +import { MAINNET_CHAIN_ID } from '../../../shared/constants/network'; import PreferencesController from './preferences'; import NetworkController from './network'; @@ -13,9 +8,6 @@ describe('preferences controller', function () { let preferencesController; let network; let currentChainId; - let triggerNetworkChange; - let switchToMainnet; - let switchToRinkeby; let provider; const migrateAddressBookState = sinon.stub(); @@ -37,22 +29,12 @@ describe('preferences controller', function () { sandbox .stub(network, 'getProviderConfig') .callsFake(() => ({ type: 'mainnet' })); - const spy = sandbox.spy(network, 'on'); preferencesController = new PreferencesController({ migrateAddressBookState, network, provider, }); - triggerNetworkChange = spy.firstCall.args[1]; - switchToMainnet = () => { - currentChainId = MAINNET_CHAIN_ID; - triggerNetworkChange(); - }; - switchToRinkeby = () => { - currentChainId = RINKEBY_CHAIN_ID; - triggerNetworkChange(); - }; }); afterEach(function () { @@ -76,17 +58,6 @@ describe('preferences controller', function () { }); }); - it('should create account tokens for each account in the store', function () { - preferencesController.setAddresses(['0xda22le', '0x7e57e2']); - - const { accountTokens } = preferencesController.store.getState(); - - assert.deepEqual(accountTokens, { - '0xda22le': {}, - '0x7e57e2': {}, - }); - }); - it('should replace its list of addresses', function () { preferencesController.setAddresses(['0xda22le', '0x7e57e2']); preferencesController.setAddresses(['0xda22le77', '0x7e57e277']); @@ -105,104 +76,6 @@ describe('preferences controller', function () { }); }); - describe('updateTokenType', function () { - it('should add isERC721 = true to token object in state when token is collectible and in our contract-metadata repo', async function () { - const contractAddresses = Object.keys(contractMaps); - const erc721ContractAddresses = contractAddresses.filter( - (contractAddress) => contractMaps[contractAddress].erc721 === true, - ); - const address = erc721ContractAddresses[0]; - const { symbol, decimals } = contractMaps[address]; - preferencesController.store.updateState({ - tokens: [{ address, symbol, decimals }], - }); - const result = await preferencesController.updateTokenType(address); - assert.equal(result.isERC721, true); - }); - - it('should add isERC721 = true to token object in state when token is collectible and not in our contract-metadata repo', async function () { - const tokenAddress = '0xda5584cc586d07c7141aa427224a4bd58e64af7d'; - preferencesController.store.updateState({ - tokens: [ - { - address: tokenAddress, - symbol: 'TESTNFT', - decimals: '0', - }, - ], - }); - sinon - .stub(preferencesController, '_detectIsERC721') - .callsFake(() => true); - - const result = await preferencesController.updateTokenType(tokenAddress); - assert.equal( - preferencesController._detectIsERC721.getCall(0).args[0], - tokenAddress, - ); - assert.equal(result.isERC721, true); - }); - }); - - describe('_detectIsERC721', function () { - it('should return true when token is in our contract-metadata repo', async function () { - const tokenAddress = '0x06012c8cf97BEaD5deAe237070F9587f8E7A266d'; - - const result = await preferencesController._detectIsERC721(tokenAddress); - assert.equal(result, true); - }); - - it('should return true when the token is not in our contract-metadata repo but tokenContract.supportsInterface returns true', async function () { - const tokenAddress = '0xda5584cc586d07c7141aa427224a4bd58e64af7d'; - - const supportsInterfaceStub = sinon.stub().returns(Promise.resolve(true)); - sinon - .stub(preferencesController, '_createEthersContract') - .callsFake(() => ({ supportsInterface: supportsInterfaceStub })); - - const result = await preferencesController._detectIsERC721(tokenAddress); - assert.equal( - preferencesController._createEthersContract.getCall(0).args[0], - tokenAddress, - ); - assert.deepEqual( - preferencesController._createEthersContract.getCall(0).args[1], - abiERC721, - ); - assert.equal( - preferencesController._createEthersContract.getCall(0).args[2], - preferencesController.ethersProvider, - ); - assert.equal(result, true); - }); - - it('should return false when the token is not in our contract-metadata repo and tokenContract.supportsInterface returns false', async function () { - const tokenAddress = '0xda5584cc586d07c7141aa427224a4bd58e64af7d'; - - const supportsInterfaceStub = sinon - .stub() - .returns(Promise.resolve(false)); - sinon - .stub(preferencesController, '_createEthersContract') - .callsFake(() => ({ supportsInterface: supportsInterfaceStub })); - - const result = await preferencesController._detectIsERC721(tokenAddress); - assert.equal( - preferencesController._createEthersContract.getCall(0).args[0], - tokenAddress, - ); - assert.deepEqual( - preferencesController._createEthersContract.getCall(0).args[1], - abiERC721, - ); - assert.equal( - preferencesController._createEthersContract.getCall(0).args[2], - preferencesController.ethersProvider, - ); - assert.equal(result, false); - }); - }); - describe('removeAddress', function () { it('should remove an address from state', function () { preferencesController.setAddresses(['0xda22le', '0x7e57e2']); @@ -215,17 +88,6 @@ describe('preferences controller', function () { ); }); - it('should remove an address from state and respective tokens', function () { - preferencesController.setAddresses(['0xda22le', '0x7e57e2']); - - preferencesController.removeAddress('0xda22le'); - - assert.equal( - preferencesController.store.getState().accountTokens['0xda22le'], - undefined, - ); - }); - it('should switch accounts if the selected address is removed', function () { preferencesController.setAddresses(['0xda22le', '0x7e57e2']); @@ -259,489 +121,6 @@ describe('preferences controller', function () { }); }); - describe('getTokens', function () { - it('should return an empty list initially', async function () { - preferencesController.setAddresses(['0x7e57e2']); - await preferencesController.setSelectedAddress('0x7e57e2'); - - const tokens = preferencesController.getTokens(); - assert.equal(tokens.length, 0, 'empty list of tokens'); - }); - }); - - describe('addToken', function () { - it('should add that token to its state', async function () { - const address = '0xabcdef1234567'; - const symbol = 'ABBR'; - const decimals = 5; - - preferencesController.setAddresses(['0x7e57e2']); - await preferencesController.setSelectedAddress('0x7e57e2'); - await preferencesController.addToken(address, symbol, decimals); - - const tokens = preferencesController.getTokens(); - assert.equal(tokens.length, 1, 'one token added'); - - const added = tokens[0]; - assert.equal(added.address, address, 'set address correctly'); - assert.equal(added.symbol, symbol, 'set symbol correctly'); - assert.equal(added.decimals, decimals, 'set decimals correctly'); - }); - - it('should allow updating a token value', async function () { - const address = '0xabcdef1234567'; - const symbol = 'ABBR'; - const decimals = 5; - - preferencesController.setAddresses(['0x7e57e2']); - await preferencesController.setSelectedAddress('0x7e57e2'); - await preferencesController.addToken(address, symbol, decimals); - - const newDecimals = 6; - await preferencesController.addToken(address, symbol, newDecimals); - - const tokens = preferencesController.getTokens(); - assert.equal(tokens.length, 1, 'one token added'); - - const added = tokens[0]; - assert.equal(added.address, address, 'set address correctly'); - assert.equal(added.symbol, symbol, 'set symbol correctly'); - assert.equal(added.decimals, newDecimals, 'updated decimals correctly'); - }); - - it('should allow adding tokens to two separate addresses', async function () { - const address = '0xabcdef1234567'; - const symbol = 'ABBR'; - const decimals = 5; - - preferencesController.setAddresses(['0x7e57e2', '0xda22le']); - - await preferencesController.setSelectedAddress('0x7e57e2'); - await preferencesController.addToken(address, symbol, decimals); - assert.equal( - preferencesController.getTokens().length, - 1, - 'one token added for 1st address', - ); - - await preferencesController.setSelectedAddress('0xda22le'); - await preferencesController.addToken(address, symbol, decimals); - assert.equal( - preferencesController.getTokens().length, - 1, - 'one token added for 2nd address', - ); - }); - - it('should add token per account', async function () { - const addressFirst = '0xabcdef1234567'; - const addressSecond = '0xabcdef1234568'; - const symbolFirst = 'ABBR'; - const symbolSecond = 'ABBB'; - const decimals = 5; - - preferencesController.setAddresses(['0x7e57e2', '0xda22le']); - - await preferencesController.setSelectedAddress('0x7e57e2'); - await preferencesController.addToken(addressFirst, symbolFirst, decimals); - const tokensFirstAddress = preferencesController.getTokens(); - - await preferencesController.setSelectedAddress('0xda22le'); - await preferencesController.addToken( - addressSecond, - symbolSecond, - decimals, - ); - const tokensSeconAddress = preferencesController.getTokens(); - - assert.notEqual( - tokensFirstAddress, - tokensSeconAddress, - 'add different tokens for two account and tokens are equal', - ); - }); - - it('should add token per network', async function () { - const addressFirst = '0xabcdef1234567'; - const addressSecond = '0xabcdef1234568'; - const symbolFirst = 'ABBR'; - const symbolSecond = 'ABBB'; - const decimals = 5; - await preferencesController.addToken(addressFirst, symbolFirst, decimals); - const tokensFirstAddress = preferencesController.getTokens(); - - switchToRinkeby(); - await preferencesController.addToken( - addressSecond, - symbolSecond, - decimals, - ); - const tokensSeconAddress = preferencesController.getTokens(); - - assert.notEqual( - tokensFirstAddress, - tokensSeconAddress, - 'add different tokens for two networks and tokens are equal', - ); - }); - }); - - describe('removeToken', function () { - it('should remove the only token from its state', async function () { - preferencesController.setAddresses(['0x7e57e2']); - await preferencesController.setSelectedAddress('0x7e57e2'); - await preferencesController.addToken('0xa', 'A', 5); - await preferencesController.removeToken('0xa'); - - const tokens = preferencesController.getTokens(); - assert.equal(tokens.length, 0, 'one token removed'); - }); - - it('should remove a token from its state', async function () { - preferencesController.setAddresses(['0x7e57e2']); - await preferencesController.setSelectedAddress('0x7e57e2'); - await preferencesController.addToken('0xa', 'A', 4); - await preferencesController.addToken('0xb', 'B', 5); - await preferencesController.removeToken('0xa'); - - const tokens = preferencesController.getTokens(); - assert.equal(tokens.length, 1, 'one token removed'); - - const [token1] = tokens; - assert.deepEqual(token1, { - address: '0xb', - symbol: 'B', - decimals: 5, - isERC721: false, - }); - }); - - it('should remove a token from its state on corresponding address', async function () { - preferencesController.setAddresses(['0x7e57e2', '0x7e57e3']); - await preferencesController.setSelectedAddress('0x7e57e2'); - await preferencesController.addToken('0xa', 'A', 4); - await preferencesController.addToken('0xb', 'B', 5); - await preferencesController.setSelectedAddress('0x7e57e3'); - await preferencesController.addToken('0xa', 'A', 4); - await preferencesController.addToken('0xb', 'B', 5); - const initialTokensSecond = preferencesController.getTokens(); - await preferencesController.setSelectedAddress('0x7e57e2'); - await preferencesController.removeToken('0xa'); - - const tokensFirst = preferencesController.getTokens(); - assert.equal(tokensFirst.length, 1, 'one token removed in account'); - - const [token1] = tokensFirst; - assert.deepEqual(token1, { - address: '0xb', - symbol: 'B', - decimals: 5, - isERC721: false, - }); - - await preferencesController.setSelectedAddress('0x7e57e3'); - const tokensSecond = preferencesController.getTokens(); - assert.deepEqual( - tokensSecond, - initialTokensSecond, - 'token deleted for account', - ); - }); - - it('should remove a token from its state on corresponding network', async function () { - await preferencesController.addToken('0xa', 'A', 4); - await preferencesController.addToken('0xb', 'B', 5); - switchToRinkeby(); - await preferencesController.addToken('0xa', 'A', 4); - await preferencesController.addToken('0xb', 'B', 5); - const initialTokensSecond = preferencesController.getTokens(); - switchToMainnet(); - await preferencesController.removeToken('0xa'); - - const tokensFirst = preferencesController.getTokens(); - assert.equal(tokensFirst.length, 1, 'one token removed in network'); - - const [token1] = tokensFirst; - assert.deepEqual(token1, { - address: '0xb', - symbol: 'B', - decimals: 5, - isERC721: false, - }); - - switchToRinkeby(); - const tokensSecond = preferencesController.getTokens(); - assert.deepEqual( - tokensSecond, - initialTokensSecond, - 'token deleted for network', - ); - }); - }); - - describe('on setSelectedAddress', function () { - it('should update tokens from its state on corresponding address', async function () { - preferencesController.setAddresses(['0x7e57e2', '0x7e57e3']); - await preferencesController.setSelectedAddress('0x7e57e2'); - await preferencesController.addToken('0xa', 'A', 4); - await preferencesController.addToken('0xb', 'B', 5); - await preferencesController.setSelectedAddress('0x7e57e3'); - await preferencesController.addToken('0xa', 'C', 4); - await preferencesController.addToken('0xb', 'D', 5); - - await preferencesController.setSelectedAddress('0x7e57e2'); - const initialTokensFirst = preferencesController.getTokens(); - await preferencesController.setSelectedAddress('0x7e57e3'); - const initialTokensSecond = preferencesController.getTokens(); - - assert.notDeepEqual( - initialTokensFirst, - initialTokensSecond, - 'tokens not equal for different accounts and tokens', - ); - - await preferencesController.setSelectedAddress('0x7e57e2'); - const tokensFirst = preferencesController.getTokens(); - await preferencesController.setSelectedAddress('0x7e57e3'); - const tokensSecond = preferencesController.getTokens(); - - assert.deepEqual( - tokensFirst, - initialTokensFirst, - 'tokens equal for same account', - ); - assert.deepEqual( - tokensSecond, - initialTokensSecond, - 'tokens equal for same account', - ); - }); - }); - - describe('on updateStateNetworkType', function () { - it('should remove a token from its state on corresponding network', async function () { - await preferencesController.addToken('0xa', 'A', 4); - await preferencesController.addToken('0xb', 'B', 5); - const initialTokensFirst = preferencesController.getTokens(); - switchToRinkeby(); - await preferencesController.addToken('0xa', 'C', 4); - await preferencesController.addToken('0xb', 'D', 5); - const initialTokensSecond = preferencesController.getTokens(); - - assert.notDeepEqual( - initialTokensFirst, - initialTokensSecond, - 'tokens not equal for different networks and tokens', - ); - - switchToMainnet(); - const tokensFirst = preferencesController.getTokens(); - switchToRinkeby(); - const tokensSecond = preferencesController.getTokens(); - assert.deepEqual( - tokensFirst, - initialTokensFirst, - 'tokens equal for same network', - ); - assert.deepEqual( - tokensSecond, - initialTokensSecond, - 'tokens equal for same network', - ); - }); - }); - - describe('on watchAsset', function () { - let req, stubHandleWatchAssetERC20; - const sandbox = sinon.createSandbox(); - - beforeEach(function () { - req = { method: 'wallet_watchAsset', params: {} }; - stubHandleWatchAssetERC20 = sandbox.stub( - preferencesController, - '_handleWatchAssetERC20', - ); - }); - - after(function () { - sandbox.restore(); - }); - - it('should error if passed no type', async function () { - await assert.rejects( - () => preferencesController.requestWatchAsset(req), - { message: 'Asset of type "undefined" not supported.' }, - 'should have errored', - ); - }); - - it('should error if method is not supported', async function () { - req.params.type = 'someasset'; - await assert.rejects( - () => preferencesController.requestWatchAsset(req), - { message: 'Asset of type "someasset" not supported.' }, - 'should have errored', - ); - }); - - it('should handle ERC20 type', async function () { - req.params.type = 'ERC20'; - await preferencesController.requestWatchAsset(req); - sandbox.assert.called(stubHandleWatchAssetERC20); - }); - }); - - describe('on watchAsset of type ERC20', function () { - let req; - - const sandbox = sinon.createSandbox(); - beforeEach(function () { - req = { params: { type: 'ERC20' } }; - }); - after(function () { - sandbox.restore(); - }); - - it('should add suggested token', async function () { - const address = '0xabcdef1234567'; - const symbol = 'ABBR'; - const decimals = 5; - const image = 'someimage'; - req.params.options = { address, symbol, decimals, image }; - - sandbox - .stub(preferencesController, '_validateERC20AssetParams') - .returns(true); - preferencesController.openPopup = async () => undefined; - - await preferencesController._handleWatchAssetERC20(req.params.options); - const suggested = preferencesController.getSuggestedTokens(); - assert.equal( - Object.keys(suggested).length, - 1, - `one token added ${Object.keys(suggested)}`, - ); - - assert.equal( - suggested[address].address, - address, - 'set address correctly', - ); - assert.equal(suggested[address].symbol, symbol, 'set symbol correctly'); - assert.equal( - suggested[address].decimals, - decimals, - 'set decimals correctly', - ); - assert.equal(suggested[address].image, image, 'set image correctly'); - }); - - it('should add token correctly if user confirms', async function () { - const address = '0xabcdef1234567'; - const symbol = 'ABBR'; - const decimals = 5; - const image = 'someimage'; - req.params.options = { address, symbol, decimals, image }; - - sandbox - .stub(preferencesController, '_validateERC20AssetParams') - .returns(true); - preferencesController.openPopup = async () => { - await preferencesController.addToken(address, symbol, decimals, image); - }; - - await preferencesController._handleWatchAssetERC20(req.params.options); - const tokens = preferencesController.getTokens(); - assert.equal(tokens.length, 1, `one token added`); - const added = tokens[0]; - assert.equal(added.address, address, 'set address correctly'); - assert.equal(added.symbol, symbol, 'set symbol correctly'); - assert.equal(added.decimals, decimals, 'set decimals correctly'); - - const assetImages = preferencesController.getAssetImages(); - assert.ok(assetImages[address], `set image correctly`); - }); - it('should validate ERC20 asset correctly', async function () { - const validate = preferencesController._validateERC20AssetParams; - - assert.doesNotThrow(() => - validate({ - address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', - symbol: 'ABC', - decimals: 0, - }), - ); - assert.doesNotThrow(() => - validate({ - address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', - symbol: 'ABCDEFGHIJK', - decimals: 0, - }), - ); - - assert.throws( - () => validate({ symbol: 'ABC', decimals: 0 }), - 'missing address should fail', - ); - assert.throws( - () => - validate({ - address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', - decimals: 0, - }), - 'missing symbol should fail', - ); - assert.throws( - () => - validate({ - address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', - symbol: 'ABC', - }), - 'missing decimals should fail', - ); - assert.throws( - () => - validate({ - address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', - symbol: 'ABCDEFGHIJKLM', - decimals: 0, - }), - 'long symbol should fail', - ); - assert.throws( - () => - validate({ - address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', - symbol: '', - decimals: 0, - }), - 'empty symbol should fail', - ); - assert.throws( - () => - validate({ - address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', - symbol: 'ABC', - decimals: -1, - }), - 'decimals < 0 should fail', - ); - assert.throws( - () => - validate({ - address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', - symbol: 'ABC', - decimals: 38, - }), - 'decimals > 36 should fail', - ); - assert.throws( - () => validate({ address: '0x123', symbol: 'ABC', decimals: 0 }), - 'invalid address should fail', - ); - }); - }); - describe('setPasswordForgotten', function () { it('should default to false', function () { const state = preferencesController.store.getState(); @@ -869,20 +248,20 @@ describe('preferences controller', function () { ); }); }); - describe('setUseStaticTokenList', function () { + describe('setUseTokenDetection', function () { it('should default to false', function () { const state = preferencesController.store.getState(); - assert.equal(state.useStaticTokenList, false); + assert.equal(state.useTokenDetection, false); }); - it('should set the useStaticTokenList property in state', function () { + it('should set the useTokenDetection property in state', function () { assert.equal( - preferencesController.store.getState().useStaticTokenList, + preferencesController.store.getState().useTokenDetection, false, ); - preferencesController.setUseStaticTokenList(true); + preferencesController.setUseTokenDetection(true); assert.equal( - preferencesController.store.getState().useStaticTokenList, + preferencesController.store.getState().useTokenDetection, true, ); }); diff --git a/app/scripts/controllers/swaps.js b/app/scripts/controllers/swaps.js index 238dba04c..150b4fece 100644 --- a/app/scripts/controllers/swaps.js +++ b/app/scripts/controllers/swaps.js @@ -24,8 +24,9 @@ import { isSwapsDefaultTokenAddress } from '../../../shared/modules/swaps.utils' import { fetchTradesInfo as defaultFetchTradesInfo, - fetchSwapsQuoteRefreshTime as defaultFetchSwapsQuoteRefreshTime, + getBaseApi, } from '../../../ui/pages/swaps/swaps.util'; +import fetchWithCache from '../../../ui/helpers/utils/fetch-with-cache'; import { MINUTE, SECOND } from '../../../shared/constants/time'; import { NETWORK_EVENTS } from './network'; @@ -40,10 +41,6 @@ const POLL_COUNT_LIMIT = 3; // provide a reasonable fallback to avoid further errors 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 = SECOND * 10; - function calculateGasEstimateWithRefund( maxGas = MAX_GAS_LIMIT, estimatedRefund = 0, @@ -64,6 +61,7 @@ function calculateGasEstimateWithRefund( const initialState = { swapsState: { quotes: {}, + quotesPollingLimitEnabled: false, fetchParams: null, tokens: null, tradeTxId: null, @@ -82,6 +80,7 @@ const initialState = { swapsFeatureIsLive: true, useNewSwapsApi: false, swapsQuoteRefreshTime: FALLBACK_QUOTE_REFRESH_TIME, + swapsQuotePrefetchingRefreshTime: FALLBACK_QUOTE_REFRESH_TIME, }, }; @@ -93,7 +92,6 @@ export default class SwapsController { getProviderConfig, tokenRatesStore, fetchTradesInfo = defaultFetchTradesInfo, - fetchSwapsQuoteRefreshTime = defaultFetchSwapsQuoteRefreshTime, getCurrentChainId, getEIP1559GasFeeEstimates, }) { @@ -102,7 +100,6 @@ export default class SwapsController { }); this._fetchTradesInfo = fetchTradesInfo; - this._fetchSwapsQuoteRefreshTime = fetchSwapsQuoteRefreshTime; this._getCurrentChainId = getCurrentChainId; this._getEIP1559GasFeeEstimates = getEIP1559GasFeeEstimates; @@ -124,38 +121,70 @@ export default class SwapsController { }); } + async fetchSwapsRefreshRates(chainId, useNewSwapsApi) { + const response = await fetchWithCache( + getBaseApi('network', chainId, useNewSwapsApi), + { method: 'GET' }, + { cacheRefreshTime: 600000 }, + ); + const { refreshRates } = response || {}; + if ( + !refreshRates || + typeof refreshRates.quotes !== 'number' || + typeof refreshRates.quotesPrefetching !== 'number' + ) { + throw new Error( + `MetaMask - invalid response for refreshRates: ${response}`, + ); + } + // We presently use milliseconds in the UI. + return { + quotes: refreshRates.quotes * 1000, + quotesPrefetching: refreshRates.quotesPrefetching * 1000, + }; + } + // Sets the refresh rate for quote updates from the MetaSwap API - async _setSwapsQuoteRefreshTime() { + async _setSwapsRefreshRates() { const chainId = this._getCurrentChainId(); const { swapsState } = this.store.getState(); - - // Default to fallback time unless API returns valid response - let swapsQuoteRefreshTime = FALLBACK_QUOTE_REFRESH_TIME; + let swapsRefreshRates; try { - swapsQuoteRefreshTime = await this._fetchSwapsQuoteRefreshTime( + swapsRefreshRates = await this.fetchSwapsRefreshRates( chainId, swapsState.useNewSwapsApi, ); } catch (e) { console.error('Request for swaps quote refresh time failed: ', e); } - const { swapsState: latestSwapsState } = this.store.getState(); - this.store.updateState({ - swapsState: { ...latestSwapsState, swapsQuoteRefreshTime }, + swapsState: { + ...latestSwapsState, + swapsQuoteRefreshTime: + swapsRefreshRates?.quotes || FALLBACK_QUOTE_REFRESH_TIME, + swapsQuotePrefetchingRefreshTime: + swapsRefreshRates?.quotesPrefetching || FALLBACK_QUOTE_REFRESH_TIME, + }, }); } // Once quotes are fetched, we poll for new ones to keep the quotes up to date. Market and aggregator contract conditions can change fast enough - // that quotes will no longer be available after 1 or 2 minutes. When fetchAndSetQuotes is first called it, receives fetch that parameters are stored in + // that quotes will no longer be available after 1 or 2 minutes. When fetchAndSetQuotes is first called, it receives fetch parameters that are stored in // state. These stored parameters are used on subsequent calls made during polling. // Note: we stop polling after 3 requests, until new quotes are explicitly asked for. The logic that enforces that maximum is in the body of fetchAndSetQuotes pollForNewQuotes() { const { - swapsState: { swapsQuoteRefreshTime }, + swapsState: { + swapsQuoteRefreshTime, + swapsQuotePrefetchingRefreshTime, + quotesPollingLimitEnabled, + }, } = this.store.getState(); - + // swapsQuoteRefreshTime is used on the View Quote page, swapsQuotePrefetchingRefreshTime is used on the Build Quote page. + const quotesRefreshRateInMs = quotesPollingLimitEnabled + ? swapsQuoteRefreshTime + : swapsQuotePrefetchingRefreshTime; this.pollingTimeout = setTimeout(() => { const { swapsState } = this.store.getState(); this.fetchAndSetQuotes( @@ -163,11 +192,13 @@ export default class SwapsController { swapsState.fetchParams?.metaData, true, ); - }, swapsQuoteRefreshTime - QUOTE_POLLING_DIFFERENCE_INTERVAL); + }, quotesRefreshRateInMs); } stopPollingForQuotes() { - clearTimeout(this.pollingTimeout); + if (this.pollingTimeout) { + clearTimeout(this.pollingTimeout); + } } async fetchAndSetQuotes( @@ -177,7 +208,7 @@ export default class SwapsController { ) { const { chainId } = fetchParamsMetaData; const { - swapsState: { useNewSwapsApi }, + swapsState: { useNewSwapsApi, quotesPollingLimitEnabled }, } = this.store.getState(); if (!fetchParams) { @@ -203,7 +234,7 @@ export default class SwapsController { ...fetchParamsMetaData, useNewSwapsApi, }), - this._setSwapsQuoteRefreshTime(), + this._setSwapsRefreshRates(), ]); newQuotes = mapValues(newQuotes, (quote) => ({ @@ -292,9 +323,13 @@ export default class SwapsController { }, }); - // We only want to do up to a maximum of three requests from polling. - this.pollCount += 1; - if (this.pollCount < POLL_COUNT_LIMIT + 1) { + if (quotesPollingLimitEnabled) { + // We only want to do up to a maximum of three requests from polling if polling limit is enabled. + // Otherwise we won't increase pollCount, so polling will run without a limit. + this.pollCount += 1; + } + + if (!quotesPollingLimitEnabled || this.pollCount < POLL_COUNT_LIMIT + 1) { this.pollForNewQuotes(); } else { this.resetPostFetchState(); @@ -322,6 +357,11 @@ export default class SwapsController { this.store.updateState({ swapsState: { ...swapsState, tokens } }); } + clearSwapsQuotes() { + const { swapsState } = this.store.getState(); + this.store.updateState({ swapsState: { ...swapsState, quotes: {} } }); + } + setSwapsErrorKey(errorKey) { const { swapsState } = this.store.getState(); this.store.updateState({ swapsState: { ...swapsState, errorKey } }); @@ -464,6 +504,13 @@ export default class SwapsController { }); } + setSwapsQuotesPollingLimitEnabled(quotesPollingLimitEnabled) { + const { swapsState } = this.store.getState(); + this.store.updateState({ + swapsState: { ...swapsState, quotesPollingLimitEnabled }, + }); + } + setSwapsTxMaxFeePriorityPerGas(maxPriorityFeePerGas) { const { swapsState } = this.store.getState(); this.store.updateState({ @@ -511,6 +558,8 @@ export default class SwapsController { swapsFeatureIsLive: swapsState.swapsFeatureIsLive, useNewSwapsApi: swapsState.useNewSwapsApi, swapsQuoteRefreshTime: swapsState.swapsQuoteRefreshTime, + swapsQuotePrefetchingRefreshTime: + swapsState.swapsQuotePrefetchingRefreshTime, }, }); clearTimeout(this.pollingTimeout); @@ -523,14 +572,15 @@ export default class SwapsController { ...initialState.swapsState, tokens: swapsState.tokens, swapsQuoteRefreshTime: swapsState.swapsQuoteRefreshTime, + swapsQuotePrefetchingRefreshTime: + swapsState.swapsQuotePrefetchingRefreshTime, }, }); clearTimeout(this.pollingTimeout); } async _findTopQuoteAndCalculateSavings(quotes = {}) { - const tokenConversionRates = this.tokenRatesStore.getState() - .contractExchangeRates; + const tokenConversionRates = this.tokenRatesStore.contractExchangeRates; const { swapsState: { customGasPrice, customMaxPriorityFeePerGas }, } = this.store.getState(); diff --git a/app/scripts/controllers/swaps.test.js b/app/scripts/controllers/swaps.test.js index 4ed83ef25..d6127c6db 100644 --- a/app/scripts/controllers/swaps.test.js +++ b/app/scripts/controllers/swaps.test.js @@ -4,7 +4,6 @@ import sinon from 'sinon'; import { ethers } from 'ethers'; import { mapValues } from 'lodash'; import BigNumber from 'bignumber.js'; -import { ObservableStore } from '@metamask/obs-store'; import { ROPSTEN_NETWORK_ID, MAINNET_NETWORK_ID, @@ -83,12 +82,12 @@ const MOCK_FETCH_METADATA = { chainId: MAINNET_CHAIN_ID, }; -const MOCK_TOKEN_RATES_STORE = new ObservableStore({ +const MOCK_TOKEN_RATES_STORE = { contractExchangeRates: { '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': 2, '0x1111111111111111111111111111111111111111': 0.1, }, -}); +}; const MOCK_GET_PROVIDER_CONFIG = () => ({ type: 'FAKE_NETWORK' }); @@ -116,6 +115,7 @@ function getMockNetworkController() { const EMPTY_INIT_STATE = { swapsState: { quotes: {}, + quotesPollingLimitEnabled: false, fetchParams: null, tokens: null, tradeTxId: null, @@ -133,13 +133,13 @@ const EMPTY_INIT_STATE = { swapsFeatureIsLive: true, useNewSwapsApi: false, swapsQuoteRefreshTime: 60000, + swapsQuotePrefetchingRefreshTime: 60000, swapsUserFeeLevel: '', }, }; const sandbox = sinon.createSandbox(); const fetchTradesInfoStub = sandbox.stub(); -const fetchSwapsQuoteRefreshTimeStub = sandbox.stub(); const getCurrentChainIdStub = sandbox.stub(); getCurrentChainIdStub.returns(MAINNET_CHAIN_ID); const getEIP1559GasFeeEstimatesStub = sandbox.stub(() => { @@ -162,7 +162,6 @@ describe('SwapsController', function () { getProviderConfig: MOCK_GET_PROVIDER_CONFIG, tokenRatesStore: MOCK_TOKEN_RATES_STORE, fetchTradesInfo: fetchTradesInfoStub, - fetchSwapsQuoteRefreshTime: fetchSwapsQuoteRefreshTimeStub, getCurrentChainId: getCurrentChainIdStub, getEIP1559GasFeeEstimates: getEIP1559GasFeeEstimatesStub, }); @@ -670,7 +669,6 @@ describe('SwapsController', function () { it('calls fetchTradesInfo with the given fetchParams and returns the correct quotes', async function () { fetchTradesInfoStub.resolves(getMockQuotes()); - fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime()); // Make it so approval is not required sandbox @@ -716,7 +714,6 @@ describe('SwapsController', function () { it('performs the allowance check', async function () { fetchTradesInfoStub.resolves(getMockQuotes()); - fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime()); // Make it so approval is not required const allowanceStub = sandbox @@ -740,7 +737,6 @@ describe('SwapsController', function () { it('gets the gas limit if approval is required', async function () { fetchTradesInfoStub.resolves(MOCK_QUOTES_APPROVAL_REQUIRED); - fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime()); // Ensure approval is required sandbox @@ -766,7 +762,6 @@ describe('SwapsController', function () { it('marks the best quote', async function () { fetchTradesInfoStub.resolves(getMockQuotes()); - fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime()); // Make it so approval is not required sandbox @@ -797,7 +792,6 @@ describe('SwapsController', function () { }; const quotes = { ...getMockQuotes(), [bestAggId]: bestQuote }; fetchTradesInfoStub.resolves(quotes); - fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime()); // Make it so approval is not required sandbox @@ -815,16 +809,15 @@ describe('SwapsController', function () { it('does not mark as best quote if no conversion rate exists for destination token', async function () { fetchTradesInfoStub.resolves(getMockQuotes()); - fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime()); // Make it so approval is not required sandbox .stub(swapsController, '_getERC20Allowance') .resolves(ethers.BigNumber.from(1)); - swapsController.tokenRatesStore.updateState({ + swapsController.tokenRatesStore = { contractExchangeRates: {}, - }); + }; const [newQuotes, topAggId] = await swapsController.fetchAndSetQuotes( MOCK_FETCH_PARAMS, MOCK_FETCH_METADATA, @@ -843,6 +836,8 @@ describe('SwapsController', function () { ...EMPTY_INIT_STATE.swapsState, tokens: old.tokens, swapsQuoteRefreshTime: old.swapsQuoteRefreshTime, + swapsQuotePrefetchingRefreshTime: + old.swapsQuotePrefetchingRefreshTime, }); }); @@ -890,6 +885,7 @@ describe('SwapsController', function () { const swapsFeatureIsLive = false; const useNewSwapsApi = false; const swapsQuoteRefreshTime = 0; + const swapsQuotePrefetchingRefreshTime = 0; swapsController.store.updateState({ swapsState: { tokens, @@ -897,6 +893,7 @@ describe('SwapsController', function () { swapsFeatureIsLive, useNewSwapsApi, swapsQuoteRefreshTime, + swapsQuotePrefetchingRefreshTime, }, }); @@ -909,6 +906,7 @@ describe('SwapsController', function () { fetchParams, swapsFeatureIsLive, swapsQuoteRefreshTime, + swapsQuotePrefetchingRefreshTime, }); }); }); @@ -1387,7 +1385,3 @@ function getTopQuoteAndSavingsBaseExpectedResults() { }, }; } - -function getMockQuoteRefreshTime() { - return 45000; -} diff --git a/app/scripts/controllers/token-rates-controller.test.js b/app/scripts/controllers/token-rates-controller.test.js index 444e53977..31032495e 100644 --- a/app/scripts/controllers/token-rates-controller.test.js +++ b/app/scripts/controllers/token-rates-controller.test.js @@ -1,31 +1,62 @@ import { strict as assert } from 'assert'; import sinon from 'sinon'; -import { ObservableStore } from '@metamask/obs-store'; +import { TokensController } from '@metamask/controllers'; import TokenRatesController from './token-rates'; +import NetworkController from './network'; +import PreferencesController from './preferences'; + +const networkControllerProviderConfig = { + getAccounts: () => undefined, +}; describe('TokenRatesController', function () { - let nativeCurrency; - let getNativeCurrency; + let nativeCurrency, + getNativeCurrency, + network, + provider, + preferences, + tokensController; beforeEach(function () { nativeCurrency = 'ETH'; getNativeCurrency = () => nativeCurrency; + network = new NetworkController(); + network.setInfuraProjectId('foo'); + network.initializeProvider(networkControllerProviderConfig); + provider = network.getProviderAndBlockTracker().provider; + preferences = new PreferencesController({ network, provider }); + tokensController = new TokensController({ + onPreferencesStateChange: preferences.store.subscribe.bind( + preferences.store, + ), + onNetworkStateChange: network.store.subscribe.bind(network.store), + }); + sinon.stub(network, 'getLatestBlock').callsFake(() => Promise.resolve({})); + sinon.stub(tokensController, '_instantiateNewEthersProvider').returns(null); + sinon + .stub(tokensController, '_detectIsERC721') + .returns(Promise.resolve(false)); }); - it('should listen for preferences store updates', function () { - const preferences = new ObservableStore({ tokens: [] }); - preferences.putState({ tokens: ['foo'] }); + it('should listen for tokenControllers state updates', async function () { const controller = new TokenRatesController({ - preferences, + tokensController, getNativeCurrency, }); - assert.deepEqual(controller._tokens, ['foo']); + await tokensController.addToken('0x1', 'TEST', 1); + assert.deepEqual(controller._tokens, [ + { + address: '0x1', + decimals: 1, + symbol: 'TEST', + image: undefined, + isERC721: false, + }, + ]); }); it('should poll on correct interval', async function () { const stub = sinon.stub(global, 'setInterval'); - const preferences = new ObservableStore({ tokens: [] }); - preferences.putState({ tokens: ['foo'] }); const controller = new TokenRatesController({ - preferences, + tokensController, getNativeCurrency, }); controller.start(1337); diff --git a/app/scripts/controllers/token-rates.js b/app/scripts/controllers/token-rates.js index 2bc7f2a19..319265593 100644 --- a/app/scripts/controllers/token-rates.js +++ b/app/scripts/controllers/token-rates.js @@ -20,11 +20,11 @@ export default class TokenRatesController { * * @param {Object} [config] - Options to configure controller */ - constructor({ preferences, getNativeCurrency } = {}) { + constructor({ tokensController, getNativeCurrency } = {}) { this.store = new ObservableStore(); this.getNativeCurrency = getNativeCurrency; - this.tokens = preferences.getState().tokens; - preferences.subscribe(({ tokens = [] }) => { + this.tokens = tokensController.state.tokens; + tokensController.subscribe(({ tokens = [] }) => { this.tokens = tokens; }); } diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 5a1d72611..0aa43d339 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -584,7 +584,7 @@ export default class TransactionController extends EventEmitter { return {}; } else if ( txMeta.txParams.to && - txMeta.type === TRANSACTION_TYPES.SENT_ETHER && + txMeta.type === TRANSACTION_TYPES.SIMPLE_SEND && chainType !== 'custom' ) { // if there's data in the params, but there's no contract code, it's not a valid transaction @@ -1191,7 +1191,7 @@ export default class TransactionController extends EventEmitter { } /** - * @typedef { 'transfer' | 'approve' | 'transferfrom' | 'contractInteraction'| 'sentEther' } InferrableTransactionTypes + * @typedef { 'transfer' | 'approve' | 'transferfrom' | 'contractInteraction'| 'simpleSend' } InferrableTransactionTypes */ /** @@ -1245,7 +1245,7 @@ export default class TransactionController extends EventEmitter { const codeIsEmpty = !code || code === '0x' || code === '0x0'; result = codeIsEmpty - ? TRANSACTION_TYPES.SENT_ETHER + ? TRANSACTION_TYPES.SIMPLE_SEND : TRANSACTION_TYPES.CONTRACT_INTERACTION; } @@ -1383,6 +1383,10 @@ export default class TransactionController extends EventEmitter { * @param {Object} extraParams - optional props and values to include in sensitiveProperties */ _trackTransactionMetricsEvent(txMeta, event, extraParams = {}) { + if (!txMeta) { + return; + } + const { type, time, diff --git a/app/scripts/controllers/transactions/index.test.js b/app/scripts/controllers/transactions/index.test.js index 8d12c4138..56669d267 100644 --- a/app/scripts/controllers/transactions/index.test.js +++ b/app/scripts/controllers/transactions/index.test.js @@ -716,7 +716,7 @@ describe('Transaction Controller', function () { gas: '0x7b0d', nonce: '0x4b', }, - type: 'sentEther', + type: TRANSACTION_TYPES.SIMPLE_SEND, transaction_envelope_type: 'legacy', origin: 'metamask', chainId: currentChainId, @@ -1239,7 +1239,7 @@ describe('Transaction Controller', function () { data: '', }); assert.deepEqual(result, { - type: TRANSACTION_TYPES.SENT_ETHER, + type: TRANSACTION_TYPES.SIMPLE_SEND, getCodeResponse: null, }); }); @@ -1285,7 +1285,7 @@ describe('Transaction Controller', function () { data: '0xabd', }); assert.deepEqual(result, { - type: TRANSACTION_TYPES.SENT_ETHER, + type: TRANSACTION_TYPES.SIMPLE_SEND, getCodeResponse: '0x', }); }); @@ -1296,7 +1296,7 @@ describe('Transaction Controller', function () { data: '0xabd', }); assert.deepEqual(result, { - type: TRANSACTION_TYPES.SENT_ETHER, + type: TRANSACTION_TYPES.SIMPLE_SEND, getCodeResponse: null, }); }); @@ -1499,7 +1499,7 @@ describe('Transaction Controller', function () { gas: '0x7b0d', nonce: '0x4b', }, - type: 'sentEther', + type: TRANSACTION_TYPES.SIMPLE_SEND, origin: 'metamask', chainId: currentChainId, time: 1624408066355, @@ -1513,7 +1513,7 @@ describe('Transaction Controller', function () { network: '42', referrer: 'metamask', source: 'user', - type: 'sentEther', + type: TRANSACTION_TYPES.SIMPLE_SEND, }, sensitiveProperties: { gas_price: '2', @@ -1546,7 +1546,7 @@ describe('Transaction Controller', function () { gas: '0x7b0d', nonce: '0x4b', }, - type: 'sentEther', + type: TRANSACTION_TYPES.SIMPLE_SEND, origin: 'other', chainId: currentChainId, time: 1624408066355, @@ -1560,7 +1560,7 @@ describe('Transaction Controller', function () { network: '42', referrer: 'other', source: 'dapp', - type: 'sentEther', + type: TRANSACTION_TYPES.SIMPLE_SEND, }, sensitiveProperties: { gas_price: '2', @@ -1593,7 +1593,7 @@ describe('Transaction Controller', function () { gas: '0x7b0d', nonce: '0x4b', }, - type: 'sentEther', + type: TRANSACTION_TYPES.SIMPLE_SEND, origin: 'other', chainId: currentChainId, time: 1624408066355, @@ -1606,7 +1606,7 @@ describe('Transaction Controller', function () { network: '42', referrer: 'other', source: 'dapp', - type: 'sentEther', + type: TRANSACTION_TYPES.SIMPLE_SEND, chain_id: '0x2a', }, sensitiveProperties: { @@ -1647,7 +1647,7 @@ describe('Transaction Controller', function () { gas: '0x7b0d', nonce: '0x4b', }, - type: 'sentEther', + type: TRANSACTION_TYPES.SIMPLE_SEND, origin: 'other', chainId: currentChainId, time: 1624408066355, @@ -1661,7 +1661,7 @@ describe('Transaction Controller', function () { network: '42', referrer: 'other', source: 'dapp', - type: 'sentEther', + type: TRANSACTION_TYPES.SIMPLE_SEND, }, sensitiveProperties: { baz: 3.0, diff --git a/app/scripts/controllers/transactions/pending-tx-tracker.js b/app/scripts/controllers/transactions/pending-tx-tracker.js index 52e686cf6..96e7d9793 100644 --- a/app/scripts/controllers/transactions/pending-tx-tracker.js +++ b/app/scripts/controllers/transactions/pending-tx-tracker.js @@ -136,8 +136,8 @@ export default class PendingTransactionTracker extends EventEmitter { const retryCount = txMeta.retryCount || 0; - // Exponential backoff to limit retries at publishing - if (txBlockDistance <= Math.pow(2, retryCount) - 1) { + // Exponential backoff to limit retries at publishing (capped at ~15 minutes between retries) + if (txBlockDistance < Math.min(50, Math.pow(2, retryCount))) { return undefined; } diff --git a/app/scripts/controllers/transactions/tx-state-manager.test.js b/app/scripts/controllers/transactions/tx-state-manager.test.js index e8a391faf..3c29efc10 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.test.js +++ b/app/scripts/controllers/transactions/tx-state-manager.test.js @@ -24,7 +24,7 @@ function generateTransactions( to, from, status, - type = TRANSACTION_TYPES.SENT_ETHER, + type = TRANSACTION_TYPES.SIMPLE_SEND, nonce = (i) => `${i}`, }, ) { @@ -653,7 +653,7 @@ describe('TransactionStateManager', function () { ? TRANSACTION_STATUSES.DROPPED : TRANSACTION_STATUSES.CONFIRMED, type: (i) => - i === 1 ? TRANSACTION_TYPES.CANCEL : TRANSACTION_STATUSES.SENT_ETHER, + i === 1 ? TRANSACTION_TYPES.CANCEL : TRANSACTION_TYPES.SIMPLE_SEND, }); txs.forEach((tx) => txStateManager.addTransaction(tx)); const result = txStateManager.getTransactions(); @@ -693,7 +693,7 @@ describe('TransactionStateManager', function () { type: (i) => i === 1 || i === 5 ? TRANSACTION_TYPES.CANCEL - : TRANSACTION_STATUSES.SENT_ETHER, + : TRANSACTION_TYPES.SIMPLE_SEND, }); txs.forEach((tx) => txStateManager.addTransaction(tx)); const result = txStateManager.getTransactions({ @@ -737,7 +737,7 @@ describe('TransactionStateManager', function () { type: (i) => i === 1 || i === 5 ? TRANSACTION_TYPES.CANCEL - : TRANSACTION_STATUSES.SENT_ETHER, + : TRANSACTION_TYPES.SIMPLE_SEND, }); txs.forEach((tx) => txStateManager.addTransaction(tx)); const result = txStateManager.getTransactions({ diff --git a/app/scripts/lib/ComposableObservableStore.js b/app/scripts/lib/ComposableObservableStore.js index 789a12d80..1563ae548 100644 --- a/app/scripts/lib/ComposableObservableStore.js +++ b/app/scripts/lib/ComposableObservableStore.js @@ -1,4 +1,5 @@ import { ObservableStore } from '@metamask/obs-store'; +import { getPersistentState } from '@metamask/controllers'; /** * @typedef {import('@metamask/controllers').ControllerMessenger} ControllerMessenger @@ -27,9 +28,11 @@ export default class ComposableObservableStore extends ObservableStore { * messenger, used for subscribing to events from BaseControllerV2-based * controllers. * @param {Object} [options.state] - The initial store state + * @param {boolean} [options.persist] - Wether or not to apply the persistence for v2 controllers */ - constructor({ config, controllerMessenger, state }) { + constructor({ config, controllerMessenger, state, persist }) { super(state); + this.persist = persist; this.controllerMessenger = controllerMessenger; if (config) { this.updateStructure(config); @@ -60,7 +63,11 @@ export default class ComposableObservableStore extends ObservableStore { this.controllerMessenger.subscribe( `${store.name}:stateChange`, (state) => { - this.updateState({ [key]: state }); + let updatedState = state; + if (this.persist) { + updatedState = getPersistentState(state, config[key].metadata); + } + this.updateState({ [key]: updatedState }); }, ); } diff --git a/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js b/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js index aebcae891..95408eee3 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/watch-asset.js @@ -32,7 +32,8 @@ async function watchAssetHandler( { handleWatchAssetRequest }, ) { try { - res.result = await handleWatchAssetRequest(req); + const { options: asset, type } = req.params; + res.result = await handleWatchAssetRequest(asset, type); return end(); } catch (error) { return end(error); diff --git a/app/scripts/lockdown-run.js b/app/scripts/lockdown-run.js index f0682654e..3c7276fdf 100644 --- a/app/scripts/lockdown-run.js +++ b/app/scripts/lockdown-run.js @@ -12,9 +12,103 @@ try { // If the `lockdown` call throws an exception, it interferes with the // contentscript injection on some versions of Firefox. The error is // caught and logged here so that the contentscript still gets injected. - // This affects Firefox v56 and Waterfox Classic + // This affects Firefox v56 and Waterfox Classic. console.error('Lockdown failed:', error); if (globalThis.sentry && globalThis.sentry.captureException) { - globalThis.sentry.captureException(error); + globalThis.sentry.captureException( + new Error(`Lockdown failed: ${error.message}`), + ); + } +} + +// Make all "object" and "function" own properties of globalThis +// non-configurable and non-writable, when possible. +// We call the a property that is non-configurable and non-writable, +// "non-modifiable". +try { + /** + * `lockdown` only hardens the properties enumerated by the + * universalPropertyNames constant specified in 'ses/src/whitelist'. This + * function makes all function and object properties on the start compartment + * global non-configurable and non-writable, unless they are already + * non-configurable. + * + * It is critical that this function runs at the right time during + * initialization, which should always be immediately after `lockdown` has been + * called. At the time of writing, the modifications this function makes to the + * runtime environment appear to be non-breaking, but that could change with + * the addition of dependencies, or the order of our scripts in our HTML files. + * Exercise caution. + * + * See inline comments for implementation details. + * + * We write this function in IIFE format to avoid polluting global scope. + */ + (function protectIntrinsics() { + const namedIntrinsics = Reflect.ownKeys(new Compartment().globalThis); + + // These named intrinsics are not automatically hardened by `lockdown` + const shouldHardenManually = new Set(['eval', 'Function']); + + const globalProperties = new Set([ + // universalPropertyNames is a constant added by lockdown to global scope + // at the time of writing, it is initialized in 'ses/src/whitelist'. + // These properties tend to be non-enumerable. + ...namedIntrinsics, + + // TODO: Also include the named platform globals + // This grabs every enumerable property on globalThis. + // ...Object.keys(globalThis), + ]); + + globalProperties.forEach((propertyName) => { + const descriptor = Reflect.getOwnPropertyDescriptor( + globalThis, + propertyName, + ); + + if (descriptor) { + if (descriptor.configurable) { + // If the property on globalThis is configurable, make it + // non-configurable. If it has no accessor properties, also make it + // non-writable. + if (hasAccessor(descriptor)) { + Object.defineProperty(globalThis, propertyName, { + configurable: false, + }); + } else { + Object.defineProperty(globalThis, propertyName, { + configurable: false, + writable: false, + }); + } + } + + if (shouldHardenManually.has(propertyName)) { + harden(globalThis[propertyName]); + } + } + }); + + /** + * Checks whether the given propertyName descriptor has any accessors, i.e. the + * properties `get` or `set`. + * + * We want to make globals non-writable, and we can't set the `writable` + * property and accessor properties at the same time. + * + * @param {Object} descriptor - The propertyName descriptor to check. + * @returns {boolean} Whether the propertyName descriptor has any accessors. + */ + function hasAccessor(descriptor) { + return 'set' in descriptor || 'get' in descriptor; + } + })(); +} catch (error) { + console.error('Protecting intrinsics failed:', error); + if (globalThis.sentry && globalThis.sentry.captureException) { + globalThis.sentry.captureException( + new Error(`Protecting intrinsics failed: ${error.message}`), + ); } } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 804a73542..7e045668c 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -16,7 +16,6 @@ import TrezorKeyring from 'eth-trezor-keyring'; import LedgerBridgeKeyring from '@metamask/eth-ledger-bridge-keyring'; import EthQuery from 'eth-query'; import nanoid from 'nanoid'; -import contractMap from '@metamask/contract-metadata'; import { AddressBookController, ApprovalController, @@ -26,8 +25,14 @@ import { NotificationController, GasFeeController, TokenListController, + TokensController, + TokenRatesController, } from '@metamask/controllers'; import { TRANSACTION_STATUSES } from '../../shared/constants/transaction'; +import { + GAS_API_BASE_URL, + GAS_DEV_API_BASE_URL, +} from '../../shared/constants/swaps'; import { MAINNET_CHAIN_ID } from '../../shared/constants/network'; import { KEYRING_TYPES } from '../../shared/constants/hardware-wallets'; import { UI_NOTIFICATIONS } from '../../shared/notifications'; @@ -59,7 +64,6 @@ import EncryptionPublicKeyManager from './lib/encryption-public-key-manager'; import PersonalMessageManager from './lib/personal-message-manager'; import TypedMessageManager from './lib/typed-message-manager'; import TransactionController from './controllers/transactions'; -import TokenRatesController from './controllers/token-rates'; import DetectTokensController from './controllers/detect-tokens'; import SwapsController from './controllers/swaps'; import { PermissionsController } from './controllers/permissions'; @@ -114,6 +118,7 @@ export default class MetamaskController extends EventEmitter { this.store = new ComposableObservableStore({ state: initState, controllerMessenger: this.controllerMessenger, + persist: true, }); // external connections by origin @@ -156,6 +161,17 @@ export default class MetamaskController extends EventEmitter { migrateAddressBookState: this.migrateAddressBookState.bind(this), }); + this.tokensController = new TokensController({ + onPreferencesStateChange: this.preferencesController.store.subscribe.bind( + this.preferencesController.store, + ), + onNetworkStateChange: this.networkController.store.subscribe.bind( + this.networkController.store, + ), + config: { provider: this.provider }, + state: initState.TokensController, + }); + this.metaMetricsController = new MetaMetricsController({ segment, preferencesStore: this.preferencesController.store, @@ -178,6 +194,10 @@ export default class MetamaskController extends EventEmitter { name: 'GasFeeController', }); + const gasApiBaseUrl = process.env.SWAPS_USE_DEV_APIS + ? GAS_DEV_API_BASE_URL + : GAS_API_BASE_URL; + this.gasFeeController = new GasFeeController({ interval: 10000, messenger: gasFeeMessenger, @@ -193,8 +213,8 @@ export default class MetamaskController extends EventEmitter { getCurrentAccountEIP1559Compatibility: this.getCurrentAccountEIP1559Compatibility.bind( this, ), - legacyAPIEndpoint: `https://gas-api.metaswap.codefi.network/networks//gasPrices`, - EIP1559APIEndpoint: `https://gas-api.metaswap.codefi.network/networks//suggestedGasFees`, + legacyAPIEndpoint: `${gasApiBaseUrl}/networks//gasPrices`, + EIP1559APIEndpoint: `${gasApiBaseUrl}/networks//suggestedGasFees`, getCurrentNetworkLegacyGasAPICompatibility: () => { const chainId = this.networkController.getCurrentChainId(); return process.env.IN_TEST || chainId === MAINNET_CHAIN_ID; @@ -229,8 +249,8 @@ export default class MetamaskController extends EventEmitter { }); this.tokenListController = new TokenListController({ chainId: hexToDecimal(this.networkController.getCurrentChainId()), - useStaticTokenList: this.preferencesController.store.getState() - .useStaticTokenList, + useStaticTokenList: !this.preferencesController.store.getState() + .useTokenDetection, onNetworkStateChange: (cb) => this.networkController.store.subscribe((networkState) => { const modifiedNetworkState = { @@ -242,11 +262,17 @@ export default class MetamaskController extends EventEmitter { }; return cb(modifiedNetworkState); }), - onPreferencesStateChange: this.preferencesController.store.subscribe.bind( - this.preferencesController.store, - ), + onPreferencesStateChange: (cb) => + this.preferencesController.store.subscribe((preferencesState) => { + const modifiedPreferencesState = { + ...preferencesState, + useStaticTokenList: !this.preferencesController.store.getState() + .useTokenDetection, + }; + return cb(modifiedPreferencesState); + }), messenger: tokenListMessenger, - state: initState.tokenListController, + state: initState.TokenListController, }); this.phishingController = new PhishingController(); @@ -258,11 +284,24 @@ export default class MetamaskController extends EventEmitter { // token exchange rate tracker this.tokenRatesController = new TokenRatesController({ - preferences: this.preferencesController.store, - getNativeCurrency: () => { - const { ticker } = this.networkController.getProviderConfig(); - return ticker ?? 'ETH'; - }, + onTokensStateChange: (listener) => + this.tokensController.subscribe(listener), + onCurrencyRateStateChange: (listener) => + this.controllerMessenger.subscribe( + `${this.currencyRateController.name}:stateChange`, + listener, + ), + onNetworkStateChange: (cb) => + this.networkController.store.subscribe((networkState) => { + const modifiedNetworkState = { + ...networkState, + provider: { + ...networkState.provider, + chainId: hexToDecimal(networkState.provider.chainId), + }, + }; + return cb(modifiedNetworkState); + }), }); this.ensController = new EnsController({ @@ -303,13 +342,11 @@ export default class MetamaskController extends EventEmitter { if (activeControllerConnections > 0) { this.accountTracker.start(); this.incomingTransactionsController.start(); - this.tokenRatesController.start(); this.currencyRateController.start(); this.tokenListController.start(); } else { this.accountTracker.stop(); this.incomingTransactionsController.stop(); - this.tokenRatesController.stop(); this.currencyRateController.stop(); this.tokenListController.stop(); } @@ -328,6 +365,10 @@ export default class MetamaskController extends EventEmitter { preferencesController: this.preferencesController, }); + this.tokensController.hub.on('pendingSuggestedAsset', async () => { + await opts.openPopup(); + }); + const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring]; this.keyringController = new KeyringController({ keyringTypes: additionalKeyrings, @@ -361,8 +402,10 @@ export default class MetamaskController extends EventEmitter { this.detectTokensController = new DetectTokensController({ preferences: this.preferencesController, + tokensController: this.tokensController, network: this.networkController, keyringMemStore: this.keyringController.memStore, + tokenList: this.tokenListController, }); this.addressBookController = new AddressBookController( @@ -494,7 +537,7 @@ export default class MetamaskController extends EventEmitter { getProviderConfig: this.networkController.getProviderConfig.bind( this.networkController, ), - tokenRatesStore: this.tokenRatesController.store, + tokenRatesStore: this.tokenRatesController.state, getCurrentChainId: this.networkController.getCurrentChainId.bind( this.networkController, ), @@ -540,6 +583,7 @@ export default class MetamaskController extends EventEmitter { NotificationController: this.notificationController, GasFeeController: this.gasFeeController, TokenListController: this.tokenListController, + TokensController: this.tokensController, }); this.memStore = new ComposableObservableStore({ @@ -549,7 +593,7 @@ export default class MetamaskController extends EventEmitter { AccountTracker: this.accountTracker.store, TxController: this.txController.memStore, CachedBalancesController: this.cachedBalancesController.store, - TokenRatesController: this.tokenRatesController.store, + TokenRatesController: this.tokenRatesController, MessageManager: this.messageManager.memStore, PersonalMessageManager: this.personalMessageManager.memStore, DecryptMessageManager: this.decryptMessageManager.memStore, @@ -573,6 +617,7 @@ export default class MetamaskController extends EventEmitter { NotificationController: this.notificationController, GasFeeController: this.gasFeeController, TokenListController: this.tokenListController, + TokensController: this.tokensController, }, controllerMessenger: this.controllerMessenger, }); @@ -753,6 +798,7 @@ export default class MetamaskController extends EventEmitter { swapsController, threeBoxController, txController, + tokensController, } = this; return { @@ -766,8 +812,8 @@ export default class MetamaskController extends EventEmitter { setUseBlockie: this.setUseBlockie.bind(this), setUseNonceField: this.setUseNonceField.bind(this), setUsePhishDetect: this.setUsePhishDetect.bind(this), - setUseStaticTokenList: nodeify( - this.preferencesController.setUseStaticTokenList, + setUseTokenDetection: nodeify( + this.preferencesController.setUseTokenDetection, this.preferencesController, ), setIpfsGateway: this.setIpfsGateway.bind(this), @@ -822,18 +868,22 @@ export default class MetamaskController extends EventEmitter { preferencesController.setSelectedAddress, preferencesController, ), - addToken: nodeify(preferencesController.addToken, preferencesController), + addToken: nodeify(tokensController.addToken, tokensController), + rejectWatchAsset: nodeify( + tokensController.rejectWatchAsset, + tokensController, + ), + acceptWatchAsset: nodeify( + tokensController.acceptWatchAsset, + tokensController, + ), updateTokenType: nodeify( - preferencesController.updateTokenType, - preferencesController, + tokensController.updateTokenType, + tokensController, ), removeToken: nodeify( - preferencesController.removeToken, - preferencesController, - ), - removeSuggestedTokens: nodeify( - preferencesController.removeSuggestedTokens, - preferencesController, + tokensController.removeAndIgnoreToken, + tokensController, ), setAccountLabel: nodeify( preferencesController.setAccountLabel, @@ -1035,6 +1085,10 @@ export default class MetamaskController extends EventEmitter { swapsController, ), setSwapsTokens: nodeify(swapsController.setSwapsTokens, swapsController), + clearSwapsQuotes: nodeify( + swapsController.clearSwapsQuotes, + swapsController, + ), setApproveTxId: nodeify(swapsController.setApproveTxId, swapsController), setTradeTxId: nodeify(swapsController.setTradeTxId, swapsController), setSwapsTxGasPrice: nodeify( @@ -1089,6 +1143,10 @@ export default class MetamaskController extends EventEmitter { swapsController.setSwapsUserFeeLevel, swapsController, ), + setSwapsQuotesPollingLimitEnabled: nodeify( + swapsController.setSwapsQuotesPollingLimitEnabled, + swapsController, + ), // MetaMetrics trackMetaMetricsEvent: nodeify( @@ -1141,6 +1199,12 @@ export default class MetamaskController extends EventEmitter { this.appStateController.removePollingToken, this.appStateController, ), + + // DetectTokenController + detectNewTokens: nodeify( + this.detectTokensController.detectNewTokens, + this.detectTokensController, + ), }; } @@ -1239,6 +1303,12 @@ export default class MetamaskController extends EventEmitter { ); } + // remove extra zero balance account potentially created from seeking ahead + if (accounts.length > 1 && lastBalance === '0x0') { + await this.removeAccount(accounts[accounts.length - 1]); + accounts = await keyringController.getAccounts(); + } + // set new identities this.preferencesController.setAddresses(accounts); this.selectFirstIdentity(); @@ -1280,43 +1350,53 @@ export default class MetamaskController extends EventEmitter { async fetchInfoToSync() { // Preferences const { - accountTokens, currentLocale, frequentRpcList, identities, selectedAddress, - tokens, + useTokenDetection, } = this.preferencesController.store.getState(); - // Filter ERC20 tokens - const filteredAccountTokens = {}; - Object.keys(accountTokens).forEach((address) => { - const checksummedAddress = toChecksumHexAddress(address); - filteredAccountTokens[checksummedAddress] = {}; - Object.keys(accountTokens[address]).forEach((chainId) => { - filteredAccountTokens[checksummedAddress][chainId] = - chainId === MAINNET_CHAIN_ID - ? accountTokens[address][chainId].filter( - ({ address: tokenAddress }) => { - const checksumAddress = toChecksumHexAddress(tokenAddress); - return contractMap[checksumAddress] - ? contractMap[checksumAddress].erc20 - : true; - }, - ) - : accountTokens[address][chainId]; - }); - }); + const { tokenList } = this.tokenListController.state; const preferences = { - accountTokens: filteredAccountTokens, currentLocale, frequentRpcList, identities, selectedAddress, - tokens, }; + // Tokens + const { allTokens, allIgnoredTokens } = this.tokensController.state; + + // Filter ERC20 tokens + const allERC20Tokens = {}; + + Object.keys(allTokens).forEach((chainId) => { + allERC20Tokens[chainId] = {}; + Object.keys(allTokens[chainId]).forEach((accountAddress) => { + const checksummedAccountAddress = toChecksumHexAddress(accountAddress); + allERC20Tokens[chainId][checksummedAccountAddress] = allTokens[chainId][ + checksummedAccountAddress + ].filter((asset) => { + if (asset.isERC721 === undefined) { + // since the token.address from allTokens is checksumaddress + // asset.address have to be changed to lowercase when we are using dynamic list + const address = useTokenDetection + ? asset.address.toLowerCase() + : asset.address; + // the tokenList will be holding only erc20 tokens + if (tokenList[address] !== undefined) { + return true; + } + } else if (asset.isERC721 === false) { + return true; + } + return false; + }); + }); + }); + // Accounts const hdKeyring = this.keyringController.getKeyringsByType( 'HD Key Tree', @@ -1356,6 +1436,7 @@ export default class MetamaskController extends EventEmitter { accounts, preferences, transactions, + tokens: { allTokens: allERC20Tokens, allIgnoredTokens }, network: this.networkController.store.getState(), }; } @@ -2371,8 +2452,8 @@ export default class MetamaskController extends EventEmitter { sendMetrics: this.metaMetricsController.trackEvent.bind( this.metaMetricsController, ), - handleWatchAssetRequest: this.preferencesController.requestWatchAsset.bind( - this.preferencesController, + handleWatchAssetRequest: this.tokensController.watchAsset.bind( + this.tokensController, ), getWeb3ShimUsageState: this.alertController.getWeb3ShimUsageState.bind( this.alertController, diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js index ced33df13..edb662bf6 100644 --- a/app/scripts/metamask-controller.test.js +++ b/app/scripts/metamask-controller.test.js @@ -91,7 +91,6 @@ const MetaMaskController = proxyquire('./metamask-controller', { const currentNetworkId = '42'; const DEFAULT_LABEL = 'Account 1'; -const DEFAULT_LABEL_2 = 'Account 2'; const TEST_SEED = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium'; const TEST_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'; @@ -362,7 +361,7 @@ describe('MetaMaskController', function () { }); }); - it('should restore any consecutive accounts with balances', async function () { + it('should restore any consecutive accounts with balances without extra zero balance accounts', async function () { sandbox.stub(metamaskController, 'getBalance'); metamaskController.getBalance.withArgs(TEST_ADDRESS).callsFake(() => { return Promise.resolve('0x14ced5122ce0a000'); @@ -388,7 +387,6 @@ describe('MetaMaskController', function () { delete identities[TEST_ADDRESS].lastSelected; assert.deepEqual(identities, { [TEST_ADDRESS]: { address: TEST_ADDRESS, name: DEFAULT_LABEL }, - [TEST_ADDRESS_2]: { address: TEST_ADDRESS_2, name: DEFAULT_LABEL_2 }, }); }); }); diff --git a/app/scripts/migrations/053.js b/app/scripts/migrations/053.js index f9d4dc55c..318ce1614 100644 --- a/app/scripts/migrations/053.js +++ b/app/scripts/migrations/053.js @@ -23,23 +23,27 @@ function transformState(state) { state?.IncomingTransactionsController?.incomingTransactions; if (Array.isArray(transactions)) { transactions.forEach((transaction) => { - if ( - transaction.type !== TRANSACTION_TYPES.RETRY && - transaction.type !== TRANSACTION_TYPES.CANCEL - ) { - transaction.type = transaction.transactionCategory; + if (transaction) { + if ( + transaction.type !== TRANSACTION_TYPES.RETRY && + transaction.type !== TRANSACTION_TYPES.CANCEL + ) { + transaction.type = transaction.transactionCategory; + } + delete transaction.transactionCategory; } - delete transaction.transactionCategory; }); } if (incomingTransactions) { const incomingTransactionsEntries = Object.entries(incomingTransactions); incomingTransactionsEntries.forEach(([key, transaction]) => { - delete transaction.transactionCategory; - state.IncomingTransactionsController.incomingTransactions[key] = { - ...transaction, - type: TRANSACTION_TYPES.INCOMING, - }; + if (transaction) { + delete transaction.transactionCategory; + state.IncomingTransactionsController.incomingTransactions[key] = { + ...transaction, + type: TRANSACTION_TYPES.INCOMING, + }; + } }); } return state; diff --git a/app/scripts/migrations/053.test.js b/app/scripts/migrations/053.test.js index 788110afa..324fa3741 100644 --- a/app/scripts/migrations/053.test.js +++ b/app/scripts/migrations/053.test.js @@ -2,6 +2,8 @@ import { strict as assert } from 'assert'; import { TRANSACTION_TYPES } from '../../../shared/constants/transaction'; import migration53 from './053'; +const SENT_ETHER = 'sentEther'; // a legacy transaction type replaced now by TRANSACTION_TYPES.SIMPLE_SEND + describe('migration #53', function () { it('should update the version metadata', async function () { const oldStorage = { @@ -25,12 +27,12 @@ describe('migration #53', function () { transactions: [ { type: TRANSACTION_TYPES.CANCEL, - transactionCategory: TRANSACTION_TYPES.SENT_ETHER, + transactionCategory: SENT_ETHER, txParams: { foo: 'bar' }, }, { type: 'standard', - transactionCategory: TRANSACTION_TYPES.SENT_ETHER, + transactionCategory: SENT_ETHER, txParams: { foo: 'bar' }, }, { @@ -40,7 +42,7 @@ describe('migration #53', function () { }, { type: TRANSACTION_TYPES.RETRY, - transactionCategory: TRANSACTION_TYPES.SENT_ETHER, + transactionCategory: SENT_ETHER, txParams: { foo: 'bar' }, }, ], @@ -64,7 +66,10 @@ describe('migration #53', function () { TransactionController: { transactions: [ { type: TRANSACTION_TYPES.CANCEL, txParams: { foo: 'bar' } }, - { type: TRANSACTION_TYPES.SENT_ETHER, txParams: { foo: 'bar' } }, + { + type: SENT_ETHER, + txParams: { foo: 'bar' }, + }, { type: TRANSACTION_TYPES.CONTRACT_INTERACTION, txParams: { foo: 'bar' }, diff --git a/app/scripts/migrations/056.js b/app/scripts/migrations/056.js index f11d4b3f1..70300f363 100644 --- a/app/scripts/migrations/056.js +++ b/app/scripts/migrations/056.js @@ -15,14 +15,14 @@ export default { const { PreferencesController } = versionedData.data; - if (Array.isArray(PreferencesController.tokens)) { + if (Array.isArray(PreferencesController?.tokens)) { PreferencesController.tokens = PreferencesController.tokens.filter( ({ address }) => address, ); } if ( - PreferencesController.accountTokens && + PreferencesController?.accountTokens && typeof PreferencesController.accountTokens === 'object' ) { Object.keys(PreferencesController.accountTokens).forEach((account) => { @@ -40,7 +40,7 @@ export default { } if ( - PreferencesController.assetImages && + PreferencesController?.assetImages && 'undefined' in PreferencesController.assetImages ) { delete PreferencesController.assetImages.undefined; diff --git a/app/scripts/migrations/059.test.js b/app/scripts/migrations/059.test.js index bdf4263c2..a9a06c2fd 100644 --- a/app/scripts/migrations/059.test.js +++ b/app/scripts/migrations/059.test.js @@ -12,6 +12,8 @@ import { } from '../../../shared/constants/transaction'; import migration59 from './059'; +const SENT_ETHER = 'sentEther'; // a legacy transaction type replaced now by TRANSACTION_TYPES.SIMPLE_SEND + const ERRONEOUS_TRANSACTION_STATE = { 0: { type: TRANSACTION_TYPES.CANCEL, @@ -22,7 +24,7 @@ const ERRONEOUS_TRANSACTION_STATE = { }, }, 1: { - type: TRANSACTION_TYPES.SENT_ETHER, + type: SENT_ETHER, id: 1, chainId: MAINNET_CHAIN_ID, txParams: { @@ -30,7 +32,7 @@ const ERRONEOUS_TRANSACTION_STATE = { }, }, 2: { - type: TRANSACTION_TYPES.SENT_ETHER, + type: SENT_ETHER, id: 2, chainId: KOVAN_CHAIN_ID, txParams: { @@ -38,7 +40,7 @@ const ERRONEOUS_TRANSACTION_STATE = { }, }, 3: { - type: TRANSACTION_TYPES.SENT_ETHER, + type: SENT_ETHER, id: 3, chainId: RINKEBY_CHAIN_ID, txParams: { @@ -46,7 +48,7 @@ const ERRONEOUS_TRANSACTION_STATE = { }, }, 4: { - type: TRANSACTION_TYPES.SENT_ETHER, + type: SENT_ETHER, id: 4, chainId: RINKEBY_CHAIN_ID, txParams: { @@ -54,7 +56,7 @@ const ERRONEOUS_TRANSACTION_STATE = { }, }, 5: { - type: TRANSACTION_TYPES.SENT_ETHER, + type: SENT_ETHER, id: 5, chainId: MAINNET_CHAIN_ID, txParams: { @@ -62,7 +64,7 @@ const ERRONEOUS_TRANSACTION_STATE = { }, }, 6: { - type: TRANSACTION_TYPES.SENT_ETHER, + type: SENT_ETHER, id: 6, chainId: KOVAN_CHAIN_ID, txParams: { @@ -70,7 +72,7 @@ const ERRONEOUS_TRANSACTION_STATE = { }, }, 7: { - type: TRANSACTION_TYPES.SENT_ETHER, + type: SENT_ETHER, id: 7, chainId: RINKEBY_CHAIN_ID, txParams: { @@ -78,7 +80,7 @@ const ERRONEOUS_TRANSACTION_STATE = { }, }, 8: { - type: TRANSACTION_TYPES.SENT_ETHER, + type: SENT_ETHER, id: 8, chainId: RINKEBY_CHAIN_ID, txParams: { @@ -86,7 +88,7 @@ const ERRONEOUS_TRANSACTION_STATE = { }, }, 9: { - type: TRANSACTION_TYPES.SENT_ETHER, + type: SENT_ETHER, id: 9, chainId: RINKEBY_CHAIN_ID, status: TRANSACTION_STATUSES.UNAPPROVED, @@ -169,7 +171,7 @@ describe('migration #59', function () { ...ERRONEOUS_TRANSACTION_STATE['0'], id: 11, chainId: GOERLI_CHAIN_ID, - type: TRANSACTION_TYPES.SENT_ETHER, + type: SENT_ETHER, }, }, }, @@ -200,7 +202,7 @@ describe('migration #59', function () { 11: { ...ERRONEOUS_TRANSACTION_STATE['0'], id: 11, - type: TRANSACTION_TYPES.SENT_ETHER, + type: SENT_ETHER, }, }, }, @@ -250,7 +252,7 @@ describe('migration #59', function () { ...ERRONEOUS_TRANSACTION_STATE_RETRY['0'], id: 11, chainId: GOERLI_CHAIN_ID, - type: TRANSACTION_TYPES.SENT_ETHER, + type: SENT_ETHER, }, }, }, @@ -281,7 +283,7 @@ describe('migration #59', function () { 11: { ...ERRONEOUS_TRANSACTION_STATE_RETRY['0'], id: 11, - type: TRANSACTION_TYPES.SENT_ETHER, + type: SENT_ETHER, }, }, }, diff --git a/app/scripts/migrations/063.js b/app/scripts/migrations/063.js new file mode 100644 index 000000000..95985bf37 --- /dev/null +++ b/app/scripts/migrations/063.js @@ -0,0 +1,78 @@ +import { cloneDeep } from 'lodash'; + +const version = 63; + +/** + * Moves token state from preferences controller to TokensController + */ +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 accountTokens = state?.PreferencesController?.accountTokens; + const accountHiddenTokens = state?.PreferencesController?.accountHiddenTokens; + + const newAllTokens = {}; + if (accountTokens) { + Object.keys(accountTokens).forEach((accountAddress) => { + Object.keys(accountTokens[accountAddress]).forEach((chainId) => { + const tokensArray = accountTokens[accountAddress][chainId]; + if (newAllTokens[chainId] === undefined) { + newAllTokens[chainId] = { [accountAddress]: tokensArray }; + } else { + newAllTokens[chainId] = { + ...newAllTokens[chainId], + [accountAddress]: tokensArray, + }; + } + }); + }); + } + + const newAllIgnoredTokens = {}; + if (accountHiddenTokens) { + Object.keys(accountHiddenTokens).forEach((accountAddress) => { + Object.keys(accountHiddenTokens[accountAddress]).forEach((chainId) => { + const ignoredTokensArray = accountHiddenTokens[accountAddress][chainId]; + if (newAllIgnoredTokens[chainId] === undefined) { + newAllIgnoredTokens[chainId] = { + [accountAddress]: ignoredTokensArray, + }; + } else { + newAllIgnoredTokens[chainId] = { + ...newAllIgnoredTokens[chainId], + [accountAddress]: ignoredTokensArray, + }; + } + }); + }); + } + + if (state.TokensController) { + state.TokensController.allTokens = newAllTokens; + state.TokensController.allIgnoredTokens = newAllIgnoredTokens; + } else { + state.TokensController = { + allTokens: newAllTokens, + allIgnoredTokens: newAllIgnoredTokens, + }; + } + + delete state?.PreferencesController?.accountHiddenTokens; + delete state?.PreferencesController?.accountTokens; + delete state?.PreferencesController?.assetImages; + delete state?.PreferencesController?.hiddenTokens; + delete state?.PreferencesController?.tokens; + delete state?.PreferencesController?.suggestedTokens; + + return state; +} diff --git a/app/scripts/migrations/063.test.js b/app/scripts/migrations/063.test.js new file mode 100644 index 000000000..30804a35c --- /dev/null +++ b/app/scripts/migrations/063.test.js @@ -0,0 +1,251 @@ +import { strict as assert } from 'assert'; +import migration63 from './063'; + +describe('migration #63', function () { + it('should update the version metadata', async function () { + const oldStorage = { + meta: { + version: 62, + }, + data: {}, + }; + + const newStorage = await migration63.migrate(oldStorage); + assert.deepEqual(newStorage.meta, { + version: 63, + }); + }); + + it('should move accountTokens data from PreferencesController to TokensController allTokens field and rotate structure from [accountAddress][chainId] to [chainId][accountAddress]', async function () { + const oldAccountTokens = { + '0x00000000000': { + '0x1': [ + { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + decimals: 18, + isERC721: false, + symbol: 'DAI', + }, + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + decimals: 18, + isERC721: false, + symbol: 'UNI', + }, + ], + '0x89': [ + { + address: '0x70d1f773a9f81c852087b77f6ae6d3032b02d2ab', + decimals: 18, + isERC721: false, + symbol: 'LINK', + }, + { + address: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f', + decimals: 6, + isERC721: false, + symbol: 'USDT', + }, + ], + }, + '0x1111111111': { + '0x1': [ + { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + decimals: 18, + isERC721: false, + symbol: 'FAI', + }, + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + decimals: 18, + isERC721: false, + symbol: 'PUNI', + }, + ], + '0x89': [ + { + address: '0x70d1f773a9f81c852087b77f6ae6d3032b02d2ab', + decimals: 18, + isERC721: false, + symbol: 'SLINK', + }, + { + address: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f', + decimals: 6, + isERC721: false, + symbol: 'USDC', + }, + ], + }, + }; + + const expectedTokens = { + '0x1': { + '0x00000000000': [ + { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + decimals: 18, + isERC721: false, + symbol: 'DAI', + }, + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + decimals: 18, + isERC721: false, + symbol: 'UNI', + }, + ], + '0x1111111111': [ + { + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + decimals: 18, + isERC721: false, + symbol: 'FAI', + }, + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + decimals: 18, + isERC721: false, + symbol: 'PUNI', + }, + ], + }, + '0x89': { + '0x00000000000': [ + { + address: '0x70d1f773a9f81c852087b77f6ae6d3032b02d2ab', + decimals: 18, + isERC721: false, + symbol: 'LINK', + }, + { + address: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f', + decimals: 6, + isERC721: false, + symbol: 'USDT', + }, + ], + '0x1111111111': [ + { + address: '0x70d1f773a9f81c852087b77f6ae6d3032b02d2ab', + decimals: 18, + isERC721: false, + symbol: 'SLINK', + }, + { + address: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f', + decimals: 6, + isERC721: false, + symbol: 'USDC', + }, + ], + }, + }; + + const oldStorage = { + meta: {}, + data: { + PreferencesController: { + completedOnboarding: true, + dismissSeedBackUpReminder: false, + accountTokens: oldAccountTokens, + }, + }, + }; + + const newStorage = await migration63.migrate(oldStorage); + assert.deepStrictEqual(newStorage.data, { + TokensController: { + allTokens: expectedTokens, + allIgnoredTokens: {}, + }, + PreferencesController: { + completedOnboarding: true, + dismissSeedBackUpReminder: false, + }, + }); + }); + + it('should move accountHiddenTokens data from PreferencesController to TokensController allIgnoredTokens field and rotate structure from [accountAddress][chainId] to [chainId][accountAddress]', async function () { + const oldStorage = { + meta: {}, + data: { + PreferencesController: { + completedOnboarding: true, + dismissSeedBackUpReminder: false, + accountTokens: {}, + accountHiddenTokens: { + '0x1111111111': { + '0x1': ['0x000000000000'], + '0x89': ['0x11111111111'], + }, + '0x222222': { + '0x4': ['0x000011112222'], + }, + '0x333333': { + '0x5': ['0x000022223333'], + '0x1': ['0x000033333344'], + }, + }, + }, + }, + }; + + const newStorage = await migration63.migrate(oldStorage); + assert.deepStrictEqual(newStorage.data, { + TokensController: { + allTokens: {}, + allIgnoredTokens: { + '0x1': { + '0x1111111111': ['0x000000000000'], + '0x333333': ['0x000033333344'], + }, + '0x89': { + '0x1111111111': ['0x11111111111'], + }, + '0x4': { + '0x222222': ['0x000011112222'], + }, + '0x5': { + '0x333333': ['0x000022223333'], + }, + }, + }, + PreferencesController: { + completedOnboarding: true, + dismissSeedBackUpReminder: false, + }, + }); + }); + + it('should should remove all token related state from the preferences controller', async function () { + const oldStorage = { + meta: {}, + data: { + PreferencesController: { + completedOnboarding: true, + dismissSeedBackUpReminder: false, + accountTokens: {}, + accountHiddenTokens: {}, + tokens: {}, + hiddenTokens: {}, + assetImages: {}, + suggestedTokens: {}, + }, + }, + }; + + const newStorage = await migration63.migrate(oldStorage); + assert.deepStrictEqual(newStorage.data, { + PreferencesController: { + completedOnboarding: true, + dismissSeedBackUpReminder: false, + }, + TokensController: { + allTokens: {}, + allIgnoredTokens: {}, + }, + }); + }); +}); diff --git a/app/scripts/migrations/064.js b/app/scripts/migrations/064.js new file mode 100644 index 000000000..01c384234 --- /dev/null +++ b/app/scripts/migrations/064.js @@ -0,0 +1,41 @@ +import { cloneDeep, isPlainObject } from 'lodash'; +import { TRANSACTION_TYPES } from '../../../shared/constants/transaction'; + +const version = 64; + +const SENT_ETHER = 'sentEther'; // the legacy transaction type being replaced in this migration with TRANSACTION_TYPES.SIMPLE_SEND + +/** + * Removes metaMetricsSendCount from MetaMetrics controller + */ +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 transactions = state?.TransactionController?.transactions; + if (isPlainObject(transactions)) { + for (const tx of Object.values(transactions)) { + if (tx.type === SENT_ETHER) { + tx.type = TRANSACTION_TYPES.SIMPLE_SEND; + } + if (tx.history) { + tx.history.map((txEvent) => { + if (txEvent.type && txEvent.type === SENT_ETHER) { + txEvent.type = TRANSACTION_TYPES.SIMPLE_SEND; + } + return txEvent; + }); + } + } + } + return state; +} diff --git a/app/scripts/migrations/064.test.js b/app/scripts/migrations/064.test.js new file mode 100644 index 000000000..25d268bdb --- /dev/null +++ b/app/scripts/migrations/064.test.js @@ -0,0 +1,469 @@ +import { strict as assert } from 'assert'; +import { TRANSACTION_TYPES } from '../../../shared/constants/transaction'; +import { MAINNET_CHAIN_ID } from '../../../shared/constants/network'; +import migration64 from './064'; + +const SENT_ETHER = 'sentEther'; // the legacy transaction type being replaced in this migration with TRANSACTION_TYPES.SIMPLE_SEND + +describe('migration #64', function () { + it('should update the version metadata', async function () { + const oldStorage = { + meta: { + version: 63, + }, + data: {}, + }; + + const newStorage = await migration64.migrate(oldStorage); + assert.deepEqual(newStorage.meta, { + version: 64, + }); + }); + + it('should do nothing if transactions state does not exist', async function () { + const oldStorage = { + meta: {}, + data: { + TransactionController: { + bar: 'baz', + }, + IncomingTransactionsController: { + foo: 'baz', + }, + foo: 'bar', + }, + }; + + const newStorage = await migration64.migrate(oldStorage); + assert.deepEqual(oldStorage.data, newStorage.data); + }); + + it('should do nothing if transactions state is empty', async function () { + const oldStorage = { + meta: {}, + data: { + TransactionController: { + transactions: {}, + bar: 'baz', + }, + foo: 'bar', + }, + }; + + const newStorage = await migration64.migrate(oldStorage); + assert.deepEqual(oldStorage.data, newStorage.data); + }); + + it('should do nothing if transactions state is not an object', async function () { + const oldStorage = { + meta: {}, + data: { + TransactionController: { + transactions: [], + bar: 'baz', + }, + foo: 'bar', + }, + }; + + const newStorage = await migration64.migrate(oldStorage); + assert.deepEqual(oldStorage.data, newStorage.data); + }); + + it('should do nothing if state is empty', async function () { + const oldStorage = { + meta: {}, + data: {}, + }; + + const newStorage = await migration64.migrate(oldStorage); + assert.deepEqual(oldStorage.data, newStorage.data); + }); + + it('should change action type of "sentEther" to "simpleSend" for any transactions and transaction history events in transactionsController.transactions', async function () { + const OLD_TRANSACTION_STATE = { + 1462177651588364: { + type: TRANSACTION_TYPES.CANCEL, + id: 0, + chainId: MAINNET_CHAIN_ID, + txParams: { + nonce: '0x0', + }, + origin: 'https://metamask.github.io', + r: '0x29f00dda09306f0f09895e80db110b9348eeb57d3e0b386409bfb674041ba45a', + rawTx: + '0x02f902fc04278459682f008459682f10830314138080b902a3608060405234801561001057600080fd5b5033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506000808190555061023b806100686000396000f300608060405260043610610057576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e1a7d4d1461005c5780638da5cb5b1461009d578063d0e30db0146100f4575b600080fd5b34801561006857600080fd5b5061008760048036038101908080359060200190929190505050610112565b6040518082815260200191505060405180910390f35b3480156100a957600080fd5b506100b26101d0565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100fc6101f6565b6040518082815260200191505060405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614151561017057600080fd5b8160008082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc839081150290604051600060405180830381858888f193505050501580156101c5573d6000803e3d6000fd5b506000549050919050565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60003460008082825401925050819055506000549050905600a165627a7a72305820f237db3ec816a52589d82512117bc85bc08d3537683ffeff9059108caf3e5d400029c001a029f00dda09306f0f09895e80db110b9348eeb57d3e0b386409bfb674041ba45aa049f74084dd8c517b305a2e60b39ae9002176a5244cb06de8f9ea3757811f5ec6', + s: '0x49f74084dd8c517b305a2e60b39ae9002176a5244cb06de8f9ea3757811f5ec6', + status: 'confirmed', + estimatedBaseFee: 'b', + hash: + '0x4d8543f12afd3795b94d723dcd0e20bfc3740e1af668e5e90a0c5ec49f36ba12', + }, + 1: { + type: SENT_ETHER, + id: 1, + chainId: MAINNET_CHAIN_ID, + txParams: { + nonce: '0x1', + }, + history: [ + { + chainId: '0x4', + dappSuggestedGasFees: { + gas: '0x31413', + }, + id: 1462177651588364, + loadingDefaults: true, + metamaskNetworkId: '4', + origin: 'https://metamask.github.io', + status: 'unapproved', + time: 1631118004776, + txParams: { + data: + '0x608060405234801561001057600080fd5b5033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506000808190555061023b806100686000396000f300608060405260043610610057576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e1a7d4d1461005c5780638da5cb5b1461009d578063d0e30db0146100f4575b600080fd5b34801561006857600080fd5b5061008760048036038101908080359060200190929190505050610112565b6040518082815260200191505060405180910390f35b3480156100a957600080fd5b506100b26101d0565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100fc6101f6565b6040518082815260200191505060405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614151561017057600080fd5b8160008082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc839081150290604051600060405180830381858888f193505050501580156101c5573d6000803e3d6000fd5b506000549050919050565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60003460008082825401925050819055506000549050905600a165627a7a72305820f237db3ec816a52589d82512117bc85bc08d3537683ffeff9059108caf3e5d400029', + from: '0x0f002c95c041f003be01c3e4f52cae1f6ab3ba6e', + gas: '0x31413', + value: '0x0', + }, + type: SENT_ETHER, + }, + [ + { + note: 'Added new unapproved transaction.', + op: 'add', + path: '/txParams/maxFeePerGas', + timestamp: 1631118004862, + value: '0x59682f10', + }, + { + op: 'add', + path: '/txParams/maxPriorityFeePerGas', + value: '0x59682f00', + }, + { + op: 'replace', + path: '/loadingDefaults', + value: false, + }, + { + op: 'add', + path: '/userFeeLevel', + value: 'medium', + }, + ], + ], + }, + 3274396743040791: { + baseFeePerGas: '0xb', + chainId: '0x4', + dappSuggestedGasFees: { + gas: '0xa9fe', + }, + estimatedBaseFee: 'b', + hash: + '0x19ffab8a9467df9afbef82d8907f9e39f0696c7a774ed5473ecf7337adcc674b', + origin: 'https://metamask.github.io', + r: '0xc2b2901f3593536d21e9b136c469b9b8f91a944f18a29a3cdf3a2eaadf660e71', + rawTx: + '0x02f87604288459682f008459682f1082a9fe949ef57335bc7d5b6cbc06dca6064a604b75e09ace883782dace9d90000084d0e30db0c001a0c2b2901f3593536d21e9b136c469b9b8f91a944f18a29a3cdf3a2eaadf660e71a057876a0292d548dd67c6faed8e835b94252b55a043ce01a1206361ccab417ad4', + s: '0x57876a0292d548dd67c6faed8e835b94252b55a043ce01a1206361ccab417ad4', + status: 'confirmed', + submittedTime: 1631118228493, + time: 1631118217596, + txParams: { + data: '0xd0e30db0', + from: '0x0f002c95c041f003be01c3e4f52cae1f6ab3ba6e', + gas: '0xa9fe', + maxFeePerGas: '0x59682f10', + maxPriorityFeePerGas: '0x59682f00', + nonce: '0x28', + to: '0x9ef57335bc7d5b6cbc06dca6064a604b75e09ace', + value: '0x3782dace9d900000', + }, + txReceipt: { + blockHash: + '0xafa4e1fd95e429d9c6e6c7c1d282b2bd0bbeb50d0a68743e9392b9c95a06e2eb', + blockNumber: { + length: 1, + negative: 0, + red: null, + words: [9257603, null], + }, + contractAddress: null, + cumulativeGasUsed: { + length: 1, + negative: 0, + red: null, + words: [4954851, null], + }, + effectiveGasPrice: '0x59682f0b', + from: '0x0f002c95c041f003be01c3e4f52cae1f6ab3ba6e', + gasUsed: 'a9fe', + logs: [], + logsBloom: + '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + status: '0x1', + to: '0x9ef57335bc7d5b6cbc06dca6064a604b75e09ace', + transactionHash: + '0x19ffab8a9467df9afbef82d8907f9e39f0696c7a774ed5473ecf7337adcc674b', + transactionIndex: { + length: 1, + negative: 0, + red: null, + words: [9, null], + }, + type: '0x2', + }, + type: SENT_ETHER, + history: [ + { + chainId: '0x4', + dappSuggestedGasFees: { + gas: '0xa9fe', + }, + id: 3274396743040791, + loadingDefaults: true, + metamaskNetworkId: '4', + origin: 'https://metamask.github.io', + status: 'unapproved', + time: 1631118217596, + txParams: { + data: '0xd0e30db0', + from: '0x0f002c95c041f003be01c3e4f52cae1f6ab3ba6e', + gas: '0xa9fe', + to: '0x9ef57335bc7d5b6cbc06dca6064a604b75e09ace', + value: '0x3782dace9d900000', + }, + }, + [ + { + note: 'Added new unapproved transaction.', + op: 'add', + path: '/txParams/maxFeePerGas', + timestamp: 1631118217762, + value: '0x59682f10', + }, + { + op: 'add', + path: '/txParams/maxPriorityFeePerGas', + value: '0x59682f00', + }, + { + op: 'replace', + path: '/loadingDefaults', + value: false, + }, + { + op: 'add', + path: '/userFeeLevel', + value: 'medium', + }, + ], + ], + }, + }; + + const EXPECTED_TRANSACTION_STATE = { + 1462177651588364: { + type: TRANSACTION_TYPES.CANCEL, + id: 0, + chainId: MAINNET_CHAIN_ID, + txParams: { + nonce: '0x0', + }, + origin: 'https://metamask.github.io', + r: '0x29f00dda09306f0f09895e80db110b9348eeb57d3e0b386409bfb674041ba45a', + rawTx: + '0x02f902fc04278459682f008459682f10830314138080b902a3608060405234801561001057600080fd5b5033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506000808190555061023b806100686000396000f300608060405260043610610057576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e1a7d4d1461005c5780638da5cb5b1461009d578063d0e30db0146100f4575b600080fd5b34801561006857600080fd5b5061008760048036038101908080359060200190929190505050610112565b6040518082815260200191505060405180910390f35b3480156100a957600080fd5b506100b26101d0565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100fc6101f6565b6040518082815260200191505060405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614151561017057600080fd5b8160008082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc839081150290604051600060405180830381858888f193505050501580156101c5573d6000803e3d6000fd5b506000549050919050565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60003460008082825401925050819055506000549050905600a165627a7a72305820f237db3ec816a52589d82512117bc85bc08d3537683ffeff9059108caf3e5d400029c001a029f00dda09306f0f09895e80db110b9348eeb57d3e0b386409bfb674041ba45aa049f74084dd8c517b305a2e60b39ae9002176a5244cb06de8f9ea3757811f5ec6', + s: '0x49f74084dd8c517b305a2e60b39ae9002176a5244cb06de8f9ea3757811f5ec6', + status: 'confirmed', + estimatedBaseFee: 'b', + hash: + '0x4d8543f12afd3795b94d723dcd0e20bfc3740e1af668e5e90a0c5ec49f36ba12', + }, + 1: { + type: TRANSACTION_TYPES.SIMPLE_SEND, + id: 1, + chainId: MAINNET_CHAIN_ID, + txParams: { + nonce: '0x1', + }, + history: [ + { + chainId: '0x4', + dappSuggestedGasFees: { + gas: '0x31413', + }, + id: 1462177651588364, + loadingDefaults: true, + metamaskNetworkId: '4', + origin: 'https://metamask.github.io', + status: 'unapproved', + time: 1631118004776, + txParams: { + data: + '0x608060405234801561001057600080fd5b5033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506000808190555061023b806100686000396000f300608060405260043610610057576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e1a7d4d1461005c5780638da5cb5b1461009d578063d0e30db0146100f4575b600080fd5b34801561006857600080fd5b5061008760048036038101908080359060200190929190505050610112565b6040518082815260200191505060405180910390f35b3480156100a957600080fd5b506100b26101d0565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100fc6101f6565b6040518082815260200191505060405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614151561017057600080fd5b8160008082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc839081150290604051600060405180830381858888f193505050501580156101c5573d6000803e3d6000fd5b506000549050919050565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60003460008082825401925050819055506000549050905600a165627a7a72305820f237db3ec816a52589d82512117bc85bc08d3537683ffeff9059108caf3e5d400029', + from: '0x0f002c95c041f003be01c3e4f52cae1f6ab3ba6e', + gas: '0x31413', + value: '0x0', + }, + type: TRANSACTION_TYPES.SIMPLE_SEND, + }, + [ + { + note: 'Added new unapproved transaction.', + op: 'add', + path: '/txParams/maxFeePerGas', + timestamp: 1631118004862, + value: '0x59682f10', + }, + { + op: 'add', + path: '/txParams/maxPriorityFeePerGas', + value: '0x59682f00', + }, + { + op: 'replace', + path: '/loadingDefaults', + value: false, + }, + { + op: 'add', + path: '/userFeeLevel', + value: 'medium', + }, + ], + ], + }, + 3274396743040791: { + baseFeePerGas: '0xb', + chainId: '0x4', + dappSuggestedGasFees: { + gas: '0xa9fe', + }, + estimatedBaseFee: 'b', + hash: + '0x19ffab8a9467df9afbef82d8907f9e39f0696c7a774ed5473ecf7337adcc674b', + origin: 'https://metamask.github.io', + r: '0xc2b2901f3593536d21e9b136c469b9b8f91a944f18a29a3cdf3a2eaadf660e71', + rawTx: + '0x02f87604288459682f008459682f1082a9fe949ef57335bc7d5b6cbc06dca6064a604b75e09ace883782dace9d90000084d0e30db0c001a0c2b2901f3593536d21e9b136c469b9b8f91a944f18a29a3cdf3a2eaadf660e71a057876a0292d548dd67c6faed8e835b94252b55a043ce01a1206361ccab417ad4', + s: '0x57876a0292d548dd67c6faed8e835b94252b55a043ce01a1206361ccab417ad4', + status: 'confirmed', + submittedTime: 1631118228493, + time: 1631118217596, + txParams: { + data: '0xd0e30db0', + from: '0x0f002c95c041f003be01c3e4f52cae1f6ab3ba6e', + gas: '0xa9fe', + maxFeePerGas: '0x59682f10', + maxPriorityFeePerGas: '0x59682f00', + nonce: '0x28', + to: '0x9ef57335bc7d5b6cbc06dca6064a604b75e09ace', + value: '0x3782dace9d900000', + }, + txReceipt: { + blockHash: + '0xafa4e1fd95e429d9c6e6c7c1d282b2bd0bbeb50d0a68743e9392b9c95a06e2eb', + blockNumber: { + length: 1, + negative: 0, + red: null, + words: [9257603, null], + }, + contractAddress: null, + cumulativeGasUsed: { + length: 1, + negative: 0, + red: null, + words: [4954851, null], + }, + effectiveGasPrice: '0x59682f0b', + from: '0x0f002c95c041f003be01c3e4f52cae1f6ab3ba6e', + gasUsed: 'a9fe', + logs: [], + logsBloom: + '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + status: '0x1', + to: '0x9ef57335bc7d5b6cbc06dca6064a604b75e09ace', + transactionHash: + '0x19ffab8a9467df9afbef82d8907f9e39f0696c7a774ed5473ecf7337adcc674b', + transactionIndex: { + length: 1, + negative: 0, + red: null, + words: [9, null], + }, + type: '0x2', + }, + type: TRANSACTION_TYPES.SIMPLE_SEND, + history: [ + { + chainId: '0x4', + dappSuggestedGasFees: { + gas: '0xa9fe', + }, + id: 3274396743040791, + loadingDefaults: true, + metamaskNetworkId: '4', + origin: 'https://metamask.github.io', + status: 'unapproved', + time: 1631118217596, + txParams: { + data: '0xd0e30db0', + from: '0x0f002c95c041f003be01c3e4f52cae1f6ab3ba6e', + gas: '0xa9fe', + to: '0x9ef57335bc7d5b6cbc06dca6064a604b75e09ace', + value: '0x3782dace9d900000', + }, + }, + [ + { + note: 'Added new unapproved transaction.', + op: 'add', + path: '/txParams/maxFeePerGas', + timestamp: 1631118217762, + value: '0x59682f10', + }, + { + op: 'add', + path: '/txParams/maxPriorityFeePerGas', + value: '0x59682f00', + }, + { + op: 'replace', + path: '/loadingDefaults', + value: false, + }, + { + op: 'add', + path: '/userFeeLevel', + value: 'medium', + }, + ], + ], + }, + }; + const oldStorage = { + meta: {}, + data: { + TransactionController: { + transactions: OLD_TRANSACTION_STATE, + }, + }, + }; + + const expectedStorage = { + meta: {}, + data: { + TransactionController: { + transactions: EXPECTED_TRANSACTION_STATE, + }, + }, + }; + + const newStorage = await migration64.migrate(oldStorage); + + assert.deepEqual(expectedStorage.data, newStorage.data); + }); +}); diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 693537f80..8798a0529 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -66,6 +66,8 @@ import m059 from './059'; import m060 from './060'; import m061 from './061'; import m062 from './062'; +import m063 from './063'; +import m064 from './064'; const migrations = [ m002, @@ -129,6 +131,8 @@ const migrations = [ m060, m061, m062, + m063, + m064, ]; export default migrations; diff --git a/app/scripts/phishing-detect.js b/app/scripts/phishing-detect.js index f036461c3..bb6bf5b2e 100644 --- a/app/scripts/phishing-detect.js +++ b/app/scripts/phishing-detect.js @@ -12,7 +12,12 @@ function start() { const hash = window.location.hash.substring(1); const suspect = querystring.parse(hash); - document.getElementById('csdbLink').href = `https://cryptoscamdb.org/search`; + const newIssueLink = document.getElementById('new-issue-link'); + const newIssueUrl = `https://github.com/MetaMask/eth-phishing-detect/issues/new`; + const newIssueParams = `?title=[Legitimate%20Site%20Blocked]%20${encodeURIComponent( + suspect.hostname, + )}&body=${encodeURIComponent(suspect.href)}`; + newIssueLink.href = `${newIssueUrl}${newIssueParams}`; global.platform = new ExtensionPlatform(); diff --git a/app/scripts/ui.js b/app/scripts/ui.js index ee4539370..362567aa8 100644 --- a/app/scripts/ui.js +++ b/app/scripts/ui.js @@ -1,5 +1,4 @@ // polyfills -import 'abortcontroller-polyfill/dist/polyfill-patch-fetch'; import '@formatjs/intl-relativetimeformat/polyfill'; // dev only, "react-devtools" import is skipped in prod builds diff --git a/babel.config.js b/babel.config.js index 6c98e6df4..efd474696 100644 --- a/babel.config.js +++ b/babel.config.js @@ -6,7 +6,7 @@ module.exports = function (api) { '@babel/preset-env', { targets: { - browsers: ['chrome >= 63', 'firefox >= 68'], + browsers: ['chrome >= 66', 'firefox >= 68'], }, }, ], diff --git a/development/README.md b/development/README.md index 1e18d4f16..a8e4eb6fc 100644 --- a/development/README.md +++ b/development/README.md @@ -1,5 +1,5 @@ # Development -Several files which are needed for developing on(!) MetaMask. +Several files which are needed for developing on MetaMask. -Usually each files contains information about its scope / usage. \ No newline at end of file +Usually each file or directory contains information about its scope / usage. diff --git a/development/build/README.md b/development/build/README.md new file mode 100644 index 000000000..6c671be27 --- /dev/null +++ b/development/build/README.md @@ -0,0 +1,60 @@ +# The MetaMask Build System + +> _tl;dr_ `yarn dist` for prod, `yarn start` for local development + +This directory contains the MetaMask build system, which is used to build the MetaMask Extension such that it can be used in a supported browser. +From the repository root, the build system entry file is located at [`./development/build/index.js`](https://github.com/MetaMask/metamask-extension/blob/develop/development/build/index.js). + +Several package scripts invoke the build system. +For example, `yarn start` creates a watched development build, and `yarn dist` creates a production build. +Some of these scripts applies `lavamoat` to the build system, and some do not. +For local development, building without `lavamoat` is faster and therefore preferable. + +The build system is not a full-featured CLI, but rather a script that expects some command line arguments and environment variables. +For instructions regarding environment variables, see [the main repository readme](../../README.md#building-locally). + +Generally speaking, the build system consists of [`gulp`](https://npmjs.com/package/gulp) tasks that either manipulate static assets or bundle source files using [Browserify](https://browserify.org/). +Production-ready zip files are written to the `./builds` directory, while "unpacked" extension builds +are written to the `./dist` directory. + +Our JavaScript source files are transformed using [Babel](https://babeljs.io/), specifically using +the [`babelify`](https://npmjs.com/package/babelify) Browserify transform. +Source file bundling tasks are implemented in the [`./development/build/scripts.js`](https://github.com/MetaMask/metamask-extension/blob/develop/development/build/scripts.js). + +> Locally implemented Browserify transforms, _some of which affect how we write JavaScript_, are listed and documented [here](./transforms/README.md). + +## Usage + +```text +Usage: yarn build [options] + +Commands: + yarn build prod Create an optimized build for production environments. + + yarn build dev Create an unoptimized, live-reloaded build for local + development. + + yarn build test Create an optimized build for running e2e tests. + + yarn build testDev Create an unoptimized, live-reloaded build for running + e2e tests. + +Options: + --beta-version If the build type is "beta", the beta version number. + [number] [default: 0] + --build-type The "type" of build to create. One of: "beta", "main" + [string] [default: "main"] + --lint-fence-files Whether files with code fences should be linted after + fences have been removed by the code fencing transform. + The build will fail if linting fails. + Defaults to `false` if the entry task is `dev` or + `testDev`, and `true` otherwise. + [boolean] [default: ] + --omit-lockdown Whether to omit SES lockdown files from the extension + bundle. Useful when linking dependencies that are + incompatible with lockdown. + [boolean] [default: false] + --skip-stats Whether to refrain from logging build progress. Mostly + used internally. + [boolean] [default: false] +``` diff --git a/development/build/display.js b/development/build/display.js index 6d16cc7e7..c3da7c7f5 100644 --- a/development/build/display.js +++ b/development/build/display.js @@ -44,7 +44,7 @@ function displayChart(data) { const colors = randomColor({ count: data.length }); // some heading before the bars - console.log(`\nbuild completed. task timeline:`); + console.log(`\nBuild completed. Task timeline:`); // build bars for bounds data.forEach((entry, index) => { diff --git a/development/build/etc.js b/development/build/etc.js index 754a64f78..c30bbe9b8 100644 --- a/development/build/etc.js +++ b/development/build/etc.js @@ -5,11 +5,17 @@ const del = require('del'); const pify = require('pify'); const pump = pify(require('pump')); const { version } = require('../../package.json'); + const { createTask, composeParallel } = require('./task'); module.exports = createEtcTasks; -function createEtcTasks({ browserPlatforms, livereload }) { +function createEtcTasks({ + betaVersionsMap, + browserPlatforms, + isBeta, + livereload, +}) { const clean = createTask('clean', async function clean() { await del(['./dist/*']); await Promise.all( @@ -27,18 +33,23 @@ function createEtcTasks({ browserPlatforms, livereload }) { const zip = createTask( 'zip', composeParallel( - ...browserPlatforms.map((platform) => createZipTask(platform)), + ...browserPlatforms.map((platform) => + createZipTask(platform, isBeta ? betaVersionsMap[platform] : undefined), + ), ), ); return { clean, reload, zip }; } -function createZipTask(target) { +function createZipTask(platform, betaVersion) { return async () => { + const path = betaVersion + ? `metamask-BETA-${platform}-${betaVersion}` + : `metamask-${platform}-${version}`; await pump( - gulp.src(`dist/${target}/**`), - gulpZip(`metamask-${target}-${version}.zip`), + gulp.src(`dist/${platform}/**`), + gulpZip(`${path}.zip`), gulp.dest('builds'), ); }; diff --git a/development/build/index.js b/development/build/index.js index 9a60ead28..01c9b50da 100755 --- a/development/build/index.js +++ b/development/build/index.js @@ -4,17 +4,20 @@ // run any task with "yarn build ${taskName}" // const livereload = require('gulp-livereload'); +const minimist = require('minimist'); +const { version } = require('../../package.json'); const { createTask, composeSeries, composeParallel, - detectAndRunEntryTask, + runTask, } = require('./task'); const createManifestTasks = require('./manifest'); const createScriptTasks = require('./scripts'); const createStyleTasks = require('./styles'); const createStaticAssetTasks = require('./static'); const createEtcTasks = require('./etc'); +const { BuildTypes, getNextBetaVersionMap } = require('./utils'); // packages required dynamically via browserify configuration in dependencies require('loose-envify'); @@ -27,19 +30,59 @@ require('@babel/preset-env'); require('@babel/preset-react'); require('@babel/core'); -const browserPlatforms = ['firefox', 'chrome', 'brave', 'opera']; +defineAndRunBuildTasks(); -defineAllTasks(); -detectAndRunEntryTask(); +function defineAndRunBuildTasks() { + const { + betaVersion, + buildType, + entryTask, + isBeta, + isLavaMoat, + shouldIncludeLockdown, + shouldLintFenceFiles, + skipStats, + } = parseArgv(); + + const browserPlatforms = ['firefox', 'chrome', 'brave', 'opera']; + + let betaVersionsMap; + if (isBeta) { + betaVersionsMap = getNextBetaVersionMap( + version, + betaVersion, + browserPlatforms, + ); + } + + const staticTasks = createStaticAssetTasks({ + livereload, + browserPlatforms, + shouldIncludeLockdown, + isBeta, + }); + + const manifestTasks = createManifestTasks({ + browserPlatforms, + betaVersionsMap, + isBeta, + }); -function defineAllTasks() { - const staticTasks = createStaticAssetTasks({ livereload, browserPlatforms }); - const manifestTasks = createManifestTasks({ browserPlatforms }); const styleTasks = createStyleTasks({ livereload }); - const scriptTasks = createScriptTasks({ livereload, browserPlatforms }); + + const scriptTasks = createScriptTasks({ + browserPlatforms, + buildType, + isLavaMoat, + livereload, + shouldLintFenceFiles, + }); + const { clean, reload, zip } = createEtcTasks({ livereload, browserPlatforms, + betaVersionsMap, + isBeta, }); // build for development (livereload) @@ -96,4 +139,72 @@ function defineAllTasks() { // special build for minimal CI testing createTask('styles', styleTasks.prod); + + // Finally, start the build process by running the entry task. + runTask(entryTask, { skipStats }); +} + +function parseArgv() { + const NamedArgs = { + BetaVersion: 'beta-version', + BuildType: 'build-type', + LintFenceFiles: 'lint-fence-files', + OmitLockdown: 'omit-lockdown', + SkipStats: 'skip-stats', + }; + + const argv = minimist(process.argv.slice(2), { + boolean: [ + NamedArgs.LintFenceFiles, + NamedArgs.OmitLockdown, + NamedArgs.SkipStats, + ], + string: [NamedArgs.BuildType], + default: { + [NamedArgs.BetaVersion]: 0, + [NamedArgs.BuildType]: BuildTypes.main, + [NamedArgs.LintFenceFiles]: true, + [NamedArgs.OmitLockdown]: false, + [NamedArgs.SkipStats]: false, + }, + }); + + if (argv._.length !== 1) { + throw new Error( + `Metamask build: Expected a single positional argument, but received "${argv._.length}" arguments.`, + ); + } + + const entryTask = argv._[0]; + if (!entryTask) { + throw new Error('MetaMask build: No entry task specified.'); + } + + const betaVersion = argv[NamedArgs.BetaVersion]; + if (!Number.isInteger(betaVersion) || betaVersion < 0) { + throw new Error(`MetaMask build: Invalid beta version: "${betaVersion}"`); + } + + const buildType = argv[NamedArgs.BuildType]; + if (!(buildType in BuildTypes)) { + throw new Error(`MetaMask build: Invalid build type: "${buildType}"`); + } + + // Manually default this to `false` for dev builds only. + const shouldLintFenceFiles = process.argv.includes( + `--${NamedArgs.LintFenceFiles}`, + ) + ? argv[NamedArgs.LintFenceFiles] + : !/dev/iu.test(entryTask); + + return { + betaVersion: String(betaVersion), + buildType, + entryTask, + isBeta: argv[NamedArgs.BuildType] === BuildTypes.beta, + isLavaMoat: process.argv[0].includes('lavamoat'), + shouldIncludeLockdown: argv[NamedArgs.OmitLockdown], + shouldLintFenceFiles, + skipStats: argv[NamedArgs.SkipStats], + }; } diff --git a/development/build/manifest.js b/development/build/manifest.js index 09df35cbd..3e80ea5b5 100644 --- a/development/build/manifest.js +++ b/development/build/manifest.js @@ -4,12 +4,13 @@ const { merge, cloneDeep } = require('lodash'); const baseManifest = require('../../app/manifest/_base.json'); const { version } = require('../../package.json'); +const betaManifestModifications = require('../../app/manifest/_beta_modifications.json'); const { createTask, composeSeries } = require('./task'); module.exports = createManifestTasks; -function createManifestTasks({ browserPlatforms }) { +function createManifestTasks({ betaVersionsMap, browserPlatforms, isBeta }) { // merge base manifest with per-platform manifests const prepPlatforms = async () => { return Promise.all( @@ -26,8 +27,10 @@ function createManifestTasks({ browserPlatforms }) { ); const result = merge( cloneDeep(baseManifest), - { version }, platformModifications, + isBeta + ? getBetaModifications(platform, betaVersionsMap) + : { version }, ); const dir = path.join('.', 'dist', platform); await fs.mkdir(dir, { recursive: true }); @@ -105,3 +108,17 @@ async function readJson(file) { async function writeJson(obj, file) { return fs.writeFile(file, JSON.stringify(obj, null, 2)); } + +function getBetaModifications(platform, betaVersionsMap) { + if (!betaVersionsMap || typeof betaVersionsMap !== 'object') { + throw new Error('MetaMask build: Expected object beta versions map.'); + } + + const betaVersion = betaVersionsMap[platform]; + + return { + ...betaManifestModifications, + version: betaVersion, + ...(platform === 'firefox' ? {} : { version_name: 'beta' }), + }; +} diff --git a/development/build/scripts.js b/development/build/scripts.js index 130e1f0f1..da7e73e26 100644 --- a/development/build/scripts.js +++ b/development/build/scripts.js @@ -37,16 +37,26 @@ const metamaskrc = require('rc')('metamask', { const { streamFlatMap } = require('../stream-flat-map.js'); const { version } = require('../../package.json'); + const { createTask, composeParallel, composeSeries, runInChildProcess, } = require('./task'); +const { + createRemoveFencedCodeTransform, +} = require('./transforms/remove-fenced-code'); module.exports = createScriptTasks; -function createScriptTasks({ browserPlatforms, livereload }) { +function createScriptTasks({ + browserPlatforms, + buildType, + isLavaMoat, + livereload, + shouldLintFenceFiles, +}) { // internal tasks const core = { // dev tasks (live reload) @@ -78,15 +88,17 @@ function createScriptTasks({ browserPlatforms, livereload }) { const standardSubtask = createTask( `${taskPrefix}:standardEntryPoints`, createFactoredBuild({ + browserPlatforms, + buildType, + devMode, entryFiles: standardEntryPoints.map((label) => { if (label === 'content-script') { return './app/vendor/trezor/content-script.js'; } return `./app/scripts/${label}.js`; }), - devMode, testing, - browserPlatforms, + shouldLintFenceFiles, }), ); @@ -137,7 +149,13 @@ function createScriptTasks({ browserPlatforms, livereload }) { disableConsoleSubtask, installSentrySubtask, phishingDetectSubtask, - ].map((subtask) => runInChildProcess(subtask)); + ].map((subtask) => + runInChildProcess(subtask, { + buildType, + isLavaMoat, + shouldLintFenceFiles, + }), + ); // make a parent task that runs each task in a child thread return composeParallel(initiateLiveReload, ...allSubtasks); } @@ -145,33 +163,39 @@ function createScriptTasks({ browserPlatforms, livereload }) { function createTaskForBundleDisableConsole({ devMode }) { const label = 'disable-console'; return createNormalBundle({ - label, - entryFilepath: `./app/scripts/${label}.js`, + browserPlatforms, + buildType, destFilepath: `${label}.js`, devMode, - browserPlatforms, + entryFilepath: `./app/scripts/${label}.js`, + label, + shouldLintFenceFiles, }); } function createTaskForBundleSentry({ devMode }) { const label = 'sentry-install'; return createNormalBundle({ - label, - entryFilepath: `./app/scripts/${label}.js`, + browserPlatforms, + buildType, destFilepath: `${label}.js`, devMode, - browserPlatforms, + entryFilepath: `./app/scripts/${label}.js`, + label, + shouldLintFenceFiles, }); } function createTaskForBundlePhishingDetect({ devMode }) { const label = 'phishing-detect'; return createNormalBundle({ - label, - entryFilepath: `./app/scripts/${label}.js`, + buildType, + browserPlatforms, destFilepath: `${label}.js`, devMode, - browserPlatforms, + entryFilepath: `./app/scripts/${label}.js`, + label, + shouldLintFenceFiles, }); } @@ -181,30 +205,36 @@ function createScriptTasks({ browserPlatforms, livereload }) { const contentscript = 'contentscript'; return composeSeries( createNormalBundle({ - label: inpage, - entryFilepath: `./app/scripts/${inpage}.js`, + buildType, + browserPlatforms, destFilepath: `${inpage}.js`, devMode, + entryFilepath: `./app/scripts/${inpage}.js`, + label: inpage, testing, - browserPlatforms, + shouldLintFenceFiles, }), createNormalBundle({ - label: contentscript, - entryFilepath: `./app/scripts/${contentscript}.js`, + buildType, + browserPlatforms, destFilepath: `${contentscript}.js`, devMode, + entryFilepath: `./app/scripts/${contentscript}.js`, + label: contentscript, testing, - browserPlatforms, + shouldLintFenceFiles, }), ); } } function createFactoredBuild({ - entryFiles, - devMode, - testing, browserPlatforms, + buildType, + devMode, + entryFiles, + testing, + shouldLintFenceFiles, }) { return async function () { // create bundler setup and apply defaults @@ -216,12 +246,14 @@ function createFactoredBuild({ const reloadOnChange = Boolean(devMode); const minify = Boolean(devMode) === false; - const envVars = getEnvironmentVariables({ devMode, testing }); + const envVars = getEnvironmentVariables({ buildType, devMode, testing }); setupBundlerDefaults(buildConfiguration, { + buildType, devMode, envVars, - reloadOnChange, minify, + reloadOnChange, + shouldLintFenceFiles, }); // set bundle entries @@ -314,14 +346,16 @@ function createFactoredBuild({ } function createNormalBundle({ - label, + browserPlatforms, + buildType, destFilepath, + devMode, entryFilepath, extraEntries = [], + label, modulesToExpose, - devMode, + shouldLintFenceFiles, testing, - browserPlatforms, }) { return async function () { // create bundler setup and apply defaults @@ -333,12 +367,14 @@ function createNormalBundle({ const reloadOnChange = Boolean(devMode); const minify = Boolean(devMode) === false; - const envVars = getEnvironmentVariables({ devMode, testing }); + const envVars = getEnvironmentVariables({ buildType, devMode, testing }); setupBundlerDefaults(buildConfiguration, { + buildType, devMode, envVars, - reloadOnChange, minify, + reloadOnChange, + shouldLintFenceFiles, }); // set bundle entries @@ -385,35 +421,37 @@ function createBuildConfiguration() { function setupBundlerDefaults( buildConfiguration, - { devMode, envVars, reloadOnChange, minify }, + { buildType, devMode, envVars, minify, reloadOnChange, shouldLintFenceFiles }, ) { const { bundlerOpts } = buildConfiguration; Object.assign(bundlerOpts, { - // source transforms + // Source transforms transform: [ - // transpile top-level code + // Remove code that should be excluded from builds of the current type + createRemoveFencedCodeTransform(buildType, shouldLintFenceFiles), + // Transpile top-level code babelify, - // inline `fs.readFileSync` files + // Inline `fs.readFileSync` files brfs, ], - // use entryFilepath for moduleIds, easier to determine origin file + // Use entryFilepath for moduleIds, easier to determine origin file fullPaths: devMode, - // for sourcemaps + // For sourcemaps debug: true, }); - // ensure react-devtools are not included in non-dev builds + // Ensure react-devtools are not included in non-dev builds if (!devMode) { bundlerOpts.manualIgnore.push('react-devtools'); } - // inject environment variables via node-style `process.env` + // Inject environment variables via node-style `process.env` if (envVars) { bundlerOpts.transform.push([envify(envVars), { global: true }]); } - // setup reload on change + // Setup reload on change if (reloadOnChange) { setupReloadOnChange(buildConfiguration); } @@ -422,21 +460,21 @@ function setupBundlerDefaults( setupMinification(buildConfiguration); } - // setup source maps + // Setup source maps setupSourcemaps(buildConfiguration, { devMode }); } function setupReloadOnChange({ bundlerOpts, events }) { - // add plugin to options + // Add plugin to options Object.assign(bundlerOpts, { plugin: [...bundlerOpts.plugin, watchify], - // required by watchify + // Required by watchify cache: {}, packageCache: {}, }); - // instrument pipeline + // Instrument pipeline events.on('configurePipeline', ({ bundleStream }) => { - // handle build error to avoid breaking build process + // Handle build error to avoid breaking build process // (eg on syntax error) bundleStream.on('error', (err) => { gracefulError(err); @@ -503,9 +541,9 @@ async function bundleIt(buildConfiguration) { // forward update event (used by watchify) bundler.on('update', () => performBundle()); - console.log(`bundle start: "${label}"`); + console.log(`Bundle start: "${label}"`); await performBundle(); - console.log(`bundle end: "${label}"`); + console.log(`Bundle end: "${label}"`); async function performBundle() { // this pipeline is created for every bundle @@ -539,7 +577,7 @@ async function bundleIt(buildConfiguration) { } } -function getEnvironmentVariables({ devMode, testing }) { +function getEnvironmentVariables({ buildType, devMode, testing }) { const environment = getEnvironment({ devMode, testing }); if (environment === 'production' && !process.env.SENTRY_DSN) { throw new Error('Missing SENTRY_DSN environment variable'); @@ -548,6 +586,7 @@ function getEnvironmentVariables({ devMode, testing }) { METAMASK_DEBUG: devMode, METAMASK_ENVIRONMENT: environment, METAMASK_VERSION: version, + METAMASK_BUILD_TYPE: buildType, NODE_ENV: devMode ? 'development' : 'production', IN_TEST: testing ? 'true' : false, PUBNUB_SUB_KEY: process.env.PUBNUB_SUB_KEY || '', @@ -572,6 +611,7 @@ function getEnvironmentVariables({ devMode, testing }) { environment === 'production' ? process.env.SEGMENT_PROD_LEGACY_WRITE_KEY : metamaskrc.SEGMENT_LEGACY_WRITE_KEY, + SWAPS_USE_DEV_APIS: process.env.SWAPS_USE_DEV_APIS === '1', }; } diff --git a/development/build/static.js b/development/build/static.js index 9d6ba3060..341b7d56b 100644 --- a/development/build/static.js +++ b/development/build/static.js @@ -7,111 +7,32 @@ const locales = require('../../app/_locales/index.json'); const { createTask, composeSeries } = require('./task'); -module.exports = createStaticAssetTasks; +const EMPTY_JS_FILE = './development/empty.js'; -const copyTargets = [ - { - src: `./app/_locales/`, - dest: `_locales`, - }, - { - src: `./app/images/`, - dest: `images`, - }, - { - src: `./node_modules/@metamask/contract-metadata/images/`, - dest: `images/contract`, - }, - { - src: `./app/fonts/`, - dest: `fonts`, - }, - { - src: `./app/vendor/`, - dest: `vendor`, - }, - { - src: `./node_modules/@fortawesome/fontawesome-free/webfonts/`, - dest: `fonts/fontawesome`, - }, - { - src: `./ui/css/output/`, - pattern: `*.css`, - dest: ``, - }, - { - src: `./app/loading.html`, - dest: `loading.html`, - }, - { - src: `./node_modules/globalthis/dist/browser.js`, - dest: `globalthis.js`, - }, - { - src: `./node_modules/ses/dist/lockdown.cjs`, - dest: `lockdown-install.js`, - }, - { - src: `./app/scripts/lockdown-run.js`, - dest: `lockdown-run.js`, - }, - { - // eslint-disable-next-line node/no-extraneous-require - src: require.resolve('@lavamoat/lavapack/src/runtime-cjs.js'), - dest: `runtime-cjs.js`, - }, - { - src: `./app/phishing.html`, - dest: `phishing.html`, - }, -]; +module.exports = function createStaticAssetTasks({ + livereload, + browserPlatforms, + shouldIncludeLockdown = true, + isBeta, +}) { + const [copyTargetsProd, copyTargetsDev] = getCopyTargets( + shouldIncludeLockdown, + ); -const languageTags = new Set(); -for (const locale of locales) { - const { code } = locale; - const tag = code.split('_')[0]; - languageTags.add(tag); -} + const copyTargetsBeta = [ + ...copyTargetsProd, + { + src: './app/build-types/beta/', + dest: `images`, + }, + ]; -for (const tag of languageTags) { - copyTargets.push({ - src: `./node_modules/@formatjs/intl-relativetimeformat/dist/locale-data/${tag}.json`, - dest: `intl/${tag}/relative-time-format-data.json`, - }); -} + const targets = isBeta ? copyTargetsBeta : copyTargetsProd; -const copyTargetsDev = [ - ...copyTargets, - { - src: './development', - pattern: '/chromereload.js', - dest: ``, - }, - // empty files to suppress missing file errors - { - src: './development/empty.js', - dest: `bg-libs.js`, - }, - { - src: './development/empty.js', - dest: `ui-libs.js`, - }, -]; - -const copyTargetsProd = [ - ...copyTargets, - // empty files to suppress missing file errors - { - src: './development/empty.js', - dest: `chromereload.js`, - }, -]; - -function createStaticAssetTasks({ livereload, browserPlatforms }) { const prod = createTask( 'static:prod', composeSeries( - ...copyTargetsProd.map((target) => { + ...targets.map((target) => { return async function copyStaticAssets() { await performCopy(target); }; @@ -169,4 +90,110 @@ function createStaticAssetTasks({ livereload, browserPlatforms }) { }), ); } +}; + +function getCopyTargets(shouldIncludeLockdown) { + const allCopyTargets = [ + { + src: `./app/_locales/`, + dest: `_locales`, + }, + { + src: `./app/images/`, + dest: `images`, + }, + { + src: `./node_modules/@metamask/contract-metadata/images/`, + dest: `images/contract`, + }, + { + src: `./app/fonts/`, + dest: `fonts`, + }, + { + src: `./app/vendor/`, + dest: `vendor`, + }, + { + src: `./node_modules/@fortawesome/fontawesome-free/webfonts/`, + dest: `fonts/fontawesome`, + }, + { + src: `./ui/css/output/`, + pattern: `*.css`, + dest: ``, + }, + { + src: `./app/loading.html`, + dest: `loading.html`, + }, + { + src: `./node_modules/globalthis/dist/browser.js`, + dest: `globalthis.js`, + }, + { + src: shouldIncludeLockdown + ? `./node_modules/ses/dist/lockdown.umd.min.js` + : EMPTY_JS_FILE, + dest: `lockdown-install.js`, + }, + { + src: shouldIncludeLockdown + ? `./app/scripts/lockdown-run.js` + : EMPTY_JS_FILE, + dest: `lockdown-run.js`, + }, + { + // eslint-disable-next-line node/no-extraneous-require + src: require.resolve('@lavamoat/lavapack/src/runtime-cjs.js'), + dest: `runtime-cjs.js`, + }, + { + src: `./app/phishing.html`, + dest: `phishing.html`, + }, + ]; + + const languageTags = new Set(); + for (const locale of locales) { + const { code } = locale; + const tag = code.split('_')[0]; + languageTags.add(tag); + } + + for (const tag of languageTags) { + allCopyTargets.push({ + src: `./node_modules/@formatjs/intl-relativetimeformat/dist/locale-data/${tag}.json`, + dest: `intl/${tag}/relative-time-format-data.json`, + }); + } + + const copyTargetsDev = [ + ...allCopyTargets, + { + src: './development', + pattern: '/chromereload.js', + dest: ``, + }, + // empty files to suppress missing file errors + { + src: EMPTY_JS_FILE, + dest: `bg-libs.js`, + }, + { + src: EMPTY_JS_FILE, + dest: `ui-libs.js`, + }, + ]; + + const copyTargetsProd = [ + ...allCopyTargets, + // empty files to suppress missing file errors + { + src: EMPTY_JS_FILE, + dest: `chromereload.js`, + }, + ]; + + return [copyTargetsProd, copyTargetsDev]; } diff --git a/development/build/task.js b/development/build/task.js index 22c70ad4a..07cd6d0c3 100644 --- a/development/build/task.js +++ b/development/build/task.js @@ -5,7 +5,6 @@ const tasks = {}; const taskEvents = new EventEmitter(); module.exports = { - detectAndRunEntryTask, tasks, taskEvents, createTask, @@ -17,24 +16,13 @@ module.exports = { const { setupTaskDisplay } = require('./display'); -function detectAndRunEntryTask() { - // get requested task name and execute - const taskName = process.argv[2]; - if (!taskName) { - throw new Error(`MetaMask build: No task name specified`); - } - const skipStats = process.argv.includes('--skip-stats'); - - runTask(taskName, { skipStats }); -} - async function runTask(taskName, { skipStats } = {}) { if (!(taskName in tasks)) { throw new Error(`MetaMask build: Unrecognized task name "${taskName}"`); } if (!skipStats) { setupTaskDisplay(taskEvents); - console.log(`running task "${taskName}"...`); + console.log(`Running task "${taskName}"...`); } try { await tasks[taskName](); @@ -60,29 +48,55 @@ function createTask(taskName, taskFn) { return task; } -function runInChildProcess(task) { +function runInChildProcess( + task, + { buildType, isLavaMoat, shouldLintFenceFiles }, +) { const taskName = typeof task === 'string' ? task : task.taskName; if (!taskName) { throw new Error( `MetaMask build: runInChildProcess unable to identify task name`, ); } + return instrumentForTaskStats(taskName, async () => { let childProcess; - // don't run subprocesses in lavamoat for dev mode if main process not run in lavamoat - if ( - process.env.npm_lifecycle_event === 'build:dev' || - (taskName.includes('scripts:core:dev') && - !process.argv[0].includes('lavamoat')) - ) { - childProcess = spawn('yarn', ['build:dev', taskName, '--skip-stats'], { - env: process.env, - }); + // Use the same build type for subprocesses, and only run them in LavaMoat + // if the parent process also ran in LavaMoat. + if (isLavaMoat) { + childProcess = spawn( + 'yarn', + [ + 'build', + taskName, + '--build-type', + buildType, + '--lint-fence-files', + shouldLintFenceFiles, + '--skip-stats', + ], + { + env: process.env, + }, + ); } else { - childProcess = spawn('yarn', ['build', taskName, '--skip-stats'], { - env: process.env, - }); + childProcess = spawn( + 'yarn', + [ + 'build:dev', + taskName, + '--build-type', + buildType, + '--lint-fence-files', + shouldLintFenceFiles, + '--skip-stats', + ], + { + env: process.env, + }, + ); } + // forward logs to main process // skip the first stdout event (announcing the process command) childProcess.stdout.once('data', () => { @@ -90,16 +104,18 @@ function runInChildProcess(task) { process.stdout.write(`${taskName}: ${data}`), ); }); + childProcess.stderr.on('data', (data) => process.stderr.write(`${taskName}: ${data}`), ); + // await end of process await new Promise((resolve, reject) => { childProcess.once('exit', (errCode) => { if (errCode !== 0) { reject( new Error( - `MetaMask build: runInChildProcess for task "${taskName}" encountered an error ${errCode}`, + `MetaMask build: runInChildProcess for task "${taskName}" encountered an error "${errCode}".`, ), ); return; diff --git a/development/build/transforms/README.md b/development/build/transforms/README.md new file mode 100644 index 000000000..4cbba5e5b --- /dev/null +++ b/development/build/transforms/README.md @@ -0,0 +1,130 @@ +# Local Browserify Transforms + +This directory contains home-grown Browserify transforms. +Each file listed here exports a transform function factory. + +## Removing Fenced Code + +> `./remove-fenced-code.js` + +When creating builds that support different features, it is desirable to exclude +unsupported features, files, and dependencies at build time. Undesired files and +dependencies can be excluded wholesale, but the _use_ of undesired modules in +files that should otherwise be included – i.e. import statements and references +to those imports – cannot. + +To support the exclusion of the use of undesired modules at build time, we +introduce the concept of code fencing to our build system. Our code fencing +syntax amounts to a tiny DSL, which is specified below. + +The transform concatenates each file into a single string, and a string parser +identifies any fences in the file. If any fences that should not be included in +the current build are found, the fences and the lines that they wrap are +deleted. The transform errors if a malformed fence line is identified. + +For example, the following fenced code: + +```javascript +this.store.updateStructure({ + ..., + GasFeeController: this.gasFeeController, + TokenListController: this.tokenListController, + ///: BEGIN:ONLY_INCLUDE_IN(beta) + PluginController: this.pluginController, + ///: END:ONLY_INCLUDE_IN +}); +``` + +Is transformed to the following if the build type is not `beta`: + +```javascript +this.store.updateStructure({ + ..., + GasFeeController: this.gasFeeController, + TokenListController: this.tokenListController, +}); +``` + +Note that multiple build types can be specified by separating them with +commands inside the parameter parentheses: + +```javascript +///: BEGIN:ONLY_INCLUDE_IN(beta,flask) +``` + +### Gotchas + +By default, the transform will invoke ESLint on files that are modified by the transform. +This is our first line of defense against creating unsyntactic code using code fences, and the transform will error if linting fails. +(Live reloading will continue to work if enabled.) +To toggle this behavior via build system arguments, see [the build system readme](../README.md). + +### Code Fencing Syntax + +> In the specification, angle brackets, `< >`, indicate required tokens, while +> straight brackets, `[ ]`, indicate optional tokens. +> +> Alphabetical characters identify the name and purpose of a token. All other +> characters, including parentheses, `( )`, are literals. + +A fence line is a single-line JavaScript comment, optionally surrounded by +whitespace, in the following format: + +```text +///: :[(parameters)] + +|__| |________________________________| + | | + | | +sentinel directive +``` + +The first part of a fence line is the `sentinel`, which is always the string +"`///:`". If the first four non-whitespace characters of a line are not the +`sentinel`, the line will be ignored by the parser. The `sentinel` must be +succeeded by a single space character, or parsing will fail. + +The remainder of the fence line is called the `directive`. +The directive consists of a `terminus`, `command`, and (optionally) `parameters`. + +- The `terminus` is one of the strings `BEGIN` and `END`. It must be followed by + a single colon, `:`. +- The `command` is a string of uppercase alphabetical characters, optionally + including underscores, `_`. The possible commands are listed later in this + specification. +- The `parameters` are a comma-separated list of RegEx `\w` strings. They must + be parenthesized, only specified for `BEGIN` directives, and valid for the + command of the directive. + +A valid code fence consists of two fence lines surrounding one or more lines of +non-fence lines. The first fence line must consist of a `BEGIN` directive, and +the second an `END` directive. The command of both directives must be the same, +and the parameters (if any) must be valid for the command. + +If an invalid fence is detected, parsing will fail, and the transform stream +will end with an error. + +### Commands + +#### `ONLY_INCLUDE_IN` + +This, the only command defined so far, is used to exclude lines of code +depending on the type of the current build. If a particular set of lines should +only be included in a particular build type, say `beta`, they should be wrapped +as follows: + +```javascript +///: BEGIN:ONLY_INCLUDE_IN(beta) +console.log('I am only included in beta builds.'); +///: END:ONLY_INCLUDE_IN +``` + +At build time, the fences and the fenced lines will be removed if the build is +not `beta`. + +Parameters are required for this command, and they must be provided as a +comma-separated list of one or more of: + +- `main` (the build system default build type) +- `beta` +- `flask` diff --git a/development/build/transforms/remove-fenced-code.js b/development/build/transforms/remove-fenced-code.js new file mode 100644 index 000000000..378972381 --- /dev/null +++ b/development/build/transforms/remove-fenced-code.js @@ -0,0 +1,463 @@ +const path = require('path'); +const { PassThrough, Transform } = require('stream'); +const { BuildTypes } = require('../utils'); +const { lintTransformedFile } = require('./utils'); + +const hasOwnProperty = (obj, key) => Reflect.hasOwnProperty.call(obj, key); + +module.exports = { + createRemoveFencedCodeTransform, + removeFencedCode, +}; + +class RemoveFencedCodeTransform extends Transform { + /** + * A transform stream that calls {@link removeFencedCode} on the complete + * string contents of the file read by Browserify. + * + * Optionally lints the file if it was modified. + * + * @param {string} filePath - The path to the file being transformed. + * @param {string} buildType - The type of the current build process. + * @param {boolean} shouldLintTransformedFiles - Whether the file should be + * linted if modified by the transform. + */ + constructor(filePath, buildType, shouldLintTransformedFiles) { + super(); + this.filePath = filePath; + this.buildType = buildType; + this.shouldLintTransformedFiles = shouldLintTransformedFiles; + this._fileBuffers = []; + } + + // This function is called whenever data is written to the stream. + // It concatenates all buffers for the current file into a single buffer. + _transform(buffer, _encoding, next) { + this._fileBuffers.push(buffer); + next(); + } + + // "flush" is called when all data has been written to the + // stream, immediately before the "end" event is emitted. + // It applies the transform to the concatenated file contents. + _flush(end) { + const [fileContent, didModify] = removeFencedCode( + this.filePath, + this.buildType, + Buffer.concat(this._fileBuffers).toString('utf8'), + ); + + const pushAndEnd = () => { + this.push(fileContent); + end(); + }; + + if (this.shouldLintTransformedFiles && didModify) { + lintTransformedFile(fileContent, this.filePath) + .then(pushAndEnd) + .catch((error) => end(error)); + } else { + pushAndEnd(); + } + } +} + +/** + * A factory for a Browserify transform that removes fenced code from all + * JavaScript source files. The transform is applied to files with the following + * extensions: + * - `.js` + * - `.cjs` + * - `.mjs` + * + * For details on how the transform mutates source files, see + * {@link removeFencedCode} and the documentation. + * + * If specified (and by default), the transform will call ESLint on the text + * contents of any file that it modifies. The transform will error if such a + * file is ignored by ESLint, since linting is our first line of defense against + * making un-syntactic modifications to files using code fences. + * + * @param {string} buildType - The type of the current build. + * @param {boolean} shouldLintTransformedFiles - Whether to lint transformed files. + * @returns {(filePath: string) => Transform} The transform function. + */ +function createRemoveFencedCodeTransform( + buildType, + shouldLintTransformedFiles = true, +) { + if (!hasOwnProperty(BuildTypes, buildType)) { + throw new Error( + `Code fencing transform received unrecognized build type "${buildType}".`, + ); + } + + // Browserify transforms are functions that receive a file name and return a + // duplex stream. The stream receives the file contents piecemeal in the form + // of Buffers. + // To apply our code fencing transform, we concatenate all buffers and convert + // them to a single string, then apply the actual transform function on that + // string. + /** + * @returns {Transform} + */ + return function removeFencedCodeTransform(filePath) { + if (!['.js', '.cjs', '.mjs'].includes(path.extname(filePath))) { + return new PassThrough(); + } + + return new RemoveFencedCodeTransform( + filePath, + buildType, + shouldLintTransformedFiles, + ); + }; +} + +const DirectiveTerminuses = { + BEGIN: 'BEGIN', + END: 'END', +}; + +const DirectiveCommands = { + ONLY_INCLUDE_IN: 'ONLY_INCLUDE_IN', +}; + +const CommandValidators = { + [DirectiveCommands.ONLY_INCLUDE_IN]: (params, filePath) => { + if (!params || params.length === 0) { + throw new Error( + getInvalidParamsMessage( + filePath, + DirectiveCommands.ONLY_INCLUDE_IN, + `No params specified.`, + ), + ); + } + + params.forEach((param) => { + if (!hasOwnProperty(BuildTypes, param)) { + throw new Error( + getInvalidParamsMessage( + filePath, + DirectiveCommands.ONLY_INCLUDE_IN, + `"${param}" is not a valid build type.`, + ), + ); + } + }); + }, +}; + +// Matches lines starting with "///:", and any preceding whitespace, except +// newlines. We except newlines to avoid eating blank lines preceding a fenced +// line. +// Double-negative RegEx credit: https://stackoverflow.com/a/3469155 +const linesWithFenceRegex = /^[^\S\r\n]*\/\/\/:.*$/gmu; + +// Matches the first "///:" in a string, and any preceding whitespace +const fenceSentinelRegex = /^\s*\/\/\/:/u; + +// Breaks a fence directive into its constituent components +// At this stage of parsing, we are looking for one of: +// - TERMINUS:COMMAND(PARAMS) +// - TERMINUS:COMMAND +const directiveParsingRegex = /^([A-Z]+):([A-Z_]+)(?:\(((?:\w+,)*\w+)\))?$/u; + +/** + * Removes fenced code from the given JavaScript source string. "Fenced code" + * includes the entire fence lines, including their trailing newlines, and the + * lines that they surround. + * + * A valid fence consists of two well-formed fence lines, separated by one or + * more lines that should be excluded. The first line must contain a `BEGIN` + * directive, and the second most contain an `END` directive. Both directives + * must specify the same command. + * + * Here's an example of a valid fence: + * + * ```javascript + * ///: BEGIN:ONLY_INCLUDE_IN(flask) + * console.log('I am Flask.'); + * ///: END:ONLY_INCLUDE_IN + * ``` + * + * For details, please see the documentation. + * + * @param {string} filePath - The path to the file being transformed. + * @param {string} typeOfCurrentBuild - The type of the current build. + * @param {string} fileContent - The contents of the file being transformed. + * @returns {[string, modified]} A tuple of the post-transform file contents and + * a boolean indicating whether they were modified. + */ +function removeFencedCode(filePath, typeOfCurrentBuild, fileContent) { + const matchedLines = [...fileContent.matchAll(linesWithFenceRegex)]; + + // If we didn't match any lines, return the unmodified file contents. + if (matchedLines.length === 0) { + return [fileContent, false]; + } + + // Parse fence lines + const parsedDirectives = matchedLines.map((matchArray) => { + const line = matchArray[0]; + + /* istanbul ignore next: should be impossible */ + if (!fenceSentinelRegex.test(line)) { + throw new Error( + getInvalidFenceLineMessage( + filePath, + line, + `Fence sentinel may only appear at the start of a line, optionally preceded by whitespace.`, + ), + ); + } + + // Store the start and end indices of each line + // Increment the end index by 1 to including the trailing newline when + // performing string operations. + const indices = [matchArray.index, matchArray.index + line.length + 1]; + + const lineWithoutSentinel = line.replace(fenceSentinelRegex, ''); + if (!/^ \w\w+/u.test(lineWithoutSentinel)) { + throw new Error( + getInvalidFenceLineMessage( + filePath, + line, + `Fence sentinel must be followed by a single space and an alphabetical string of two or more characters.`, + ), + ); + } + + const directiveMatches = lineWithoutSentinel + .trim() + .match(directiveParsingRegex); + + if (!directiveMatches) { + throw new Error( + getInvalidFenceLineMessage( + filePath, + line, + `Failed to parse fence directive.`, + ), + ); + } + + // The first element of a RegEx match array is the input + const [, terminus, command, parameters] = directiveMatches; + + if (!hasOwnProperty(DirectiveTerminuses, terminus)) { + throw new Error( + getInvalidFenceLineMessage( + filePath, + line, + `Line contains invalid directive terminus "${terminus}".`, + ), + ); + } + if (!hasOwnProperty(DirectiveCommands, command)) { + throw new Error( + getInvalidFenceLineMessage( + filePath, + line, + `Line contains invalid directive command "${command}".`, + ), + ); + } + + const parsed = { + line, + indices, + terminus, + command, + }; + + if (parameters !== undefined) { + parsed.parameters = parameters.split(','); + } + return parsed; + }); + + if (parsedDirectives.length % 2 !== 0) { + throw new Error( + getInvalidFenceStructureMessage( + filePath, + `A valid fence consists of two fence lines, but the file contains an uneven number, "${parsedDirectives.length}", of fence lines.`, + ), + ); + } + + // The below for-loop iterates over the parsed fence directives and performs + // the following work: + // - Ensures that the array of parsed directives consists of valid directive + // pairs, as specified in the documentation. + // - For each directive pair, determines whether their fenced lines should be + // removed for the current build, and if so, stores the indices we will use + // to splice the file content string. + + const splicingIndices = []; + let shouldSplice = false; + let currentCommand; + + for (let i = 0; i < parsedDirectives.length; i++) { + const { line, indices, terminus, command, parameters } = parsedDirectives[ + i + ]; + if (i % 2 === 0) { + if (terminus !== DirectiveTerminuses.BEGIN) { + throw new Error( + getInvalidFencePairMessage( + filePath, + line, + `The first directive of a pair must be a "BEGIN" directive.`, + ), + ); + } + + currentCommand = command; + // Throws an error if the command parameters are invalid + CommandValidators[command](parameters, filePath); + + if (parameters.includes(typeOfCurrentBuild)) { + shouldSplice = false; + } else { + shouldSplice = true; + // Add start index of BEGIN directive line to splicing indices + splicingIndices.push(indices[0]); + } + } else { + if (terminus !== DirectiveTerminuses.END) { + throw new Error( + getInvalidFencePairMessage( + filePath, + line, + `The second directive of a pair must be an "END" directive.`, + ), + ); + } + + /* istanbul ignore next: impossible until there's more than one command */ + if (command !== currentCommand) { + throw new Error( + getInvalidFencePairMessage( + filePath, + line, + `Expected "END" directive to have command "${currentCommand}" but found "${command}".`, + ), + ); + } + + // Forbid empty fences + const { line: previousLine, indices: previousIndices } = parsedDirectives[ + i - 1 + ]; + if (fileContent.substring(previousIndices[1], indices[0]).trim() === '') { + throw new Error( + `Empty fence found in file "${filePath}":\n${previousLine}\n${line}\n`, + ); + } + + if (shouldSplice) { + // Add end index of END directive line to splicing indices + splicingIndices.push(indices[1]); + } + } + } + + // This indicates that the present build type should include all fenced code, + // and so we just returned the unmodified file contents. + if (splicingIndices.length === 0) { + return [fileContent, false]; + } + + /* istanbul ignore next: should be impossible */ + if (splicingIndices.length % 2 !== 0) { + throw new Error( + `Internal error while transforming file "${filePath}":\nCollected an uneven number of splicing indices: "${splicingIndices.length}"`, + ); + } + + return [multiSplice(fileContent, splicingIndices), true]; +} + +/** + * Returns a copy of the given string, without the character ranges specified + * by the splicing indices array. + * + * The splicing indices must be a non-empty, even-length array of non-negative + * integers, specifying the character ranges to remove from the given string, as + * follows: + * + * `[ start, end, start, end, start, end, ... ]` + * + * @param {string} toSplice - The string to splice. + * @param {number[]} splicingIndices - Indices to splice at. + * @returns {string} The spliced string. + */ +function multiSplice(toSplice, splicingIndices) { + const retainedSubstrings = []; + + // Get the first part to be included + // The substring() call returns an empty string if splicingIndices[0] is 0, + // which is exactly what we want in that case. + retainedSubstrings.push(toSplice.substring(0, splicingIndices[0])); + + // This loop gets us all parts of the string that should be retained, except + // the first and the last. + // It iterates over all "end" indices of the array except the last one, and + // pushes the substring between each "end" index and the next "begin" index + // to the array of retained substrings. + if (splicingIndices.length > 2) { + for (let i = 1; i < splicingIndices.length; i += 2) { + retainedSubstrings.push( + toSplice.substring(splicingIndices[i], splicingIndices[i + 1]), + ); + } + } + + // Get the last part to be included + retainedSubstrings.push( + toSplice.substring(splicingIndices[splicingIndices.length - 1]), + ); + return retainedSubstrings.join(''); +} + +/** + * @param {string} filePath - The path to the file that caused the error. + * @param {string} line - The contents of the line with the error. + * @param {string} details - An explanation of the error. + * @returns The error message. + */ +function getInvalidFenceLineMessage(filePath, line, details) { + return `Invalid fence line in file "${filePath}": "${line}":\n${details}`; +} + +/** + * @param {string} filePath - The path to the file that caused the error. + * @param {string} details - An explanation of the error. + * @returns The error message. + */ +function getInvalidFenceStructureMessage(filePath, details) { + return `Invalid fence structure in file "${filePath}":\n${details}`; +} + +/** + * @param {string} filePath - The path to the file that caused the error. + * @param {string} line - The contents of the line with the error. + * @param {string} details - An explanation of the error. + * @returns The error message. + */ +function getInvalidFencePairMessage(filePath, line, details) { + return `Invalid fence pair in file "${filePath}" due to line "${line}":\n${details}`; +} + +/** + * @param {string} filePath - The path to the file that caused the error. + * @param {string} command - The command of the directive with the invalid + * parameters. + * @param {string} details - An explanation of the error. + * @returns The error message. + */ +function getInvalidParamsMessage(filePath, command, details) { + return `Invalid code fence parameters in file "${filePath}" for command "${command}":\n${details}`; +} diff --git a/development/build/transforms/remove-fenced-code.test.js b/development/build/transforms/remove-fenced-code.test.js new file mode 100644 index 000000000..10ee99add --- /dev/null +++ b/development/build/transforms/remove-fenced-code.test.js @@ -0,0 +1,663 @@ +const deepFreeze = require('deep-freeze-strict'); +const { BuildTypes } = require('../utils'); +const { + createRemoveFencedCodeTransform, + removeFencedCode, +} = require('./remove-fenced-code'); +const transformUtils = require('./utils'); + +jest.mock('./utils', () => ({ + lintTransformedFile: jest.fn(), +})); + +// The test data is just strings. We get it from a function at the end of this +// file because it takes up a lot of lines and is very distracting. +const testData = getTestData(); + +const getMinimalFencedCode = (params = 'flask') => + `///: BEGIN:ONLY_INCLUDE_IN(${params}) +Conditionally_Included +///: END:ONLY_INCLUDE_IN +`; + +describe('build/transforms/remove-fenced-code', () => { + describe('createRemoveFencedCodeTransform', () => { + const { lintTransformedFile: lintTransformedFileMock } = transformUtils; + const mockJsFileName = 'file.js'; + + beforeEach(() => { + lintTransformedFileMock.mockImplementation(() => Promise.resolve()); + }); + + it('rejects invalid build types', () => { + expect(() => createRemoveFencedCodeTransform('foobar')).toThrow( + /received unrecognized build type "foobar".$/u, + ); + }); + + it('returns a PassThrough stream for files with ignored extensions', async () => { + const fileContent = '"Valid JSON content"\n'; + const stream = createRemoveFencedCodeTransform('main')('file.json'); + let streamOutput = ''; + + await new Promise((resolve) => { + stream.on('data', (data) => { + streamOutput = streamOutput.concat(data.toString('utf8')); + }); + + stream.on('end', () => { + expect(streamOutput).toStrictEqual(fileContent); + expect(lintTransformedFileMock).not.toHaveBeenCalled(); + resolve(); + }); + + stream.write(Buffer.from(fileContent)); + setTimeout(() => stream.end()); + }); + }); + + it('transforms a file read as a single chunk', async () => { + const filePrefix = '// A comment\n'; + const fileContent = filePrefix.concat(getMinimalFencedCode()); + + const stream = createRemoveFencedCodeTransform('main')(mockJsFileName); + let streamOutput = ''; + + await new Promise((resolve) => { + stream.on('data', (data) => { + streamOutput = streamOutput.concat(data.toString('utf8')); + }); + + stream.on('end', () => { + expect(streamOutput).toStrictEqual(filePrefix); + expect(lintTransformedFileMock).toHaveBeenCalledTimes(1); + expect(lintTransformedFileMock).toHaveBeenCalledWith( + filePrefix, + mockJsFileName, + ); + resolve(); + }); + + stream.end(fileContent); + }); + }); + + it('transforms a file read as multiple chunks', async () => { + const filePrefix = '// A comment\n'; + const chunks = filePrefix + .concat(getMinimalFencedCode()) + .split('\n') + // The final element in the split array is the empty string, which is + // useful for calling .join, but undesirable here. + .filter((line) => line !== '') + .map((line) => `${line}\n`); + + const stream = createRemoveFencedCodeTransform('main')(mockJsFileName); + let streamOutput = ''; + + await new Promise((resolve) => { + stream.on('data', (data) => { + streamOutput = streamOutput.concat(data.toString('utf8')); + }); + + stream.on('end', () => { + expect(streamOutput).toStrictEqual(filePrefix); + expect(lintTransformedFileMock).toHaveBeenCalledTimes(1); + expect(lintTransformedFileMock).toHaveBeenCalledWith( + filePrefix, + mockJsFileName, + ); + resolve(); + }); + + chunks.forEach((chunk) => stream.write(chunk)); + setTimeout(() => stream.end()); + }); + }); + + it('handles file with fences that is unmodified by the transform', async () => { + const fileContent = getMinimalFencedCode('main'); + + const stream = createRemoveFencedCodeTransform('main')(mockJsFileName); + let streamOutput = ''; + + await new Promise((resolve) => { + stream.on('data', (data) => { + streamOutput = streamOutput.concat(data.toString('utf8')); + }); + + stream.on('end', () => { + expect(streamOutput).toStrictEqual(fileContent); + expect(lintTransformedFileMock).not.toHaveBeenCalled(); + resolve(); + }); + + stream.end(fileContent); + }); + }); + + it('skips linting for transformed file if shouldLintTransformedFiles is false', async () => { + const filePrefix = '// A comment\n'; + const fileContent = filePrefix.concat(getMinimalFencedCode()); + + const stream = createRemoveFencedCodeTransform( + 'main', + false, + )(mockJsFileName); + let streamOutput = ''; + + await new Promise((resolve) => { + stream.on('data', (data) => { + streamOutput = streamOutput.concat(data.toString('utf8')); + }); + + stream.on('end', () => { + expect(streamOutput).toStrictEqual(filePrefix); + expect(lintTransformedFileMock).not.toHaveBeenCalled(); + resolve(); + }); + + stream.end(fileContent); + }); + }); + + it('handles transformed file lint failure', async () => { + lintTransformedFileMock.mockImplementationOnce(() => + Promise.reject(new Error('lint failure')), + ); + + const filePrefix = '// A comment\n'; + const fileContent = filePrefix.concat(getMinimalFencedCode()); + + const stream = createRemoveFencedCodeTransform('main')(mockJsFileName); + + await new Promise((resolve) => { + stream.on('error', (error) => { + expect(error).toStrictEqual(new Error('lint failure')); + expect(lintTransformedFileMock).toHaveBeenCalledTimes(1); + expect(lintTransformedFileMock).toHaveBeenCalledWith( + filePrefix, + mockJsFileName, + ); + resolve(); + }); + + stream.end(fileContent); + }); + }); + }); + + describe('removeFencedCode', () => { + const mockFileName = 'file.js'; + + // Valid inputs + Object.keys(BuildTypes).forEach((buildType) => { + it(`transforms file with fences for build type "${buildType}"`, () => { + expect( + removeFencedCode( + mockFileName, + buildType, + testData.validInputs.withFences, + ), + ).toStrictEqual(testData.validOutputs[buildType]); + + // Ensure that the minimal input template is in fact valid + const minimalInput = getMinimalFencedCode(buildType); + expect( + removeFencedCode(mockFileName, buildType, minimalInput), + ).toStrictEqual([minimalInput, false]); + }); + + it(`does not modify file without fences for build type "${buildType}"`, () => { + expect( + removeFencedCode( + mockFileName, + buildType, + testData.validInputs.withoutFences, + ), + ).toStrictEqual([testData.validInputs.withoutFences, false]); + }); + }); + + // This is an edge case for the splicing function + it('transforms file with two fence lines', () => { + expect( + removeFencedCode( + mockFileName, + BuildTypes.flask, + getMinimalFencedCode('main'), + ), + ).toStrictEqual(['', true]); + }); + + it('ignores sentinels preceded by non-whitespace', () => { + const validBeginDirective = '///: BEGIN:ONLY_INCLUDE_IN(flask)\n'; + const ignoredLines = [ + `a ${validBeginDirective}`, + `2 ${validBeginDirective}`, + `@ ${validBeginDirective}`, + ]; + + ignoredLines.forEach((ignoredLine) => { + // These inputs will be transformed + expect( + removeFencedCode( + mockFileName, + BuildTypes.flask, + getMinimalFencedCode('main').concat(ignoredLine), + ), + ).toStrictEqual([ignoredLine, true]); + + const modifiedInputWithoutFences = testData.validInputs.withoutFences.concat( + ignoredLine, + ); + + // These inputs will not be transformed + expect( + removeFencedCode( + mockFileName, + BuildTypes.flask, + modifiedInputWithoutFences, + ), + ).toStrictEqual([modifiedInputWithoutFences, false]); + }); + }); + + // Invalid inputs + it('rejects empty fences', () => { + const jsComment = '// A comment\n'; + + const emptyFence = getMinimalFencedCode() + .split('\n') + .filter((line) => line.startsWith('///:')) + .map((line) => `${line}\n`) + .join(''); + + const emptyFenceWithPrefix = jsComment.concat(emptyFence); + const emptyFenceWithSuffix = emptyFence.concat(jsComment); + const emptyFenceSurrounded = emptyFenceWithPrefix.concat(jsComment); + + const inputs = [ + emptyFence, + emptyFenceWithPrefix, + emptyFenceWithSuffix, + emptyFenceSurrounded, + ]; + + inputs.forEach((input) => { + expect(() => + removeFencedCode(mockFileName, BuildTypes.flask, input), + ).toThrow( + `Empty fence found in file "${mockFileName}":\n${emptyFence}`, + ); + }); + }); + + it('rejects sentinels not followed by a single space and a multi-character alphabetical string', () => { + // Matches the sentinel and terminus component of the first line + // beginning with "///: TERMINUS" + const fenceSentinelAndTerminusRegex = /^\/\/\/: \w+/mu; + + const replacements = [ + '///:BEGIN', + '///:XBEGIN', + '///:_BEGIN', + '///:B', + '///:_', + '///: ', + '///: B', + '///:', + ]; + + replacements.forEach((replacement) => { + expect(() => + removeFencedCode( + mockFileName, + BuildTypes.flask, + getMinimalFencedCode().replace( + fenceSentinelAndTerminusRegex, + replacement, + ), + ), + ).toThrow( + /Fence sentinel must be followed by a single space and an alphabetical string of two or more characters.$/u, + ); + }); + }); + + it('rejects malformed BEGIN directives', () => { + // This is the first line of the minimal input template + const directiveString = '///: BEGIN:ONLY_INCLUDE_IN(flask)'; + + const replacements = [ + // Invalid terminus + '///: BE_GIN:ONLY_INCLUDE_IN(flask)', + '///: BE6IN:ONLY_INCLUDE_IN(flask)', + '///: BEGIN7:ONLY_INCLUDE_IN(flask)', + '///: BeGIN:ONLY_INCLUDE_IN(flask)', + '///: BE3:ONLY_INCLUDE_IN(flask)', + '///: BEG-IN:ONLY_INCLUDE_IN(flask)', + '///: BEG N:ONLY_INCLUDE_IN(flask)', + + // Invalid commands + '///: BEGIN:ONLY-INCLUDE_IN(flask)', + '///: BEGIN:ONLY_INCLUDE:IN(flask)', + '///: BEGIN:ONL6_INCLUDE_IN(flask)', + '///: BEGIN:ONLY_IN@LUDE_IN(flask)', + '///: BEGIN:ONLy_INCLUDE_IN(flask)', + '///: BEGIN:ONLY INCLUDE_IN(flask)', + + // Invalid parameters + '///: BEGIN:ONLY_INCLUDE_IN(,flask)', + '///: BEGIN:ONLY_INCLUDE_IN(flask,)', + '///: BEGIN:ONLY_INCLUDE_IN(flask,,main)', + '///: BEGIN:ONLY_INCLUDE_IN(,)', + '///: BEGIN:ONLY_INCLUDE_IN()', + '///: BEGIN:ONLY_INCLUDE_IN( )', + '///: BEGIN:ONLY_INCLUDE_IN(flask]', + '///: BEGIN:ONLY_INCLUDE_IN[flask)', + '///: BEGIN:ONLY_INCLUDE_IN(flask.main)', + '///: BEGIN:ONLY_INCLUDE_IN(flask,@)', + '///: BEGIN:ONLY_INCLUDE_IN(fla k)', + + // Stuff after the directive + '///: BEGIN:ONLY_INCLUDE_IN(flask) A', + '///: BEGIN:ONLY_INCLUDE_IN(flask) 9', + '///: BEGIN:ONLY_INCLUDE_IN(flask)A', + '///: BEGIN:ONLY_INCLUDE_IN(flask)9', + '///: BEGIN:ONLY_INCLUDE_IN(flask)_', + '///: BEGIN:ONLY_INCLUDE_IN(flask))', + ]; + + replacements.forEach((replacement) => { + expect(() => + removeFencedCode( + mockFileName, + BuildTypes.flask, + getMinimalFencedCode().replace(directiveString, replacement), + ), + ).toThrow( + new RegExp( + `${replacement.replace( + /([()[\]])/gu, + '\\$1', + )}":\nFailed to parse fence directive.$`, + 'u', + ), + ); + }); + }); + + it('rejects malformed END directives', () => { + // This is the last line of the minimal input template + const directiveString = '///: END:ONLY_INCLUDE_IN'; + + const replacements = [ + // Invalid terminus + '///: ENx:ONLY_INCLUDE_IN', + '///: EN3:ONLY_INCLUDE_IN', + '///: EN_:ONLY_INCLUDE_IN', + '///: EN :ONLY_INCLUDE_IN', + '///: EN::ONLY_INCLUDE_IN', + + // Invalid commands + '///: END:ONLY-INCLUDE_IN', + '///: END::ONLY_INCLUDE_IN', + '///: END:ONLY_INCLUDE:IN', + '///: END:ONL6_INCLUDE_IN', + '///: END:ONLY_IN@LUDE_IN', + '///: END:ONLy_INCLUDE_IN', + '///: END:ONLY INCLUDE_IN', + + // Stuff after the directive + '///: END:ONLY_INCLUDE_IN A', + '///: END:ONLY_INCLUDE_IN 9', + '///: END:ONLY_INCLUDE_IN _', + ]; + + replacements.forEach((replacement) => { + expect(() => + removeFencedCode( + mockFileName, + BuildTypes.flask, + getMinimalFencedCode().replace(directiveString, replacement), + ), + ).toThrow( + new RegExp( + `${replacement}":\nFailed to parse fence directive.$`, + 'u', + ), + ); + }); + }); + + it('rejects files with uneven number of fence lines', () => { + const additions = [ + '///: BEGIN:ONLY_INCLUDE_IN(flask)', + '///: END:ONLY_INCLUDE_IN', + ]; + additions.forEach((addition) => { + expect(() => + removeFencedCode( + mockFileName, + BuildTypes.flask, + getMinimalFencedCode().concat(addition), + ), + ).toThrow( + /A valid fence consists of two fence lines, but the file contains an uneven number, "3", of fence lines.$/u, + ); + }); + }); + + it('rejects invalid terminuses', () => { + const testCases = [ + ['BEGIN', ['KAPLAR', 'FLASK', 'FOO']], + ['END', ['KAPLAR', 'FOO', 'BAR']], + ]; + + testCases.forEach(([validTerminus, replacements]) => { + replacements.forEach((replacement) => { + expect(() => + removeFencedCode( + mockFileName, + BuildTypes.flask, + getMinimalFencedCode().replace(validTerminus, replacement), + ), + ).toThrow( + new RegExp( + `Line contains invalid directive terminus "${replacement}".$`, + 'u', + ), + ); + }); + }); + }); + + it('rejects invalid commands', () => { + const testCases = [ + [/ONLY_INCLUDE_IN\(/mu, ['ONLY_KEEP_IN(', 'FLASK(', 'FOO(']], + [/ONLY_INCLUDE_IN$/mu, ['ONLY_KEEP_IN', 'FLASK', 'FOO']], + ]; + + testCases.forEach(([validCommand, replacements]) => { + replacements.forEach((replacement) => { + expect(() => + removeFencedCode( + mockFileName, + BuildTypes.flask, + getMinimalFencedCode().replace(validCommand, replacement), + ), + ).toThrow( + new RegExp( + `Line contains invalid directive command "${replacement.replace( + '(', + '', + )}".$`, + 'u', + ), + ); + }); + }); + }); + + it('rejects invalid command parameters', () => { + const testCases = [ + ['bar', ['bar', 'flask,bar', 'flask,beta,main,bar']], + ['Foo', ['Foo', 'flask,Foo', 'flask,beta,main,Foo']], + ['b3ta', ['b3ta', 'flask,b3ta', 'flask,beta,main,b3ta']], + ['bEta', ['bEta', 'flask,bEta', 'flask,beta,main,bEta']], + ]; + + testCases.forEach(([invalidParam, replacements]) => { + replacements.forEach((replacement) => { + expect(() => + removeFencedCode( + mockFileName, + BuildTypes.flask, + getMinimalFencedCode(replacement), + ), + ).toThrow( + new RegExp(`"${invalidParam}" is not a valid build type.$`, 'u'), + ); + }); + }); + + // Should fail for empty params + expect(() => + removeFencedCode( + mockFileName, + BuildTypes.flask, + getMinimalFencedCode('').replace('()', ''), + ), + ).toThrow(/No params specified.$/u); + }); + + it('rejects directive pairs with wrong terminus order', () => { + // We need more than one directive pair for this test + const input = getMinimalFencedCode().concat(getMinimalFencedCode('beta')); + + const expectedBeginError = + 'The first directive of a pair must be a "BEGIN" directive.'; + const expectedEndError = + 'The second directive of a pair must be an "END" directive.'; + const testCases = [ + [ + 'BEGIN:ONLY_INCLUDE_IN(flask)', + 'END:ONLY_INCLUDE_IN', + expectedBeginError, + ], + [ + /END:ONLY_INCLUDE_IN/mu, + 'BEGIN:ONLY_INCLUDE_IN(main)', + expectedEndError, + ], + [ + 'BEGIN:ONLY_INCLUDE_IN(beta)', + 'END:ONLY_INCLUDE_IN', + expectedBeginError, + ], + ]; + + testCases.forEach(([target, replacement, expectedError]) => { + expect(() => + removeFencedCode( + mockFileName, + BuildTypes.flask, + input.replace(target, replacement), + ), + ).toThrow(expectedError); + }); + }); + + // We can't do this until there's more than one command + it.todo('rejects directive pairs with mismatched commands'); + }); +}); + +function getTestData() { + const data = { + validInputs: { + withFences: ` +///: BEGIN:ONLY_INCLUDE_IN(flask,beta) +Conditionally_Included +///: END:ONLY_INCLUDE_IN + Always_Included +Always_Included + Always_Included +Always_Included + ///: BEGIN:ONLY_INCLUDE_IN(flask,beta) + Conditionally_Included + + Conditionally_Included + Conditionally_Included + ///: END:ONLY_INCLUDE_IN +Always_Included + +Always_Included + Always_Included + ///: BEGIN:ONLY_INCLUDE_IN(flask) + + Conditionally_Included + Conditionally_Included + ///: END:ONLY_INCLUDE_IN +Always_Included + Always_Included +Always_Included + +///: BEGIN:ONLY_INCLUDE_IN(flask) + Conditionally_Included +Conditionally_Included + + ///: END:ONLY_INCLUDE_IN +`, + + withoutFences: ` + Always_Included +Always_Included + Always_Included +Always_Included +Always_Included + +Always_Included + Always_Included +Always_Included + Always_Included +Always_Included + +`, + }, + + validOutputs: { + beta: [ + ` +///: BEGIN:ONLY_INCLUDE_IN(flask,beta) +Conditionally_Included +///: END:ONLY_INCLUDE_IN + Always_Included +Always_Included + Always_Included +Always_Included + ///: BEGIN:ONLY_INCLUDE_IN(flask,beta) + Conditionally_Included + + Conditionally_Included + Conditionally_Included + ///: END:ONLY_INCLUDE_IN +Always_Included + +Always_Included + Always_Included +Always_Included + Always_Included +Always_Included + +`, + true, + ], + }, + }; + + data.validOutputs.flask = [data.validInputs.withFences, false]; + data.validOutputs.main = [data.validInputs.withoutFences, true]; + return deepFreeze(data); +} diff --git a/development/build/transforms/utils.js b/development/build/transforms/utils.js new file mode 100644 index 000000000..c438d5351 --- /dev/null +++ b/development/build/transforms/utils.js @@ -0,0 +1,68 @@ +const { ESLint } = require('eslint'); +const eslintrc = require('../../../.eslintrc.js'); + +/** + * The singleton ESLint instance. + * + * @type {ESLint} + */ +let eslintInstance; + +// We only need a single ESLint instance, and we only initialize it if necessary +const initializeESLint = () => { + if (!eslintInstance) { + eslintInstance = new ESLint({ baseConfig: eslintrc, useEslintrc: false }); + } +}; + +// Four spaces +const TAB = ' '; + +module.exports = { + lintTransformedFile, +}; + +/** + * Lints a transformed file by invoking ESLint programmatically on the string + * file contents. The path to the file must be specified so that the repository + * ESLint config can be applied properly. + * + * An error is thrown if linting produced any errors, or if the file is ignored + * by ESLint. Files linted by this function should never be ignored. + * + * @param {string} content - The file content. + * @param {string} filePath - The path to the file. + * @returns {Promise} Returns `undefined` or throws an error if linting produced + * any errors, or if the linted file is ignored. + */ +async function lintTransformedFile(content, filePath) { + initializeESLint(); + + const lintResult = ( + await eslintInstance.lintText(content, { filePath, warnIgnored: false }) + )[0]; + + // This indicates that the file is ignored, which should never be the case for + // a transformed file. + if (lintResult === undefined) { + throw new Error( + `MetaMask build: Transformed file "${filePath}" appears to be ignored by ESLint.`, + ); + } + + // This is the success case + if (lintResult.errorCount === 0) { + return; + } + + // Errors are stored in the messages array, and their "severity" is 2 + const errorsString = lintResult.messages + .filter(({ severity }) => severity === 2) + .reduce((allErrors, { message, ruleId }) => { + return allErrors.concat(`${TAB}${ruleId}\n${TAB}${message}\n\n`); + }, ''); + + throw new Error( + `MetaMask build: Lint errors encountered for transformed file "${filePath}":\n\n${errorsString}`, + ); +} diff --git a/development/build/transforms/utils.test.js b/development/build/transforms/utils.test.js new file mode 100644 index 000000000..ba273a15b --- /dev/null +++ b/development/build/transforms/utils.test.js @@ -0,0 +1,71 @@ +const { lintTransformedFile } = require('./utils'); + +let mockESLint; + +jest.mock('eslint', () => ({ + ESLint: class MockESLint { + constructor() { + if (mockESLint) { + throw new Error('Mock ESLint ref already assigned!'); + } + + // eslint-disable-next-line consistent-this + mockESLint = this; + + // eslint-disable-next-line jest/prefer-spy-on + this.lintText = jest.fn(); + } + }, +})); + +describe('transform utils', () => { + describe('lintTransformedFile', () => { + it('initializes the ESLint singleton', async () => { + expect(mockESLint).not.toBeDefined(); + + // This error is an artifact of how we're mocking the ESLint singleton, + // and won't actually occur in production. + await expect(() => lintTransformedFile()).rejects.toThrow( + `Cannot read property '0' of undefined`, + ); + expect(mockESLint).toBeDefined(); + }); + + it('returns if linting passes with no errors', async () => { + mockESLint.lintText.mockImplementationOnce(() => + Promise.resolve([{ errorCount: 0 }]), + ); + + expect( + await lintTransformedFile('/* JavaScript */', 'file.js'), + ).toBeUndefined(); + }); + + it('throws if the file is ignored by ESLint', async () => { + mockESLint.lintText.mockImplementationOnce(() => Promise.resolve([])); + + await expect(() => + lintTransformedFile('/* JavaScript */', 'file.js'), + ).rejects.toThrow( + /Transformed file "file\.js" appears to be ignored by ESLint\.$/u, + ); + }); + + it('throws if linting produced any errors', async () => { + const ruleId = 'some-eslint-rule'; + const message = 'You violated the rule!'; + + mockESLint.lintText.mockImplementationOnce(() => + Promise.resolve([ + { errorCount: 1, messages: [{ message, ruleId, severity: 2 }] }, + ]), + ); + + await expect(() => + lintTransformedFile('/* JavaScript */', 'file.js'), + ).rejects.toThrow( + /Lint errors encountered for transformed file "file\.js":\n\n {4}some-eslint-rule\n {4}You violated the rule!\n\n$/u, + ); + }); + }); +}); diff --git a/development/build/utils.js b/development/build/utils.js new file mode 100644 index 000000000..0b8503d19 --- /dev/null +++ b/development/build/utils.js @@ -0,0 +1,32 @@ +/** + * @returns {Object} An object with browser as key and next version of beta + * as the value. E.g. { firefox: '9.6.0.beta0', chrome: '9.6.0.1' } + */ +function getNextBetaVersionMap(currentVersion, betaVersion, platforms) { + const [major, minor] = currentVersion.split('.'); + + return platforms.reduce((platformMap, platform) => { + platformMap[platform] = [ + // Keeps the current major + major, + // Bump the minor version + Number(minor) + 1, + // This isn't typically used + 0, + // The beta number + `${platform === 'firefox' ? 'beta' : ''}${betaVersion}`, + ].join('.'); + return platformMap; + }, {}); +} + +const BuildTypes = { + beta: 'beta', + flask: 'flask', + main: 'main', +}; + +module.exports = { + BuildTypes, + getNextBetaVersionMap, +}; diff --git a/development/highlights/README.md b/development/highlights/README.md new file mode 100644 index 000000000..1f6788ea9 --- /dev/null +++ b/development/highlights/README.md @@ -0,0 +1,3 @@ +### highlights + +the purpose of this directory is to house utilities for generating "highlight" messages for the metamaskbot comment based on changes included in the PR \ No newline at end of file diff --git a/development/highlights/index.js b/development/highlights/index.js new file mode 100644 index 000000000..5ed891e5d --- /dev/null +++ b/development/highlights/index.js @@ -0,0 +1,31 @@ +const { promisify } = require('util'); +const exec = promisify(require('child_process').exec); +const storybook = require('./storybook.js'); + +module.exports = { getHighlights }; + +async function getHighlights({ artifactBase }) { + let highlights = ''; + // here we assume the PR base branch ("target") is `develop` in lieu of doing + // a query against the github api which requires an access token + // see https://discuss.circleci.com/t/how-to-retrieve-a-pull-requests-base-branch-name-github/36911 + const changedFiles = await getChangedFiles({ target: 'develop' }); + console.log(`detected changed files vs develop:`); + for (const filename of changedFiles) { + console.log(` ${filename}`); + } + const announcement = await storybook.getHighlightAnnouncement({ + changedFiles, + artifactBase, + }); + if (announcement) { + highlights += announcement; + } + return highlights; +} + +async function getChangedFiles({ target }) { + const { stdout } = await exec(`git diff --name-only ${target}...HEAD`); + const changedFiles = stdout.split('\n').slice(0, -1); + return changedFiles; +} diff --git a/development/highlights/storybook.js b/development/highlights/storybook.js new file mode 100644 index 000000000..9c71e129e --- /dev/null +++ b/development/highlights/storybook.js @@ -0,0 +1,86 @@ +const path = require('path'); +const { promisify } = require('util'); +const exec = promisify(require('child_process').exec); +const dependencyTree = require('dependency-tree'); + +const cwd = process.cwd(); +const resolutionCache = {}; + +// 1. load stories +// 2. load list per story +// 3. filter against files +module.exports = { + getHighlights, + getHighlightAnnouncement, +}; + +async function getHighlightAnnouncement({ changedFiles, artifactBase }) { + const highlights = await getHighlights({ changedFiles }); + if (!highlights.length) return null; + const highlightsBody = highlights + .map((entry) => `\n- [${entry}](${urlForStoryFile(entry, artifactBase)})`) + .join(''); + const announcement = `
+ storybook + ${highlightsBody} +
\n\n`; + return announcement; +} + +async function getHighlights({ changedFiles }) { + const highlights = []; + const storyFiles = await getAllStories(); + // check each story file for dep graph overlap with changed files + for (const storyFile of storyFiles) { + const list = await getLocalDependencyList(storyFile); + if (list.some((entry) => changedFiles.includes(entry))) { + highlights.push(storyFile); + } + } + return highlights; +} + +async function getAllStories() { + const { stdout } = await exec('find ui -name "*.stories.js"'); + const matches = stdout.split('\n').slice(0, -1); + return matches; +} + +async function getLocalDependencyList(filename) { + const list = dependencyTree + .toList({ + filename, + // not sure what this does but its mandatory + directory: cwd, + webpackConfig: `.storybook/main.js`, + // skip all dependencies + filter: (entry) => !entry.includes('node_modules'), + // for memoization across trees: 30s -> 5s + visited: resolutionCache, + }) + .map((entry) => path.relative(cwd, entry)); + return list; +} + +function urlForStoryFile(filename, artifactBase) { + const storyId = sanitize(filename); + return `${artifactBase}/storybook/index.html?path=/story/${storyId}`; +} + +/** + * Remove punctuation and illegal characters from a story ID. + * See: + * https://gist.github.com/davidjrice/9d2af51100e41c6c4b4a + * https://github.com/ComponentDriven/csf/blame/7ac941eee85816a4c567ca85460731acb5360f50/src/index.ts + */ +function sanitize(string) { + return ( + string + .toLowerCase() + // eslint-disable-next-line no-useless-escape + .replace(/[ ’–—―′¿'`~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/giu, '-') + .replace(/-+/gu, '-') + .replace(/^-+/u, '') + .replace(/-+$/u, '') + ); +} diff --git a/development/jest.config.js b/development/jest.config.js new file mode 100644 index 000000000..4eeaa048c --- /dev/null +++ b/development/jest.config.js @@ -0,0 +1,19 @@ +module.exports = { + displayName: '/development', + collectCoverageFrom: ['/**/*.js'], + coverageDirectory: '../jest-coverage/development/', + coverageReporters: ['json', 'lcov', 'text', 'clover'], + coverageThreshold: { + './development/build/transforms/**/*.js': { + branches: 100, + functions: 100, + lines: 100, + statements: 100, + }, + }, + resetMocks: true, + restoreMocks: true, + testEnvironment: 'node', + testMatch: ['/build/**/*.test.js'], + testTimeout: 2500, +}; diff --git a/development/metamaskbot-build-announce.js b/development/metamaskbot-build-announce.js index 8403a4843..ee547728b 100755 --- a/development/metamaskbot-build-announce.js +++ b/development/metamaskbot-build-announce.js @@ -4,6 +4,7 @@ const path = require('path'); const fetch = require('node-fetch'); const glob = require('fast-glob'); const VERSION = require('../dist/chrome/manifest.json').version; // eslint-disable-line import/no-unresolved +const { getHighlights } = require('./highlights'); start().catch(console.error); @@ -98,7 +99,7 @@ async function start() { .map((row) => `
  • ${row}
  • `) .join('\n')}`; const exposedContent = `Builds ready [${SHORT_SHA1}]`; - const artifactsBody = `
    ${exposedContent}${hiddenContent}
    `; + const artifactsBody = `
    ${exposedContent}${hiddenContent}
    \n\n`; const benchmarkResults = {}; for (const platform of platforms) { @@ -124,7 +125,7 @@ async function start() { const summaryPlatform = 'chrome'; const summaryPage = 'home'; - let commentBody; + let commentBody = artifactsBody; if (benchmarkResults[summaryPlatform]) { try { const summaryPageLoad = Math.round( @@ -196,15 +197,23 @@ async function start() { .join('')}`; const benchmarkTableBody = `${tableRows.join('')}`; const benchmarkTable = `${benchmarkTableHeader}${benchmarkTableBody}
    `; - const benchmarkBody = `
    ${benchmarkSummary}${benchmarkTable}
    `; - commentBody = `${artifactsBody}${benchmarkBody}`; + const benchmarkBody = `
    ${benchmarkSummary}${benchmarkTable}
    \n\n`; + commentBody += `${benchmarkBody}`; } catch (error) { console.error(`Error constructing benchmark results: '${error}'`); - commentBody = artifactsBody; } } else { console.log(`No results for ${summaryPlatform} found; skipping benchmark`); - commentBody = artifactsBody; + } + + try { + const highlights = await getHighlights({ artifactBase: BUILD_LINK_BASE }); + if (highlights) { + const highlightsBody = `### highlights:\n${highlights}\n`; + commentBody += highlightsBody; + } + } catch (error) { + console.error(`Error constructing highlight results: '${error}'`); } const JSON_PAYLOAD = JSON.stringify({ body: commentBody }); diff --git a/docs/extension_description/en.txt b/docs/extension_description/en.txt index 53bc4780d..b52d476cc 100644 --- a/docs/extension_description/en.txt +++ b/docs/extension_description/en.txt @@ -5,4 +5,4 @@ The extension injects the Ethereum web3 API into every website's javascript cont MetaMask also lets the user create and manage their own identities, so when a Dapp wants to perform a transaction and write to the blockchain, the user gets a secure interface to review the transaction, before approving or rejecting it. Because it adds functionality to the normal browser context, MetaMask requires the permission to read and write to any webpage. You can always "view the source" of MetaMask the way you do any extension, or view the source code on Github: -https://github.com/MetaMask/metamask-plugin \ No newline at end of file +https://github.com/MetaMask/metamask-extension \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index 7b556fa22..1c0d0692a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,23 +1,22 @@ module.exports = { - restoreMocks: true, - coverageDirectory: 'jest-coverage/', - collectCoverageFrom: [ - '/ui/**/swaps/**', - '/ui/ducks/send/**', - ], + displayName: '/ui, /shared', + collectCoverageFrom: ['/ui/**/*.js', '/shared/**/*.js'], + coverageDirectory: './jest-coverage/main', coveragePathIgnorePatterns: ['.stories.js', '.snap'], + coverageReporters: ['json', 'lcov', 'text', 'clover'], coverageThreshold: { global: { - branches: 45.24, - functions: 51.94, - lines: 58.36, - statements: 58.6, + branches: 35, + functions: 37, + lines: 43, + statements: 43, }, }, - setupFiles: ['./test/setup.js', './test/env.js'], - setupFilesAfterEnv: ['./test/jest/setup.js'], - testMatch: [ - '/ui/**/?(*.)+(test).js', - '/shared/**/?(*.)+(test).js', - ], + // TODO: enable resetMocks + // resetMocks: true, + restoreMocks: true, + setupFiles: ['/test/setup.js', '/test/env.js'], + setupFilesAfterEnv: ['/test/jest/setup.js'], + testMatch: ['/ui/**/*.test.js', '/shared/**/*.test.js'], + testTimeout: 2500, }; diff --git a/lavamoat/node/policy.json b/lavamoat/node/policy.json index d9eef720a..0cdbcf152 100644 --- a/lavamoat/node/policy.json +++ b/lavamoat/node/policy.json @@ -785,6 +785,42 @@ "console.log": true } }, + "@eslint/eslintrc": { + "builtin": { + "assert": true, + "fs.existsSync": true, + "fs.readFileSync": true, + "module.createRequire": true, + "module.createRequireFromPath": true, + "os.homedir": true, + "path.basename": true, + "path.dirname": true, + "path.extname": true, + "path.isAbsolute": true, + "path.join": true, + "path.relative": true, + "path.resolve": true, + "path.sep": true, + "util.inspect": true + }, + "globals": { + "__dirname": true, + "process.cwd": true, + "process.emitWarning": true, + "process.platform": true + }, + "packages": { + "ajv": true, + "debug": true, + "espree": true, + "globals": true, + "ignore": true, + "import-fresh": true, + "js-yaml": true, + "minimatch": true, + "strip-json-comments": true + } + }, "@gulp-sourcemaps/identity-map": { "packages": { "acorn": true, @@ -897,6 +933,11 @@ "acorn": true } }, + "acorn-jsx": { + "packages": { + "acorn": true + } + }, "acorn-node": { "packages": { "acorn": true, @@ -911,8 +952,14 @@ } }, "ajv": { + "globals": { + "console": true + }, "packages": { - "fast-deep-equal": true + "fast-deep-equal": true, + "fast-json-stable-stringify": true, + "json-schema-traverse": true, + "uri-js": true } }, "amdefine": { @@ -970,6 +1017,16 @@ "buffer-equal": true } }, + "are-we-there-yet": { + "builtin": { + "events.EventEmitter": true, + "util.inherits": true + }, + "packages": { + "delegates": true, + "readable-stream": true + } + }, "arr-diff": { "packages": { "arr-flatten": true, @@ -1329,6 +1386,7 @@ "anymatch": true, "async-each": true, "braces": true, + "fsevents": true, "glob-parent": true, "inherits": true, "is-binary-path": true, @@ -1580,6 +1638,16 @@ "through2": true } }, + "detect-libc": { + "builtin": { + "child_process.spawnSync": true, + "fs.readdirSync": true, + "os.platform": true + }, + "globals": { + "process.env": true + } + }, "detective": { "packages": { "acorn-node": true, @@ -1600,6 +1668,14 @@ "path-type": true } }, + "doctrine": { + "builtin": { + "assert": true + }, + "packages": { + "esutils": true + } + }, "dom-serializer": { "packages": { "domelementtype": true, @@ -1729,6 +1805,92 @@ "source-map": true } }, + "eslint": { + "builtin": { + "assert": true, + "fs.existsSync": true, + "fs.lstatSync": true, + "fs.readFileSync": true, + "fs.readdirSync": true, + "fs.statSync": true, + "fs.unlinkSync": true, + "fs.writeFile": true, + "fs.writeFileSync": true, + "path.extname": true, + "path.isAbsolute": true, + "path.join": true, + "path.normalize": true, + "path.relative": true, + "path.resolve": true, + "path.sep": true, + "util.format": true, + "util.inspect": true, + "util.promisify": true + }, + "globals": { + "__dirname": true, + "console.log": true, + "describe": true, + "it": true, + "process": true + }, + "packages": { + "@eslint/eslintrc": true, + "ajv": true, + "debug": true, + "doctrine": true, + "eslint-scope": true, + "eslint-utils": true, + "eslint-visitor-keys": true, + "espree": true, + "esquery": true, + "esutils": true, + "file-entry-cache": true, + "functional-red-black-tree": true, + "glob-parent": true, + "globals": true, + "ignore": true, + "imurmurhash": true, + "is-glob": true, + "json-stable-stringify-without-jsonify": true, + "levn": true, + "lodash": true, + "minimatch": true, + "natural-compare": true, + "regexpp": true + } + }, + "eslint-scope": { + "builtin": { + "assert": true + }, + "packages": { + "esrecurse": true, + "estraverse": true + } + }, + "eslint-utils": { + "packages": { + "eslint-visitor-keys": true + } + }, + "espree": { + "packages": { + "acorn": true, + "acorn-jsx": true, + "eslint-visitor-keys": true + } + }, + "esquery": { + "globals": { + "define": true + } + }, + "esrecurse": { + "packages": { + "estraverse": true + } + }, "event-emitter": { "packages": { "d": true, @@ -1911,7 +2073,9 @@ "flat-cache": { "builtin": { "fs.existsSync": true, + "fs.mkdirSync": true, "fs.readFileSync": true, + "fs.writeFileSync": true, "path.basename": true, "path.dirname": true, "path.resolve": true @@ -2020,6 +2184,45 @@ "process.version": true } }, + "fsevents": { + "builtin": { + "events.EventEmitter": true, + "fs.stat": true, + "path.join": true, + "util.inherits": true + }, + "globals": { + "__dirname": true, + "process.nextTick": true, + "process.platform": true, + "setImmediate": true + }, + "native": true, + "packages": { + "node-pre-gyp": true + } + }, + "gauge": { + "builtin": { + "util.format": true + }, + "globals": { + "clearInterval": true, + "process": true, + "setImmediate": true, + "setInterval": true + }, + "packages": { + "aproba": true, + "console-control-strings": true, + "has-unicode": true, + "object-assign": true, + "signal-exit": true, + "string-width": true, + "strip-ansi": true, + "wide-align": true + } + }, "get-assigned-identifiers": { "builtin": { "assert.equal": true @@ -2380,6 +2583,16 @@ "process.argv": true } }, + "has-unicode": { + "builtin": { + "os.type": true + }, + "globals": { + "process.env.LANG": true, + "process.env.LC_ALL": true, + "process.env.LC_CTYPE": true + } + }, "has-value": { "packages": { "get-value": true, @@ -2533,6 +2746,11 @@ "is-plain-object": true } }, + "is-fullwidth-code-point": { + "packages": { + "number-is-nan": true + } + }, "is-glob": { "packages": { "is-extglob": true @@ -2611,6 +2829,11 @@ "isarray": true } }, + "js-yaml": { + "globals": { + "esprima": true + } + }, "jsesc": { "globals": { "Buffer.isBuffer": true @@ -2688,6 +2911,12 @@ "flush-write-stream": true } }, + "levn": { + "packages": { + "prelude-ls": true, + "type-check": true + } + }, "lodash": { "globals": { "define": true @@ -2917,6 +3146,56 @@ "setTimeout": true } }, + "node-pre-gyp": { + "builtin": { + "events.EventEmitter": true, + "fs.existsSync": true, + "fs.readFileSync": true, + "fs.renameSync": true, + "path.dirname": true, + "path.existsSync": true, + "path.join": true, + "path.resolve": true, + "url.parse": true, + "url.resolve": true, + "util.inherits": true + }, + "globals": { + "__dirname": true, + "console.log": true, + "process.arch": true, + "process.cwd": true, + "process.env": true, + "process.platform": true, + "process.version.substr": true, + "process.versions": true + }, + "packages": { + "detect-libc": true, + "nopt": true, + "npmlog": true, + "rimraf": true, + "semver": true + } + }, + "nopt": { + "builtin": { + "path": true, + "stream.Stream": true, + "url": true + }, + "globals": { + "console": true, + "process.argv": true, + "process.env.DEBUG_NOPT": true, + "process.env.NOPT_DEBUG": true, + "process.platform": true + }, + "packages": { + "abbrev": true, + "osenv": true + } + }, "normalize-path": { "packages": { "remove-trailing-separator": true @@ -2932,6 +3211,22 @@ "once": true } }, + "npmlog": { + "builtin": { + "events.EventEmitter": true, + "util": true + }, + "globals": { + "process.nextTick": true, + "process.stderr": true + }, + "packages": { + "are-we-there-yet": true, + "console-control-strings": true, + "gauge": true, + "set-blocking": true + } + }, "object-copy": { "packages": { "copy-descriptor": true, @@ -2998,6 +3293,54 @@ "readable-stream": true } }, + "os-homedir": { + "builtin": { + "os.homedir": true + }, + "globals": { + "process.env": true, + "process.getuid": true, + "process.platform": true + } + }, + "os-tmpdir": { + "globals": { + "process.env.SystemRoot": true, + "process.env.TEMP": true, + "process.env.TMP": true, + "process.env.TMPDIR": true, + "process.env.windir": true, + "process.platform": true + } + }, + "osenv": { + "builtin": { + "child_process.exec": true, + "path": true + }, + "globals": { + "process.env.COMPUTERNAME": true, + "process.env.ComSpec": true, + "process.env.EDITOR": true, + "process.env.HOSTNAME": true, + "process.env.PATH": true, + "process.env.PROMPT": true, + "process.env.PS1": true, + "process.env.Path": true, + "process.env.SHELL": true, + "process.env.USER": true, + "process.env.USERDOMAIN": true, + "process.env.USERNAME": true, + "process.env.VISUAL": true, + "process.env.path": true, + "process.nextTick": true, + "process.platform": true + }, + "packages": { + "os-homedir": true, + "os-tmpdir": true + } + }, "parent-module": { "packages": { "callsites": true @@ -3580,6 +3923,12 @@ "process": true } }, + "set-blocking": { + "globals": { + "process.stderr": true, + "process.stdout": true + } + }, "set-value": { "packages": { "extend-shallow": true, @@ -3782,6 +4131,7 @@ }, "string-width": { "packages": { + "code-point-at": true, "emoji-regex": true, "is-fullwidth-code-point": true, "strip-ansi": true @@ -4040,6 +4390,11 @@ "through2": true } }, + "type-check": { + "packages": { + "prelude-ls": true + } + }, "typedarray-to-buffer": { "globals": { "Buffer.from": true @@ -4145,6 +4500,11 @@ "path": true } }, + "uri-js": { + "globals": { + "define": true + } + }, "urix": { "builtin": { "path.sep": true @@ -4348,6 +4708,11 @@ "isexe": true } }, + "wide-align": { + "packages": { + "string-width": true + } + }, "write": { "builtin": { "fs.createWriteStream": true, diff --git a/package.json b/package.json index 880543a3e..c41dfdcef 100644 --- a/package.json +++ b/package.json @@ -26,10 +26,7 @@ "dapp-forwarder": "concurrently -k -n forwarder,dapp -p '[{time}][{name}]' 'yarn forwarder' 'yarn dapp'", "test:unit": "mocha --exit --require test/env.js --require test/setup.js --recursive './app/**/*.test.js'", "test:unit:global": "mocha --exit --require test/env.js --require test/setup.js --recursive test/unit-global/*.test.js", - "test:unit:jest": "jest", - "test:unit:jest:watch": "jest --watch", - "test:unit:jest:watch:silent": "jest --watch --silent", - "test:unit:jest:ci": "jest --maxWorkers=2", + "test:unit:jest": "./test/run-jest.sh", "test:unit:lax": "mocha --exit --require test/env.js --require test/setup.js --ignore './app/scripts/controllers/permissions/*.test.js' --recursive './app/**/*.test.js'", "test:unit:strict": "mocha --exit --require test/env.js --require test/setup.js --recursive './app/scripts/controllers/permissions/*.test.js'", "test:unit:path": "mocha --exit --require test/env.js --require test/setup.js --recursive", @@ -37,9 +34,9 @@ "test:e2e:chrome:metrics": "SELENIUM_BROWSER=chrome node test/e2e/run-e2e-test.js test/e2e/metrics.spec.js", "test:e2e:firefox": "SELENIUM_BROWSER=firefox node test/e2e/run-all.js", "test:e2e:firefox:metrics": "SELENIUM_BROWSER=firefox node test/e2e/run-e2e-test.js test/e2e/metrics.spec.js", - "test:coverage": "nyc --silent --check-coverage yarn test:unit:strict && nyc --silent --no-clean yarn test:unit:lax && nyc report --reporter=text --reporter=html", "test:e2e:single": "node test/e2e/run-e2e-test.js", - "test:coverage:jest": "jest --coverage --maxWorkers=2", + "test:coverage": "nyc --silent --check-coverage yarn test:unit:strict && nyc --silent --no-clean yarn test:unit:lax && nyc report --reporter=text --reporter=html", + "test:coverage:jest": "yarn test:unit:jest --coverage --maxWorkers=2", "test:coverage:strict": "nyc --check-coverage yarn test:unit:strict", "test:coverage:path": "nyc --check-coverage yarn test:unit:path", "ganache:start": "./development/run-ganache.sh", @@ -48,6 +45,8 @@ "lint:fix": "prettier --write '**/*.json' && eslint . --ext js --cache --fix", "lint:changed": "{ git ls-files --others --exclude-standard ; git diff-index --name-only --diff-filter=d HEAD ; } | grep --regexp='[.]js$' | tr '\\n' '\\0' | xargs -0 eslint", "lint:changed:fix": "{ git ls-files --others --exclude-standard ; git diff-index --name-only --diff-filter=d HEAD ; } | grep --regexp='[.]js$' | tr '\\n' '\\0' | xargs -0 eslint --fix", + "lint:changelog": "auto-changelog validate", + "lint:changelog:rc": "auto-changelog validate --rc", "lint:shellcheck": "./development/shellcheck.sh", "lint:styles": "stylelint '*/**/*.scss'", "lint:lockfile": "lockfile-lint --path yarn.lock --allowed-hosts npm yarn github.com codeload.github.com --empty-hostname false --allowed-schemes \"https:\" \"git+https:\"", @@ -60,8 +59,8 @@ "devtools:redux": "remotedev --hostname=localhost --port=8000", "start:dev": "concurrently -k -n build,react,redux yarn:start yarn:devtools:react yarn:devtools:redux", "announce": "node development/announcer.js", - "storybook": "start-storybook -p 6006 -c .storybook --static-dir ./app ./storybook/images", - "storybook:build": "build-storybook -c .storybook -o storybook-build --static-dir ./app ./storybook/images", + "storybook": "start-storybook -p 6006 -c .storybook -s ./app,./.storybook/images", + "storybook:build": "build-storybook -c .storybook -o storybook-build -s ./app,./.storybook/images", "storybook:deploy": "storybook-to-ghpages --existing-output-dir storybook-build --remote storybook --branch master", "update-changelog": "auto-changelog update", "generate:migration": "./development/generate-migration.sh", @@ -108,7 +107,7 @@ "@metamask/eth-token-tracker": "^3.0.1", "@metamask/etherscan-link": "^2.1.0", "@metamask/jazzicon": "^2.0.0", - "@metamask/logo": "^2.5.0", + "@metamask/logo": "^3.0.1", "@metamask/obs-store": "^5.0.0", "@metamask/post-message-stream": "^4.0.0", "@metamask/providers": "^8.1.1", @@ -117,7 +116,6 @@ "@sentry/browser": "^5.26.0", "@sentry/integrations": "^5.26.0", "@zxing/library": "^0.8.0", - "abortcontroller-polyfill": "^1.4.0", "analytics-node": "^3.4.0-beta.3", "await-semaphore": "^0.1.1", "base32-encode": "^1.2.0", @@ -156,7 +154,6 @@ "fast-safe-stringify": "^2.0.7", "fuse.js": "^3.2.0", "globalthis": "^1.0.1", - "human-standard-collectible-abi": "^1.0.2", "human-standard-token-abi": "^2.0.0", "immer": "^8.0.1", "json-rpc-engine": "^6.1.0", @@ -248,7 +245,7 @@ "brfs": "^2.0.2", "browserify": "^16.5.1", "chalk": "^3.0.0", - "chromedriver": "^87.0.1", + "chromedriver": "^93.0.1", "concurrently": "^5.2.0", "copy-webpack-plugin": "^6.0.3", "cross-spawn": "^7.0.3", @@ -256,6 +253,7 @@ "css-to-xpath": "^0.1.0", "del": "^3.0.0", "depcheck": "^1.4.2", + "dependency-tree": "^8.1.1", "duplexify": "^4.1.1", "enzyme": "^3.10.0", "enzyme-adapter-react-16": "^1.15.1", @@ -292,6 +290,7 @@ "lavamoat-viz": "^6.0.9", "lockfile-lint": "^4.0.0", "loose-envify": "^1.4.0", + "minimist": "^1.2.5", "mocha": "^7.2.0", "nock": "^9.0.14", "node-fetch": "^2.6.1", diff --git a/shared/constants/metametrics.js b/shared/constants/metametrics.js index ab71141bc..e9262d750 100644 --- a/shared/constants/metametrics.js +++ b/shared/constants/metametrics.js @@ -110,7 +110,9 @@ * the page view */ -export const METAMETRICS_ANONYMOUS_ID = '0x0000000000000000'; +// An empty string "" is a, currently undocumented, way of telling mixpanel +// that these events are meant to be anonymous and not identified to any user +export const METAMETRICS_ANONYMOUS_ID = '""'; /** * This object is used to identify events that are triggered by the background diff --git a/shared/constants/network.js b/shared/constants/network.js index 2c4f24467..3087a68e0 100644 --- a/shared/constants/network.js +++ b/shared/constants/network.js @@ -122,6 +122,7 @@ export const NATIVE_CURRENCY_TOKEN_IMAGE_MAP = { [ETH_SYMBOL]: ETH_TOKEN_IMAGE_URL, [TEST_ETH_SYMBOL]: TEST_ETH_TOKEN_IMAGE_URL, [BNB_SYMBOL]: BNB_TOKEN_IMAGE_URL, + [MATIC_SYMBOL]: MATIC_TOKEN_IMAGE_URL, }; export const INFURA_BLOCKED_KEY = 'countryBlocked'; diff --git a/shared/constants/swaps.js b/shared/constants/swaps.js index b9d75f99a..5da0d08d8 100644 --- a/shared/constants/swaps.js +++ b/shared/constants/swaps.js @@ -75,8 +75,14 @@ const BSC_CONTRACT_ADDRESS = '0x1a1ec25dc08e98e5e93f1104b5e5cdd298707d31'; // It's the same as we use for BSC. const POLYGON_CONTRACT_ADDRESS = '0x1a1ec25dc08e98e5e93f1104b5e5cdd298707d31'; -export const ETH_WETH_CONTRACT_ADDRESS = +export const WETH_CONTRACT_ADDRESS = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; +export const WETH_RINKEBY_CONTRACT_ADDRESS = + '0xc778417e063141139fce010982780140aa0cd5ab'; +export const WBNB_CONTRACT_ADDRESS = + '0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c'; +export const WMATIC_CONTRACT_ADDRESS = + '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270'; const METASWAP_ETH_API_HOST = 'https://api.metaswap.codefi.network'; @@ -85,6 +91,13 @@ const METASWAP_BSC_API_HOST = 'https://bsc-api.metaswap.codefi.network'; const SWAPS_TESTNET_CHAIN_ID = '0x539'; const SWAPS_TESTNET_HOST = 'https://metaswap-api.airswap-dev.codefi.network'; +export const SWAPS_API_V2_BASE_URL = 'https://api2.metaswap.codefi.network'; +export const SWAPS_DEV_API_V2_BASE_URL = + 'https://api2.metaswap-dev.codefi.network'; +export const GAS_API_BASE_URL = 'https://gas-api.metaswap.codefi.network'; +export const GAS_DEV_API_BASE_URL = + 'https://gas-api.metaswap-dev.codefi.network'; + const BSC_DEFAULT_BLOCK_EXPLORER_URL = 'https://bscscan.com/'; const MAINNET_DEFAULT_BLOCK_EXPLORER_URL = 'https://etherscan.io/'; const RINKEBY_DEFAULT_BLOCK_EXPLORER_URL = 'https://rinkeby.etherscan.io/'; @@ -114,6 +127,37 @@ export const SWAPS_CHAINID_CONTRACT_ADDRESS_MAP = { [RINKEBY_CHAIN_ID]: TESTNET_CONTRACT_ADDRESS, }; +export const SWAPS_WRAPPED_TOKENS_ADDRESSES = { + [MAINNET_CHAIN_ID]: WETH_CONTRACT_ADDRESS, + [SWAPS_TESTNET_CHAIN_ID]: WETH_CONTRACT_ADDRESS, + [BSC_CHAIN_ID]: WBNB_CONTRACT_ADDRESS, + [POLYGON_CHAIN_ID]: WMATIC_CONTRACT_ADDRESS, + [RINKEBY_CHAIN_ID]: WETH_RINKEBY_CONTRACT_ADDRESS, +}; + +export const ALLOWED_CONTRACT_ADDRESSES = { + [MAINNET_CHAIN_ID]: [ + SWAPS_CHAINID_CONTRACT_ADDRESS_MAP[MAINNET_CHAIN_ID], + SWAPS_WRAPPED_TOKENS_ADDRESSES[MAINNET_CHAIN_ID], + ], + [SWAPS_TESTNET_CHAIN_ID]: [ + SWAPS_CHAINID_CONTRACT_ADDRESS_MAP[SWAPS_TESTNET_CHAIN_ID], + SWAPS_WRAPPED_TOKENS_ADDRESSES[SWAPS_TESTNET_CHAIN_ID], + ], + [RINKEBY_CHAIN_ID]: [ + SWAPS_CHAINID_CONTRACT_ADDRESS_MAP[RINKEBY_CHAIN_ID], + SWAPS_WRAPPED_TOKENS_ADDRESSES[RINKEBY_CHAIN_ID], + ], + [BSC_CHAIN_ID]: [ + SWAPS_CHAINID_CONTRACT_ADDRESS_MAP[BSC_CHAIN_ID], + SWAPS_WRAPPED_TOKENS_ADDRESSES[BSC_CHAIN_ID], + ], + [POLYGON_CHAIN_ID]: [ + SWAPS_CHAINID_CONTRACT_ADDRESS_MAP[POLYGON_CHAIN_ID], + SWAPS_WRAPPED_TOKENS_ADDRESSES[POLYGON_CHAIN_ID], + ], +}; + export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = { [MAINNET_CHAIN_ID]: ETH_SWAPS_TOKEN_OBJECT, [SWAPS_TESTNET_CHAIN_ID]: TEST_ETH_SWAPS_TOKEN_OBJECT, diff --git a/shared/constants/transaction.js b/shared/constants/transaction.js index 09d780677..d886c030c 100644 --- a/shared/constants/transaction.js +++ b/shared/constants/transaction.js @@ -11,7 +11,7 @@ import { MESSAGE_TYPE } from './app'; * @property {'approve'} TOKEN_METHOD_APPROVE - A token transaction requesting an * allowance of the token to spend on behalf of the user * @property {'incoming'} INCOMING - An incoming (deposit) transaction - * @property {'sentEther'} SENT_ETHER - A transaction sending ether to a recipient + * @property {'simpleSend'} SIMPLE_SEND - A transaction sending a network's native asset to a recipient * @property {'contractInteraction'} CONTRACT_INTERACTION - A transaction that is * interacting with a smart contract's methods that we have not treated as a special * case, such as approve, transfer, and transferfrom @@ -48,7 +48,7 @@ export const TRANSACTION_TYPES = { TOKEN_METHOD_TRANSFER_FROM: 'transferfrom', TOKEN_METHOD_APPROVE: 'approve', INCOMING: 'incoming', - SENT_ETHER: 'sentEther', + SIMPLE_SEND: 'simpleSend', CONTRACT_INTERACTION: 'contractInteraction', DEPLOY_CONTRACT: 'contractDeployment', SWAP: 'swap', diff --git a/shared/modules/fetch-with-timeout.test.js b/shared/modules/fetch-with-timeout.test.js index a7400e617..d7b1f2624 100644 --- a/shared/modules/fetch-with-timeout.test.js +++ b/shared/modules/fetch-with-timeout.test.js @@ -30,7 +30,9 @@ describe('getFetchWithTimeout', function () { throw new Error('Request should throw'); }; - await expect(fetchWithTimeoutThrowsError()).rejects.toThrow('Aborted'); + await expect(fetchWithTimeoutThrowsError()).rejects.toThrow( + 'The user aborted a request.', + ); }); it('should abort the request when the custom timeout is hit', async function () { @@ -48,7 +50,9 @@ describe('getFetchWithTimeout', function () { throw new Error('Request should be aborted'); }; - await expect(fetchWithTimeoutThrowsError()).rejects.toThrow('Aborted'); + await expect(fetchWithTimeoutThrowsError()).rejects.toThrow( + 'The user aborted a request.', + ); }); it('throws on invalid timeout', async function () { diff --git a/test/data/fetch-mocks.json b/test/data/fetch-mocks.json index 9299b4602..f9b2885f2 100644 --- a/test/data/fetch-mocks.json +++ b/test/data/fetch-mocks.json @@ -25,5 +25,49 @@ "fallback_to_v1": false } } + }, + "tokenList": { + "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0": { + "address": "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0", + "symbol": "MATIC", + "decimals": 18, + "name": "Polygon", + "iconUrl": "https://raw.githubusercontent.com/MetaMask/eth-contract-metadata/master/images/matic-network-logo.svg", + "aggregators": [ + "airswapLight", + "bancor", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + }, + "0x0d8775f648430679a709e98d2b0cb6250d2887ef": { + "address": "0x0d8775f648430679a709e98d2b0cb6250d2887ef", + "symbol": "BAT", + "decimals": 18, + "name": "Basic Attention Tok", + "iconUrl": "https://s3.amazonaws.com/airswap-token-images/BAT.png", + "aggregators": [ + "airswapLight", + "bancor", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + } } } diff --git a/test/data/mock-state.json b/test/data/mock-state.json index caaa378d8..8b6171339 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -154,6 +154,53 @@ "editingTransactionId": null, "toNickname": "" }, + "useTokenDetection": true, + "tokenList": { + "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599": { + "address": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + "symbol": "WBTC", + "decimals": 8, + "name": "Wrapped Bitcoin", + "iconUrl": "https://s3.amazonaws.com/airswap-token-images/WBTC.png", + "aggregators": [ + "airswapLight", + "bancor", + "cmc", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 12 + }, + "0x0bc529c00c6401aef6d220be8c6ea1667f6ad93e": { + "address": "0x0bc529c00c6401aef6d220be8c6ea1667f6ad93e", + "symbol": "YFI", + "decimals": 18, + "name": "yearn.finance", + "iconUrl": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e/logo.png", + "aggregators": [ + "airswapLight", + "bancor", + "cmc", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 12 + } + }, "currentNetworkTxList": [ { "id": 3387511061307736, diff --git a/test/data/transaction-data.json b/test/data/transaction-data.json index 740e522d1..dbc1ec138 100644 --- a/test/data/transaction-data.json +++ b/test/data/transaction-data.json @@ -16,7 +16,7 @@ "gasPrice": "0x2540be400" }, "origin": "metamask", - "type": "sentEther", + "type": "simpleSend", "nonceDetails": { "params": { "highestLocallyConfirmed": 12, @@ -138,7 +138,7 @@ "gasPrice": "0x2540be400" }, "origin": "metamask", - "type": "sentEther", + "type": "simpleSend", "nonceDetails": { "params": { "highestLocallyConfirmed": 12, @@ -203,7 +203,7 @@ "gasPrice": "0x2540be400" }, "origin": "metamask", - "type": "sentEther", + "type": "simpleSend", "nonceDetails": { "params": { "highestLocallyConfirmed": 0, @@ -325,7 +325,7 @@ "gasPrice": "0x2540be400" }, "origin": "metamask", - "type": "sentEther", + "type": "simpleSend", "nonceDetails": { "params": { "highestLocallyConfirmed": 0, @@ -390,7 +390,7 @@ "gasPrice": "0x306dc4200" }, "origin": "metamask", - "type": "sentEther", + "type": "simpleSend", "nonceDetails": { "params": { "highestLocallyConfirmed": 0, @@ -514,7 +514,7 @@ "gasPrice": "0x306dc4200" }, "origin": "metamask", - "type": "sentEther", + "type": "simpleSend", "nonceDetails": { "params": { "highestLocallyConfirmed": 0, diff --git a/test/e2e/benchmark.js b/test/e2e/benchmark.js index c5ad8ebb7..fe2dc7926 100644 --- a/test/e2e/benchmark.js +++ b/test/e2e/benchmark.js @@ -7,7 +7,7 @@ const { hideBin } = require('yargs/helpers'); const ttest = require('ttest'); const { retry } = require('../../development/lib/retry'); const { exitWithError } = require('../../development/lib/exit-with-error'); -const { withFixtures } = require('./helpers'); +const { withFixtures, tinyDelayMs } = require('./helpers'); const { PAGES } = require('./webdriver/driver'); const DEFAULT_NUM_SAMPLES = 20; @@ -16,6 +16,7 @@ const ALL_PAGES = Object.values(PAGES); async function measurePage(pageName) { let metrics; await withFixtures({ fixtures: 'imported-account' }, async ({ driver }) => { + await driver.delay(tinyDelayMs); await driver.navigate(); await driver.fill('#password', 'correct horse battery staple'); await driver.press('#password', driver.Key.ENTER); diff --git a/test/e2e/fixtures/custom-token/state.json b/test/e2e/fixtures/custom-token/state.json index bd85436ad..1e2bff3bf 100644 --- a/test/e2e/fixtures/custom-token/state.json +++ b/test/e2e/fixtures/custom-token/state.json @@ -1,150 +1,165 @@ { "data": { + "AlertController": { + "alertEnabledness": { + "unconnectedAccount": true, + "web3ShimUsage": true + }, + "unconnectedAccountAlertShownOrigins": {}, + "web3ShimUsageOrigins": {} + }, "AppStateController": { - "mkrMigrationReminderTimestamp": null + "connectedStatusPopoverHasBeenShown": true, + "defaultHomeActiveTabName": null, + "recoveryPhraseReminderHasBeenShown": true, + "recoveryPhraseReminderLastShown": 1627317428214 }, "CachedBalancesController": { "cachedBalances": { - "4": {} + "0x4": { + "0x5cfe73b6021e818b776b421b1c4db2474086a7e1": "0x0" + } } }, "CurrencyController": { - "conversionDate": 1575697244.188, - "conversionRate": 149.61, + "conversionDate": 1626907353.891, + "conversionRate": 1968.5, "currentCurrency": "usd", - "nativeCurrency": "ETH" + "nativeCurrency": "ETH", + "pendingCurrentCurrency": null, + "pendingNativeCurrency": null, + "usdConversionRate": 1968.5 }, "IncomingTransactionsController": { "incomingTransactions": {}, - "incomingTxLastFetchedBlocksByNetwork": { - "goerli": null, - "kovan": null, - "mainnet": null, - "rinkeby": 5570536 + "incomingTxLastFetchedBlockByChainId": { + "0x1": null, + "0x2a": null, + "0x3": null, + "0x4": 8977934, + "0x5": null } }, "KeyringController": { "vault": "{\"data\":\"s6TpYjlUNsn7ifhEFTkuDGBUM1GyOlPrim7JSjtfIxgTt8/6MiXgiR/CtFfR4dWW2xhq85/NGIBYEeWrZThGdKGarBzeIqBfLFhw9n509jprzJ0zc2Rf+9HVFGLw+xxC4xPxgCS0IIWeAJQ+XtGcHmn0UZXriXm8Ja4kdlow6SWinB7sr/WM3R0+frYs4WgllkwggDf2/Tv6VHygvLnhtzp6hIJFyTjh+l/KnyJTyZW1TkZhDaNDzX3SCOHT\",\"iv\":\"FbeHDAW5afeWNORfNJBR0Q==\",\"salt\":\"TxZ+WbCW6891C9LK/hbMAoUsSEW1E8pyGLVBU6x5KR8=\"}" }, + "MetaMetricsController": { + "metaMetricsId": "0xff3e952b9f5a27ffcab42b0b4abf689e77dcc1f9f441871dc962d622b089fb51", + "participateInMetaMetrics": true + }, "NetworkController": { "network": "1337", + "networkDetails": { + "EIPS": {} + }, + "previousProviderStore": { + "chainId": "0x4", + "ticker": "ETH", + "type": "rinkeby" + }, "provider": { - "nickname": "Localhost 8545", - "rpcUrl": "http://localhost:8545", "chainId": "0x539", + "nickname": "Localhost 8545", + "rpcPrefs": {}, + "rpcUrl": "http://localhost:8545", "ticker": "ETH", "type": "rpc" } }, "NotificationController": { - "notifications": { - "1": { - "isShown": true - }, - "3": { - "isShown": true - }, - "5": { - "isShown": true - }, - "6": { - "isShown": true - } - } + "notifications": {} }, "OnboardingController": { "onboardingTabs": {}, - "seedPhraseBackedUp": false + "seedPhraseBackedUp": true }, - "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 - } - ] + "PermissionsController": { + "domains": {}, + "permissionsDescriptions": {}, + "permissionsRequests": [] }, "PreferencesController": { - "accountTokens": { - "0x5cfe73b6021e818b776b421b1c4db2474086a7e1": { - "0x539": [ - { - "address": "0x86002be4cdd922de1ccb831582bf99284b99ac12", - "symbol": "TST", - "decimals": 4 - } - ], - "rinkeby": [], - "ropsten": [] - } - }, - "assetImages": {}, "completedOnboarding": true, "currentLocale": "en", + "dismissSeedBackUpReminder": true, "featureFlags": { - "showIncomingTransactions": true, - "transactionTime": false + "showIncomingTransactions": true }, - "firstTimeFlowType": "create", + "firstTimeFlowType": "import", "forgottenPassword": false, - "frequentRpcListDetail": [], + "frequentRpcListDetail": [ + { + "chainId": "0x539", + "nickname": "Localhost 8545", + "rpcPrefs": {}, + "rpcUrl": "http://localhost:8545", + "ticker": "ETH" + } + ], "identities": { "0x5cfe73b6021e818b776b421b1c4db2474086a7e1": { "address": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1", + "lastSelected": 1626907346643, "name": "Account 1" } }, + "infuraBlocked": false, + "ipfsGateway": "dweb.link", "knownMethodData": {}, "lostIdentities": {}, - "metaMetricsId": null, - "participateInMetaMetrics": false, "preferences": { + "hideZeroBalanceTokens": false, + "showFiatInTestnets": false, "useNativeCurrencyAsPrimaryCurrency": true }, "selectedAddress": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1", - "suggestedTokens": {}, + "useBlockie": false, + "useLedgerLive": false, + "useNonceField": false, + "usePhishDetect": true, + "useStaticTokenList": false + }, + "TokenListController": { + "tokenList": {}, + "tokensChainsCache": {} + }, + "TokensController": { + "allTokens": { + "0x539": { + "0x5cfe73b6021e818b776b421b1c4db2474086a7e1": [ + { + "address": "0x86002be4cdd922de1ccb831582bf99284b99ac12", + "decimals": 4, + "image": null, + "isERC721": false, + "symbol": "TST" + } + ] + } + }, + "ignoredTokens": [], + "suggestedAssets": [], + "allIgnoredTokens": {}, "tokens": [ { "address": "0x86002be4cdd922de1ccb831582bf99284b99ac12", - "symbol": "TST", - "decimals": 4 + "decimals": 4, + "image": null, + "isERC721": false, + "symbol": "TST" } - ], - "useBlockie": false, - "useNonceField": false, - "usePhishDetect": true + ] + }, + "TransactionController": { + "transactions": {} }, "config": {}, "firstTimeInfo": { - "date": 1575697234195, - "version": "7.7.0" + "date": 1626907328205, + "version": "9.8.1" } }, "meta": { - "version": 40 + "version": 63 } } diff --git a/test/e2e/fixtures/import-ui/state.json b/test/e2e/fixtures/import-ui/state.json index 758a85d66..9f5cb4459 100644 --- a/test/e2e/fixtures/import-ui/state.json +++ b/test/e2e/fixtures/import-ui/state.json @@ -150,7 +150,7 @@ "gasPrice": "0x363fe1da00" }, "origin": "metamask", - "type": "sentEther", + "type": "simpleSend", "history": [], "nonceDetails": { "params": { diff --git a/test/e2e/fixtures/imported-account/state.json b/test/e2e/fixtures/imported-account/state.json index 407bbbd83..edb463d87 100644 --- a/test/e2e/fixtures/imported-account/state.json +++ b/test/e2e/fixtures/imported-account/state.json @@ -88,6 +88,361 @@ } ] }, + "TokenListController": { + "tokenList": { + "0xbbbbca6a901c926f240b89eacb641d8aec7aeafd": { + "address": "0xbbbbca6a901c926f240b89eacb641d8aec7aeafd", + "symbol": "LRC", + "decimals": 18, + "name": "Loopring", + "iconUrl": "https://airswap-token-images.s3.amazonaws.com/LRC.png", + "aggregators": [ + "airswapLight", + "bancor", + "cmc", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 12 + }, + "0x04fa0d235c4abf4bcf4787af4cf447de572ef828": { + "address": "0x04fa0d235c4abf4bcf4787af4cf447de572ef828", + "symbol": "UMA", + "decimals": 18, + "name": "UMA", + "iconUrl": "https://assets.coingecko.com/coins/images/10951/thumb/UMA.png?1586307916", + "aggregators": [ + "bancor", + "cmc", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + }, + "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2": { + "address": "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2", + "symbol": "SUSHI", + "decimals": 18, + "name": "SushiSwap", + "iconUrl": "https://assets.coingecko.com/coins/images/12271/thumb/512x512_Logo_no_chop.png?1606986688", + "aggregators": [ + "bancor", + "cmc", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + }, + "0xd533a949740bb3306d119cc777fa900ba034cd52": { + "address": "0xd533a949740bb3306d119cc777fa900ba034cd52", + "symbol": "CRV", + "decimals": 18, + "name": "Curve DAO Token", + "iconUrl": "https://assets.coingecko.com/coins/images/12124/thumb/Curve.png?1597369484", + "aggregators": [ + "bancor", + "cmc", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + }, + "0xc00e94cb662c3520282e6f5717214004a7f26888": { + "address": "0xc00e94cb662c3520282e6f5717214004a7f26888", + "symbol": "COMP", + "decimals": 18, + "name": "Compound", + "iconUrl": "https://assets.coingecko.com/coins/images/10775/thumb/COMP.png?1592625425", + "aggregators": [ + "bancor", + "cmc", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + }, + "0xba100000625a3754423978a60c9317c58a424e3d": { + "address": "0xba100000625a3754423978a60c9317c58a424e3d", + "symbol": "BAL", + "decimals": 18, + "name": "Balancer", + "iconUrl": "https://assets.coingecko.com/coins/images/11683/thumb/Balancer.png?1592792958", + "aggregators": [ + "bancor", + "cmc", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + }, + "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0": { + "address": "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0", + "symbol": "MATIC", + "decimals": 18, + "name": "Polygon", + "iconUrl": "https://raw.githubusercontent.com/MetaMask/eth-contract-metadata/master/images/matic-network-logo.svg", + "aggregators": [ + "airswapLight", + "bancor", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + }, + "0x0d8775f648430679a709e98d2b0cb6250d2887ef": { + "address": "0x0d8775f648430679a709e98d2b0cb6250d2887ef", + "symbol": "BAT", + "decimals": 18, + "name": "Basic Attention Tok", + "iconUrl": "https://s3.amazonaws.com/airswap-token-images/BAT.png", + "aggregators": [ + "airswapLight", + "bancor", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + } + }, + "tokensChainsCache": { + "1": { + "timestamp": 1628769574961, + "data": [ + { + "address": "0xbbbbca6a901c926f240b89eacb641d8aec7aeafd", + "symbol": "LRC", + "decimals": 18, + "name": "Loopring", + "iconUrl": "https://airswap-token-images.s3.amazonaws.com/LRC.png", + "aggregators": [ + "airswapLight", + "bancor", + "cmc", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 12 + }, + { + "address": "0x04fa0d235c4abf4bcf4787af4cf447de572ef828", + "symbol": "UMA", + "decimals": 18, + "name": "UMA", + "iconUrl": "https://assets.coingecko.com/coins/images/10951/thumb/UMA.png?1586307916", + "aggregators": [ + "bancor", + "cmc", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + }, + { + "address": "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2", + "symbol": "SUSHI", + "decimals": 18, + "name": "SushiSwap", + "iconUrl": "https://assets.coingecko.com/coins/images/12271/thumb/512x512_Logo_no_chop.png?1606986688", + "aggregators": [ + "bancor", + "cmc", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + }, + { + "address": "0xd533a949740bb3306d119cc777fa900ba034cd52", + "symbol": "CRV", + "decimals": 18, + "name": "Curve DAO Token", + "iconUrl": "https://assets.coingecko.com/coins/images/12124/thumb/Curve.png?1597369484", + "aggregators": [ + "bancor", + "cmc", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + }, + { + "address": "0xc00e94cb662c3520282e6f5717214004a7f26888", + "symbol": "COMP", + "decimals": 18, + "name": "Compound", + "iconUrl": "https://assets.coingecko.com/coins/images/10775/thumb/COMP.png?1592625425", + "aggregators": [ + "bancor", + "cmc", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + }, + { + "address": "0xba100000625a3754423978a60c9317c58a424e3d", + "symbol": "BAL", + "decimals": 18, + "name": "Balancer", + "iconUrl": "https://assets.coingecko.com/coins/images/11683/thumb/Balancer.png?1592792958", + "aggregators": [ + "bancor", + "cmc", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + }, + { + "address": "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0", + "symbol": "MATIC", + "decimals": 18, + "name": "Polygon", + "iconUrl": "https://raw.githubusercontent.com/MetaMask/eth-contract-metadata/master/images/matic-network-logo.svg", + "aggregators": [ + "airswapLight", + "bancor", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + }, + { + "address": "0x0d8775f648430679a709e98d2b0cb6250d2887ef", + "symbol": "BAT", + "decimals": 18, + "name": "Basic Attention Tok", + "iconUrl": "https://s3.amazonaws.com/airswap-token-images/BAT.png", + "aggregators": [ + "airswapLight", + "bancor", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + } + ] + }, + "3": { + "timestamp": 1628769543620 + }, + "1337": { + "timestamp": 1628769513476 + } + } + }, "PreferencesController": { "accountTokens": { "0x5cfe73b6021e818b776b421b1c4db2474086a7e1": { @@ -97,6 +452,7 @@ }, "assetImages": {}, "completedOnboarding": true, + "dismissSeedBackUpReminder": true, "currentLocale": "en", "featureFlags": { "showIncomingTransactions": true, @@ -123,7 +479,8 @@ "tokens": [], "useBlockie": false, "useNonceField": false, - "usePhishDetect": true + "usePhishDetect": true, + "useTokenDetection": true }, "config": {}, "firstTimeInfo": { diff --git a/test/e2e/fixtures/navigate-transactions/state.json b/test/e2e/fixtures/navigate-transactions/state.json new file mode 100644 index 000000000..988098e68 --- /dev/null +++ b/test/e2e/fixtures/navigate-transactions/state.json @@ -0,0 +1,710 @@ +{ + "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 + } + ] + }, + "TokenListController": { + "tokenList": { + "0xbbbbca6a901c926f240b89eacb641d8aec7aeafd": { + "address": "0xbbbbca6a901c926f240b89eacb641d8aec7aeafd", + "symbol": "LRC", + "decimals": 18, + "name": "Loopring", + "iconUrl": "https://airswap-token-images.s3.amazonaws.com/LRC.png", + "aggregators": [ + "airswapLight", + "bancor", + "cmc", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 12 + }, + "0x04fa0d235c4abf4bcf4787af4cf447de572ef828": { + "address": "0x04fa0d235c4abf4bcf4787af4cf447de572ef828", + "symbol": "UMA", + "decimals": 18, + "name": "UMA", + "iconUrl": "https://assets.coingecko.com/coins/images/10951/thumb/UMA.png?1586307916", + "aggregators": [ + "bancor", + "cmc", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + }, + "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2": { + "address": "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2", + "symbol": "SUSHI", + "decimals": 18, + "name": "SushiSwap", + "iconUrl": "https://assets.coingecko.com/coins/images/12271/thumb/512x512_Logo_no_chop.png?1606986688", + "aggregators": [ + "bancor", + "cmc", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + }, + "0xd533a949740bb3306d119cc777fa900ba034cd52": { + "address": "0xd533a949740bb3306d119cc777fa900ba034cd52", + "symbol": "CRV", + "decimals": 18, + "name": "Curve DAO Token", + "iconUrl": "https://assets.coingecko.com/coins/images/12124/thumb/Curve.png?1597369484", + "aggregators": [ + "bancor", + "cmc", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + }, + "0xc00e94cb662c3520282e6f5717214004a7f26888": { + "address": "0xc00e94cb662c3520282e6f5717214004a7f26888", + "symbol": "COMP", + "decimals": 18, + "name": "Compound", + "iconUrl": "https://assets.coingecko.com/coins/images/10775/thumb/COMP.png?1592625425", + "aggregators": [ + "bancor", + "cmc", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + }, + "0xba100000625a3754423978a60c9317c58a424e3d": { + "address": "0xba100000625a3754423978a60c9317c58a424e3d", + "symbol": "BAL", + "decimals": 18, + "name": "Balancer", + "iconUrl": "https://assets.coingecko.com/coins/images/11683/thumb/Balancer.png?1592792958", + "aggregators": [ + "bancor", + "cmc", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + }, + "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0": { + "address": "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0", + "symbol": "MATIC", + "decimals": 18, + "name": "Polygon", + "iconUrl": "https://raw.githubusercontent.com/MetaMask/eth-contract-metadata/master/images/matic-network-logo.svg", + "aggregators": [ + "airswapLight", + "bancor", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + }, + "0x0d8775f648430679a709e98d2b0cb6250d2887ef": { + "address": "0x0d8775f648430679a709e98d2b0cb6250d2887ef", + "symbol": "BAT", + "decimals": 18, + "name": "Basic Attention Tok", + "iconUrl": "https://s3.amazonaws.com/airswap-token-images/BAT.png", + "aggregators": [ + "airswapLight", + "bancor", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + } + }, + "tokensChainsCache": { + "1": { + "timestamp": 1628769574961, + "data": [ + { + "address": "0xbbbbca6a901c926f240b89eacb641d8aec7aeafd", + "symbol": "LRC", + "decimals": 18, + "name": "Loopring", + "iconUrl": "https://airswap-token-images.s3.amazonaws.com/LRC.png", + "aggregators": [ + "airswapLight", + "bancor", + "cmc", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 12 + }, + { + "address": "0x04fa0d235c4abf4bcf4787af4cf447de572ef828", + "symbol": "UMA", + "decimals": 18, + "name": "UMA", + "iconUrl": "https://assets.coingecko.com/coins/images/10951/thumb/UMA.png?1586307916", + "aggregators": [ + "bancor", + "cmc", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + }, + { + "address": "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2", + "symbol": "SUSHI", + "decimals": 18, + "name": "SushiSwap", + "iconUrl": "https://assets.coingecko.com/coins/images/12271/thumb/512x512_Logo_no_chop.png?1606986688", + "aggregators": [ + "bancor", + "cmc", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + }, + { + "address": "0xd533a949740bb3306d119cc777fa900ba034cd52", + "symbol": "CRV", + "decimals": 18, + "name": "Curve DAO Token", + "iconUrl": "https://assets.coingecko.com/coins/images/12124/thumb/Curve.png?1597369484", + "aggregators": [ + "bancor", + "cmc", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + }, + { + "address": "0xc00e94cb662c3520282e6f5717214004a7f26888", + "symbol": "COMP", + "decimals": 18, + "name": "Compound", + "iconUrl": "https://assets.coingecko.com/coins/images/10775/thumb/COMP.png?1592625425", + "aggregators": [ + "bancor", + "cmc", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + }, + { + "address": "0xba100000625a3754423978a60c9317c58a424e3d", + "symbol": "BAL", + "decimals": 18, + "name": "Balancer", + "iconUrl": "https://assets.coingecko.com/coins/images/11683/thumb/Balancer.png?1592792958", + "aggregators": [ + "bancor", + "cmc", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + }, + { + "address": "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0", + "symbol": "MATIC", + "decimals": 18, + "name": "Polygon", + "iconUrl": "https://raw.githubusercontent.com/MetaMask/eth-contract-metadata/master/images/matic-network-logo.svg", + "aggregators": [ + "airswapLight", + "bancor", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + }, + { + "address": "0x0d8775f648430679a709e98d2b0cb6250d2887ef", + "symbol": "BAT", + "decimals": 18, + "name": "Basic Attention Tok", + "iconUrl": "https://s3.amazonaws.com/airswap-token-images/BAT.png", + "aggregators": [ + "airswapLight", + "bancor", + "coinGecko", + "kleros", + "oneInch", + "paraswap", + "pmm", + "totle", + "zapper", + "zerion", + "zeroEx" + ], + "occurrences": 11 + } + ] + }, + "3": { + "timestamp": 1628769543620 + }, + "1337": { + "timestamp": 1628769513476 + } + } + }, + "PreferencesController": { + "accountTokens": { + "0x5cfe73b6021e818b776b421b1c4db2474086a7e1": { + "rinkeby": [], + "ropsten": [] + } + }, + "assetImages": {}, + "completedOnboarding": true, + "dismissSeedBackUpReminder": true, + "currentLocale": "en", + "featureFlags": { + "showIncomingTransactions": true, + "transactionTime": false + }, + "firstTimeFlowType": "create", + "forgottenPassword": false, + "frequentRpcListDetail": [], + "identities": { + "0x5cfe73b6021e818b776b421b1c4db2474086a7e1": { + "address": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1", + "name": "Account 1" + } + }, + "knownMethodData": {}, + "lostIdentities": {}, + "metaMetricsId": null, + "participateInMetaMetrics": false, + "preferences": { + "useNativeCurrencyAsPrimaryCurrency": true + }, + "selectedAddress": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1", + "suggestedTokens": {}, + "tokens": [], + "useBlockie": false, + "useNonceField": false, + "usePhishDetect": true, + "useTokenDetection": true + }, + "TransactionController": { + "transactions": { + "7911313280012623": { + "chainId": "0x539", + "dappSuggestedGasFees": { + "gas": "0x5208", + "gasPrice": "0x4a817c800" + }, + "history": [ + { + "chainId": "0x539", + "dappSuggestedGasFees": { + "gas": "0x5208", + "gasPrice": "0x4a817c800" + }, + "id": 7911313280012623, + "loadingDefaults": true, + "metamaskNetworkId": "1337", + "origin": "https://metamask.github.io", + "status": "unapproved", + "time": 1631545991949, + "txParams": { + "from": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1", + "gas": "0x5208", + "gasPrice": "0x4a817c800", + "to": "0x2f318c334780961fb129d2a6c30d0763d9a5c970", + "value": "0x29a2241af62c0000" + }, + "type": "sentEther" + }, + [ + { + "note": "Added new unapproved transaction.", + "op": "replace", + "path": "/loadingDefaults", + "timestamp": 1631545992244, + "value": false + } + ] + ], + "id": 7911313280012623, + "loadingDefaults": false, + "metamaskNetworkId": "1337", + "origin": "https://metamask.github.io", + "status": "unapproved", + "time": 1631545991949, + "txParams": { + "from": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1", + "gas": "0x5208", + "gasPrice": "0x4a817c800", + "to": "0x2f318c334780961fb129d2a6c30d0763d9a5c970", + "value": "0x29a2241af62c0000" + }, + "type": "sentEther" + }, + "7911313280012624": { + "chainId": "0x539", + "dappSuggestedGasFees": { + "gas": "0x5208", + "gasPrice": "0x4a817c800" + }, + "history": [ + { + "chainId": "0x539", + "dappSuggestedGasFees": { + "gas": "0x5208", + "gasPrice": "0x4a817c800" + }, + "id": 7911313280012624, + "loadingDefaults": true, + "metamaskNetworkId": "1337", + "origin": "https://metamask.github.io", + "status": "unapproved", + "time": 1631545994578, + "txParams": { + "from": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1", + "gas": "0x5208", + "gasPrice": "0x4a817c800", + "to": "0x2f318c334780961fb129d2a6c30d0763d9a5c970", + "value": "0x29a2241af62c0000" + }, + "type": "sentEther" + }, + [ + { + "note": "Added new unapproved transaction.", + "op": "replace", + "path": "/loadingDefaults", + "timestamp": 1631545994695, + "value": false + } + ] + ], + "id": 7911313280012624, + "loadingDefaults": false, + "metamaskNetworkId": "1337", + "origin": "https://metamask.github.io", + "status": "unapproved", + "time": 1631545994578, + "txParams": { + "from": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1", + "gas": "0x5208", + "gasPrice": "0x4a817c800", + "to": "0x2f318c334780961fb129d2a6c30d0763d9a5c970", + "value": "0x29a2241af62c0000" + }, + "type": "sentEther" + }, + "7911313280012625": { + "chainId": "0x539", + "dappSuggestedGasFees": { + "gas": "0x5208", + "gasPrice": "0x4a817c800" + }, + "history": [ + { + "chainId": "0x539", + "dappSuggestedGasFees": { + "gas": "0x5208", + "gasPrice": "0x4a817c800" + }, + "id": 7911313280012625, + "loadingDefaults": true, + "metamaskNetworkId": "1337", + "origin": "https://metamask.github.io", + "status": "unapproved", + "time": 1631545996673, + "txParams": { + "from": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1", + "gas": "0x5208", + "gasPrice": "0x4a817c800", + "to": "0x2f318c334780961fb129d2a6c30d0763d9a5c970", + "value": "0x29a2241af62c0000" + }, + "type": "sentEther" + }, + [ + { + "note": "Added new unapproved transaction.", + "op": "replace", + "path": "/loadingDefaults", + "timestamp": 1631545996678, + "value": false + } + ] + ], + "id": 7911313280012625, + "loadingDefaults": false, + "metamaskNetworkId": "1337", + "origin": "https://metamask.github.io", + "status": "unapproved", + "time": 1631545996673, + "txParams": { + "from": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1", + "gas": "0x5208", + "gasPrice": "0x4a817c800", + "to": "0x2f318c334780961fb129d2a6c30d0763d9a5c970", + "value": "0x29a2241af62c0000" + }, + "type": "sentEther" + }, + "7911313280012626": { + "chainId": "0x539", + "dappSuggestedGasFees": { + "gas": "0x5208", + "gasPrice": "0x4a817c800" + }, + "history": [ + { + "chainId": "0x539", + "dappSuggestedGasFees": { + "gas": "0x5208", + "gasPrice": "0x4a817c800" + }, + "id": 7911313280012626, + "loadingDefaults": true, + "metamaskNetworkId": "1337", + "origin": "https://metamask.github.io", + "status": "unapproved", + "time": 1631545998675, + "txParams": { + "from": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1", + "gas": "0x5208", + "gasPrice": "0x4a817c800", + "to": "0x2f318c334780961fb129d2a6c30d0763d9a5c970", + "value": "0x29a2241af62c0000" + }, + "type": "sentEther" + }, + [ + { + "note": "Added new unapproved transaction.", + "op": "replace", + "path": "/loadingDefaults", + "timestamp": 1631545998677, + "value": false + } + ] + ], + "id": 7911313280012626, + "loadingDefaults": false, + "metamaskNetworkId": "1337", + "origin": "https://metamask.github.io", + "status": "unapproved", + "time": 1631545998675, + "txParams": { + "from": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1", + "gas": "0x5208", + "gasPrice": "0x4a817c800", + "to": "0x2f318c334780961fb129d2a6c30d0763d9a5c970", + "value": "0x29a2241af62c0000" + }, + "type": "sentEther" + } + } + }, + "config": {}, + "firstTimeInfo": { + "date": 1575697234195, + "version": "7.7.0" + } + }, + "meta": { + "version": 40 + } +} diff --git a/test/e2e/fixtures/send-edit/state.json b/test/e2e/fixtures/send-edit/state.json index 3070c06a6..79efbaed2 100644 --- a/test/e2e/fixtures/send-edit/state.json +++ b/test/e2e/fixtures/send-edit/state.json @@ -163,7 +163,7 @@ "to": "0x2f318C334780961FB129D2a6c30D0763d9a5C970", "value": "0xde0b6b3a7640000" }, - "type": "sentEther" + "type": "simpleSend" }, [ { @@ -188,7 +188,7 @@ "to": "0x2f318C334780961FB129D2a6c30D0763d9a5C970", "value": "0xde0b6b3a7640000" }, - "type": "sentEther" + "type": "simpleSend" } } }, diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index b799f1d53..dca25366a 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -82,7 +82,10 @@ async function withFixtures(options, testSuite) { }); await segmentServer.start(9090); } - if (process.env.SELENIUM_BROWSER === 'chrome') { + if ( + process.env.SELENIUM_BROWSER === 'chrome' && + process.env.CI === 'true' + ) { await ensureXServerIsRunning(); } const { driver } = await buildWebDriver(driverOptions); diff --git a/test/e2e/metamask-ui.spec.js b/test/e2e/metamask-ui.spec.js index 73af211d0..74ea431f2 100644 --- a/test/e2e/metamask-ui.spec.js +++ b/test/e2e/metamask-ui.spec.js @@ -40,7 +40,10 @@ describe('MetaMask', function () { dappServer.on('listening', resolve); dappServer.on('error', reject); }); - if (process.env.SELENIUM_BROWSER === 'chrome') { + if ( + process.env.SELENIUM_BROWSER === 'chrome' && + process.env.CI === 'true' + ) { await ensureXServerIsRunning(); } const result = await buildWebDriver(); @@ -191,25 +194,6 @@ describe('MetaMask', function () { }); }); - describe('Show account information', function () { - it('shows the QR code for the account', async function () { - await driver.clickElement('[data-testid="account-options-menu-button"]'); - await driver.clickElement( - '[data-testid="account-options-menu__account-details"]', - ); - await driver.findVisibleElement('.qr-code__wrapper'); - await driver.delay(regularDelayMs); - - // wait for permission modal to be visible. - const permissionModal = await driver.findVisibleElement('span .modal'); - await driver.clickElement('.account-modal__close'); - - // wait for permission modal to be removed from DOM. - await permissionModal.waitForElementState('hidden'); - await driver.delay(regularDelayMs); - }); - }); - describe('Import Secret Recovery Phrase', function () { it('logs out of the vault', async function () { await driver.clickElement('.account-menu__icon'); @@ -257,7 +241,7 @@ describe('MetaMask', function () { }); }); - describe('Navigate transactions', function () { + describe('Add a custom token from a dapp', function () { let windowHandles; let extension; let popup; @@ -294,201 +278,11 @@ describe('MetaMask', function () { await driver.delay(regularDelayMs); }); - it('adds multiple transactions', async function () { - const send3eth = await driver.findClickableElement({ - text: 'Send', - tag: 'button', - }); - await send3eth.click(); - await driver.delay(largeDelayMs); - - const contractDeployment = await driver.findClickableElement({ - text: 'Deploy Contract', - tag: 'button', - }); - await contractDeployment.click(); - await driver.delay(largeDelayMs); - - await send3eth.click(); - await driver.delay(largeDelayMs); - await contractDeployment.click(); - await driver.delay(largeDelayMs); - - await driver.switchToWindow(extension); - await driver.delay(regularDelayMs); - - await driver.clickElement('[data-testid="home__activity-tab"]'); - await driver.clickElement('.transaction-list-item'); - await driver.delay(largeDelayMs); - }); - - it('navigates the transactions', async function () { - await driver.clickElement('[data-testid="next-page"]'); - let navigationElement = await driver.findElement( - '.confirm-page-container-navigation', - ); - let navigationText = await navigationElement.getText(); - assert.equal( - navigationText.includes('2'), - true, - 'changed transaction right', - ); - - await driver.clickElement('[data-testid="next-page"]'); - navigationElement = await driver.findElement( - '.confirm-page-container-navigation', - ); - navigationText = await navigationElement.getText(); - assert.equal( - navigationText.includes('3'), - true, - 'changed transaction right', - ); - - await driver.clickElement('[data-testid="next-page"]'); - navigationElement = await driver.findElement( - '.confirm-page-container-navigation', - ); - navigationText = await navigationElement.getText(); - assert.equal( - navigationText.includes('4'), - true, - 'changed transaction right', - ); - - await driver.clickElement('[data-testid="first-page"]'); - navigationElement = await driver.findElement( - '.confirm-page-container-navigation', - ); - navigationText = await navigationElement.getText(); - assert.equal( - navigationText.includes('1'), - true, - 'navigate to first transaction', - ); - - await driver.clickElement('[data-testid="last-page"]'); - navigationElement = await driver.findElement( - '.confirm-page-container-navigation', - ); - navigationText = await navigationElement.getText(); - assert.equal( - navigationText.split('4').length, - 3, - 'navigate to last transaction', - ); - - await driver.clickElement('[data-testid="previous-page"]'); - navigationElement = await driver.findElement( - '.confirm-page-container-navigation', - ); - navigationText = await navigationElement.getText(); - assert.equal( - navigationText.includes('3'), - true, - 'changed transaction left', - ); - - await driver.clickElement('[data-testid="previous-page"]'); - navigationElement = await driver.findElement( - '.confirm-page-container-navigation', - ); - navigationText = await navigationElement.getText(); - assert.equal( - navigationText.includes('2'), - true, - 'changed transaction left', - ); - }); - - it('adds a transaction while confirm screen is in focus', async function () { - let navigationElement = await driver.findElement( - '.confirm-page-container-navigation', - ); - let navigationText = await navigationElement.getText(); - assert.equal( - navigationText.includes('2'), - true, - 'second transaction in focus', - ); - - await driver.switchToWindow(dapp); - await driver.delay(regularDelayMs); - - await driver.clickElement({ text: 'Send', tag: 'button' }); - await driver.delay(regularDelayMs); - - await driver.switchToWindow(extension); - await driver.delay(regularDelayMs); - - navigationElement = await driver.findElement( - '.confirm-page-container-navigation', - ); - navigationText = await navigationElement.getText(); - assert.equal( - navigationText.includes('2'), - true, - 'correct (same) transaction in focus', - ); - }); - - it('rejects a transaction', async function () { - await driver.delay(tinyDelayMs); - await driver.clickElement({ text: 'Reject', tag: 'button' }); - await driver.delay(largeDelayMs * 2); - - const navigationElement = await driver.findElement( - '.confirm-page-container-navigation', - ); - await driver.delay(tinyDelayMs); - const navigationText = await navigationElement.getText(); - assert.equal(navigationText.includes('4'), true, 'transaction rejected'); - }); - - it('confirms a transaction', async function () { - await driver.delay(tinyDelayMs / 2); - await driver.clickElement({ text: 'Confirm', tag: 'button' }); - await driver.delay(regularDelayMs); - - const navigationElement = await driver.findElement( - '.confirm-page-container-navigation', - ); - await driver.delay(tinyDelayMs / 2); - const navigationText = await navigationElement.getText(); - await driver.delay(tinyDelayMs / 2); - assert.equal(navigationText.includes('3'), true, 'transaction confirmed'); - }); - - it('rejects the rest of the transactions', async function () { - await driver.clickElement({ text: 'Reject 3', tag: 'a' }); - await driver.delay(regularDelayMs); - - await driver.clickElement({ text: 'Reject All', tag: 'button' }); - await driver.delay(largeDelayMs * 2); - - await driver.wait(async () => { - const confirmedTxes = await driver.findElements( - '.transaction-list__completed-transactions .transaction-list-item', - ); - return confirmedTxes.length === 1; - }, 10000); - }); - }); - - describe('Add a custom token from a dapp', function () { it('creates a new token', async function () { - let windowHandles = await driver.getAllWindowHandles(); - const extension = windowHandles[0]; - const dapp = windowHandles[1]; - await driver.delay(regularDelayMs * 2); - - await driver.switchToWindow(dapp); - await driver.delay(regularDelayMs * 2); - await driver.clickElement({ text: 'Create Token', tag: 'button' }); windowHandles = await driver.waitUntilXWindowHandles(3); - const popup = windowHandles[2]; + popup = windowHandles[2]; await driver.switchToWindow(popup); await driver.delay(regularDelayMs); await driver.clickElement({ text: 'Edit', tag: 'button' }, 10000); @@ -520,9 +314,9 @@ describe('MetaMask', function () { await driver.delay(largeDelayMs); }); - it('clicks on the Add Token button', async function () { + it('clicks on the import tokens button', async function () { await driver.clickElement(`[data-testid="home__asset-tab"]`); - await driver.clickElement({ text: 'Add Token', tag: 'button' }); + await driver.clickElement({ text: 'import tokens', tag: 'a' }); await driver.delay(regularDelayMs); }); @@ -536,10 +330,10 @@ describe('MetaMask', function () { await driver.fill('#custom-address', tokenAddress); await driver.delay(regularDelayMs); - await driver.clickElement({ text: 'Next', tag: 'button' }); + await driver.clickElement({ text: 'Add Custom Token', tag: 'button' }); await driver.delay(regularDelayMs); - await driver.clickElement({ text: 'Add Tokens', tag: 'button' }); + await driver.clickElement({ text: 'Import Tokens', tag: 'button' }); await driver.delay(regularDelayMs); }); diff --git a/test/e2e/metrics.spec.js b/test/e2e/metrics.spec.js index fb54eea5d..498a6ccd0 100644 --- a/test/e2e/metrics.spec.js +++ b/test/e2e/metrics.spec.js @@ -1,6 +1,6 @@ const { strict: assert } = require('assert'); const waitUntilCalled = require('../lib/wait-until-called'); -const { withFixtures } = require('./helpers'); +const { withFixtures, tinyDelayMs } = require('./helpers'); /** * WARNING: These tests must be run using a build created with `yarn build:test:metrics`, so that it has @@ -30,6 +30,7 @@ describe('Segment metrics', function () { const threeSegmentEventsReceived = waitUntilCalled(segmentStub, null, { callCount: 3, }); + await driver.delay(tinyDelayMs); await driver.navigate(); await driver.fill('#password', 'correct horse battery staple'); diff --git a/test/e2e/tests/account-details.spec.js b/test/e2e/tests/account-details.spec.js new file mode 100644 index 000000000..82f28ad15 --- /dev/null +++ b/test/e2e/tests/account-details.spec.js @@ -0,0 +1,38 @@ +const { strict: assert } = require('assert'); +const { withFixtures } = require('../helpers'); + +describe('Show account details', function () { + const ganacheOptions = { + accounts: [ + { + secretKey: + '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', + balance: 25000000000000000000, + }, + ], + }; + it('should show the QR code for the account', 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( + '[data-testid="account-options-menu-button"]', + ); + await driver.clickElement( + '[data-testid="account-options-menu__account-details"]', + ); + + const qrCode = await driver.findElement('.qr-code__wrapper'); + assert.equal(await qrCode.isDisplayed(), true); + }, + ); + }); +}); diff --git a/test/e2e/tests/add-hide-token.spec.js b/test/e2e/tests/add-hide-token.spec.js index 8b98378cd..fa5e85009 100644 --- a/test/e2e/tests/add-hide-token.spec.js +++ b/test/e2e/tests/add-hide-token.spec.js @@ -27,6 +27,7 @@ describe('Hide token', function () { css: '.asset-list-item__token-button', text: '0 TST', }); + await driver.clickElement('.popover-header__button'); let assets = await driver.findElements('.asset-list-item'); assert.equal(assets.length, 2); @@ -78,11 +79,11 @@ describe('Add existing token using search', function () { 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.clickElement({ text: 'import tokens', tag: 'a' }); 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.clickElement({ text: 'Import Tokens', tag: 'button' }); await driver.waitForSelector({ css: '.token-overview__primary-balance', diff --git a/test/e2e/tests/contract-interactions.spec.js b/test/e2e/tests/contract-interactions.spec.js index a88a12032..e94c18f0a 100644 --- a/test/e2e/tests/contract-interactions.spec.js +++ b/test/e2e/tests/contract-interactions.spec.js @@ -51,9 +51,12 @@ describe('Deploy contract and call contract methods', function () { await driver.clickElement('#deployButton'); // displays the contract creation data - await driver.switchToWindow(extension); - await driver.clickElement('[data-testid="home__activity-tab"]'); - await driver.clickElement({ text: 'Contract Deployment', tag: 'h2' }); + await driver.waitUntilXWindowHandles(3); + windowHandles = await driver.getAllWindowHandles(); + await driver.switchToWindowWithTitle( + 'MetaMask Notification', + windowHandles, + ); await driver.clickElement({ text: 'Data', tag: 'button' }); await driver.findElement({ text: '127.0.0.1', tag: 'div' }); const confirmDataDiv = await driver.findElement( @@ -68,6 +71,9 @@ describe('Deploy contract and call contract methods', function () { // confirms a deploy contract transaction await driver.clickElement({ text: 'Details', tag: 'button' }); await driver.clickElement({ text: 'Confirm', tag: 'button' }); + await driver.waitUntilXWindowHandles(2); + await driver.switchToWindow(extension); + await driver.clickElement('[data-testid="home__activity-tab"]'); await driver.waitForSelector( '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(1)', { timeout: 10000 }, @@ -86,6 +92,7 @@ describe('Deploy contract and call contract methods', function () { windowHandles, ); await driver.clickElement({ text: 'Confirm', tag: 'button' }); + await driver.waitUntilXWindowHandles(2); await driver.switchToWindow(extension); await driver.waitForSelector( '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(2)', @@ -109,6 +116,7 @@ describe('Deploy contract and call contract methods', function () { windowHandles, ); await driver.clickElement({ text: 'Confirm', tag: 'button' }); + await driver.waitUntilXWindowHandles(2); await driver.switchToWindow(extension); await driver.waitForSelector( '.transaction-list__completed-transactions .transaction-list-item:nth-of-type(3)', diff --git a/test/e2e/tests/custom-rpc-history.spec.js b/test/e2e/tests/custom-rpc-history.spec.js index 792bfa980..08a2bdd31 100644 --- a/test/e2e/tests/custom-rpc-history.spec.js +++ b/test/e2e/tests/custom-rpc-history.spec.js @@ -49,7 +49,7 @@ describe('Stores custom RPC history', function () { await chainIdInput.sendKeys(chainId.toString()); await driver.clickElement('.network-form__footer .btn-secondary'); - await driver.findElement({ text: networkName, tag: 'div' }); + await driver.findElement({ text: networkName, tag: 'span' }); }, ); }); diff --git a/test/e2e/tests/navigate-transactions.spec.js b/test/e2e/tests/navigate-transactions.spec.js new file mode 100644 index 000000000..a5a6a4166 --- /dev/null +++ b/test/e2e/tests/navigate-transactions.spec.js @@ -0,0 +1,251 @@ +const { strict: assert } = require('assert'); +const { withFixtures } = require('../helpers'); + +describe('Navigate transactions', function () { + const ganacheOptions = { + accounts: [ + { + secretKey: + '0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', + balance: 25000000000000000000, + }, + ], + }; + it('should navigate the unapproved transactions', async function () { + await withFixtures( + { + dapp: true, + fixtures: 'navigate-transactions', + 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); + + // navigate transactions + await driver.clickElement('[data-testid="next-page"]'); + let navigationElement = await driver.findElement( + '.confirm-page-container-navigation', + ); + let navigationText = await navigationElement.getText(); + assert.equal( + navigationText.includes('2 of 4'), + true, + 'changed transaction right', + ); + await driver.clickElement('[data-testid="next-page"]'); + navigationElement = await driver.findElement( + '.confirm-page-container-navigation', + ); + navigationText = await navigationElement.getText(); + assert.equal( + navigationText.includes('3 of 4'), + true, + 'changed transaction right', + ); + await driver.clickElement('[data-testid="next-page"]'); + navigationElement = await driver.findElement( + '.confirm-page-container-navigation', + ); + navigationText = await navigationElement.getText(); + assert.equal( + navigationText.includes('4 of 4'), + true, + 'changed transaction right', + ); + await driver.clickElement('[data-testid="first-page"]'); + navigationElement = await driver.findElement( + '.confirm-page-container-navigation', + ); + navigationText = await navigationElement.getText(); + assert.equal( + navigationText.includes('1 of 4'), + true, + 'navigate to first transaction', + ); + await driver.clickElement('[data-testid="last-page"]'); + navigationElement = await driver.findElement( + '.confirm-page-container-navigation', + ); + navigationText = await navigationElement.getText(); + assert.equal( + navigationText.includes('4 of 4'), + true, + 'navigate to last transaction', + ); + await driver.clickElement('[data-testid="previous-page"]'); + navigationElement = await driver.findElement( + '.confirm-page-container-navigation', + ); + navigationText = await navigationElement.getText(); + assert.equal( + navigationText.includes('3 of 4'), + true, + 'changed transaction left', + ); + await driver.clickElement('[data-testid="previous-page"]'); + navigationElement = await driver.findElement( + '.confirm-page-container-navigation', + ); + navigationText = await navigationElement.getText(); + assert.equal( + navigationText.includes('2 of 4'), + true, + 'changed transaction left', + ); + }, + ); + }); + + it('should add a transaction while the confirm page is in focus', async function () { + await withFixtures( + { + dapp: true, + fixtures: 'navigate-transactions', + 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('[data-testid="next-page"]'); + let navigationElement = await driver.findElement( + '.confirm-page-container-navigation', + ); + let navigationText = await navigationElement.getText(); + assert.equal( + navigationText.includes('2 of 4'), + true, + 'second transaction in focus', + ); + + // connects the dapp + await driver.openNewPage('http://127.0.0.1:8080/'); + await driver.clickElement({ text: 'Connect', tag: 'button' }); + await driver.waitUntilXWindowHandles(3); + const windowHandles = await driver.getAllWindowHandles(); + const extension = windowHandles[0]; + const dapp = await driver.switchToWindowWithTitle( + 'E2E Test Dapp', + windowHandles, + ); + const popup = windowHandles.find( + (handle) => handle !== extension && handle !== dapp, + ); + await driver.switchToWindow(popup); + await driver.clickElement({ text: 'Next', tag: 'button' }); + await driver.clickElement({ text: 'Connect', tag: 'button' }); + + // add transaction + await driver.switchToWindow(dapp); + await driver.clickElement({ text: 'Send', tag: 'button' }); + await driver.switchToWindow(extension); + navigationElement = await driver.waitForSelector( + { + css: '.confirm-page-container-navigation', + text: '2 of 5', + }, + { timeout: 10000 }, + ); + navigationText = await navigationElement.getText(); + assert.equal( + navigationText.includes('2 of 5'), + true, + 'correct (same) transaction in focus', + ); + }, + ); + }); + + it('should reject and remove an unapproved transaction', async function () { + await withFixtures( + { + dapp: true, + fixtures: 'navigate-transactions', + 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); + + // reject transaction + await driver.clickElement({ text: 'Reject', tag: 'button' }); + const navigationElement = await driver.waitForSelector( + { + css: '.confirm-page-container-navigation', + text: '1 of 3', + }, + { timeout: 10000 }, + ); + const navigationText = await navigationElement.getText(); + assert.equal( + navigationText.includes('1 of 3'), + true, + 'transaction rejected', + ); + }, + ); + }); + + it('should confirm and remove an unapproved transaction', async function () { + await withFixtures( + { + dapp: true, + fixtures: 'navigate-transactions', + 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); + + // confirm transaction + await driver.clickElement({ text: 'Confirm', tag: 'button' }); + const navigationElement = await driver.waitForSelector( + { + css: '.confirm-page-container-navigation', + text: '1 of 3', + }, + { timeout: 10000 }, + ); + const navigationText = await navigationElement.getText(); + assert.equal( + navigationText.includes('1 of 3'), + true, + 'transaction confirmed', + ); + }, + ); + }); + + it('should reject and remove all unapproved transactions', async function () { + await withFixtures( + { + dapp: true, + fixtures: 'navigate-transactions', + 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); + + // reject transactions + await driver.clickElement({ text: 'Reject 4', tag: 'a' }); + await driver.clickElement({ text: 'Reject All', tag: 'button' }); + const balance = await driver.findElement( + '[data-testid="eth-overview__primary-currency"]', + ); + assert.ok(/^25\sETH$/u.test(await balance.getText())); + }, + ); + }); +}); diff --git a/test/e2e/tests/permissions.spec.js b/test/e2e/tests/permissions.spec.js index afc860731..7b7b12bb3 100644 --- a/test/e2e/tests/permissions.spec.js +++ b/test/e2e/tests/permissions.spec.js @@ -61,7 +61,10 @@ describe('Permissions', function () { text: 'Connected sites', tag: 'h2', }); - + await driver.waitForSelector({ + css: '.connected-sites-list__domain-name', + text: '127.0.0.1:8080', + }); const domains = await driver.findClickableElements( '.connected-sites-list__domain-name', ); @@ -75,9 +78,10 @@ describe('Permissions', function () { tag: 'button', }); - const getAccountsResult = await driver.findElement( - '#getAccountsResult', - ); + const getAccountsResult = await driver.waitForSelector({ + css: '#getAccountsResult', + text: publicAddress, + }); assert.equal( (await getAccountsResult.getText()).toLowerCase(), publicAddress.toLowerCase(), diff --git a/test/e2e/tests/provider-events.spec.js b/test/e2e/tests/provider-events.spec.js index 0949dbf46..effe3e717 100644 --- a/test/e2e/tests/provider-events.spec.js +++ b/test/e2e/tests/provider-events.spec.js @@ -1,5 +1,5 @@ const { strict: assert } = require('assert'); -const { withFixtures, regularDelayMs } = require('../helpers'); +const { withFixtures } = require('../helpers'); describe('MetaMask', function () { it('provider should inform dapp when switching networks', async function () { @@ -25,9 +25,14 @@ describe('MetaMask', function () { await driver.press('#password', driver.Key.ENTER); await driver.openNewPage('http://127.0.0.1:8080/'); - const networkDiv = await driver.findElement('#network'); - const chainIdDiv = await driver.findElement('#chainId'); - await driver.delay(regularDelayMs); + const networkDiv = await driver.waitForSelector({ + css: '#network', + text: '1337', + }); + const chainIdDiv = await driver.waitForSelector({ + css: '#chainId', + text: '0x539', + }); assert.equal(await networkDiv.getText(), '1337'); assert.equal(await chainIdDiv.getText(), '0x539'); @@ -36,11 +41,16 @@ describe('MetaMask', function () { await driver.clickElement('.network-display'); await driver.clickElement({ text: 'Ropsten', tag: 'span' }); - await driver.delay(regularDelayMs); await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); - const switchedNetworkDiv = await driver.findElement('#network'); - const switchedChainIdDiv = await driver.findElement('#chainId'); + const switchedNetworkDiv = await driver.waitForSelector({ + css: '#network', + text: '3', + }); + const switchedChainIdDiv = await driver.waitForSelector({ + css: '#chainId', + text: '0x3', + }); const accountsDiv = await driver.findElement('#accounts'); assert.equal(await switchedNetworkDiv.getText(), '3'); diff --git a/test/e2e/tests/signature-request.spec.js b/test/e2e/tests/signature-request.spec.js index 3c59f7c69..bf2a547e2 100644 --- a/test/e2e/tests/signature-request.spec.js +++ b/test/e2e/tests/signature-request.spec.js @@ -31,7 +31,7 @@ describe('Signature Request', function () { await driver.clickElement('#signTypedDataV4', 10000); await driver.waitUntilXWindowHandles(3); - const windowHandles = await driver.getAllWindowHandles(); + let windowHandles = await driver.getAllWindowHandles(); await driver.switchToWindowWithTitle( 'MetaMask Notification', windowHandles, @@ -60,6 +60,8 @@ describe('Signature Request', function () { // Approve signing typed data await driver.clickElement({ text: 'Sign', tag: 'button' }, 10000); + await driver.waitUntilXWindowHandles(2); + windowHandles = await driver.getAllWindowHandles(); // switch to the Dapp and verify the signed addressed await driver.switchToWindowWithTitle('E2E Test Dapp', windowHandles); diff --git a/test/e2e/webdriver/driver.js b/test/e2e/webdriver/driver.js index 0188c8257..d7a742399 100644 --- a/test/e2e/webdriver/driver.js +++ b/test/e2e/webdriver/driver.js @@ -275,16 +275,25 @@ class Driver { throw new Error('waitUntilXWindowHandles timed out polling window handles'); } - async switchToWindowWithTitle(title, windowHandles) { + async switchToWindowWithTitle( + title, + windowHandles, + delayStep = 1000, + timeout = 5000, + ) { + let timeElapsed = 0; // eslint-disable-next-line no-param-reassign windowHandles = windowHandles || (await this.driver.getAllWindowHandles()); - - for (const handle of windowHandles) { - await this.driver.switchTo().window(handle); - const handleTitle = await this.driver.getTitle(); - if (handleTitle === title) { - return handle; + while (timeElapsed <= timeout) { + for (const handle of windowHandles) { + await this.driver.switchTo().window(handle); + const handleTitle = await this.driver.getTitle(); + if (handleTitle === title) { + return handle; + } } + await this.delay(delayStep); + timeElapsed += delayStep; } throw new Error(`No window with title: ${title}`); diff --git a/test/e2e/webdriver/index.js b/test/e2e/webdriver/index.js index 075718631..eb552f34d 100644 --- a/test/e2e/webdriver/index.js +++ b/test/e2e/webdriver/index.js @@ -52,6 +52,12 @@ async function setupFetchMocking(driver) { if (url.match(/featureFlags$/u)) { return { json: async () => clone(mockResponses.swaps.featureFlags) }; } + } else if ( + url.match(/^https:\/\/(token-api\.airswap-prod\.codefi\.network)/u) + ) { + if (url.match(/tokens\/1337$/u)) { + return { json: async () => clone(mockResponses.tokenList) }; + } } return window.origFetch(...args); }; diff --git a/test/helpers/setup-helper.js b/test/helpers/setup-helper.js index 749577c40..6e371a790 100644 --- a/test/helpers/setup-helper.js +++ b/test/helpers/setup-helper.js @@ -56,17 +56,12 @@ const popoverContent = window.document.createElement('div'); popoverContent.setAttribute('id', 'popover-content'); window.document.body.appendChild(popoverContent); -// delete AbortController added by jsdom so it can be polyfilled correctly below -delete window.AbortController; - // fetch const fetch = require('node-fetch'); const { Headers, Request, Response } = fetch; Object.assign(window, { fetch, Headers, Request, Response }); -require('abortcontroller-polyfill/dist/polyfill-patch-fetch'); - // localStorage window.localStorage = { removeItem: () => null, diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js index f57ae13ae..a23d7ad35 100644 --- a/test/jest/mock-store.js +++ b/test/jest/mock-store.js @@ -212,6 +212,7 @@ export const createSwapsMockStore = () => { approveTxId: null, quotesLastFetched: 1519211809934, swapsQuoteRefreshTime: 60000, + swapsQuotePrefetchingRefreshTime: 60000, customMaxGas: '', customGasPrice: null, selectedAggId: 'TEST_AGG_2', @@ -222,6 +223,76 @@ export const createSwapsMockStore = () => { swapsFeatureIsLive: false, useNewSwapsApi: false, }, + useTokenDetection: true, + tokenList: { + '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984': { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + symbol: 'UNI', + decimals: 18, + name: 'Uniswap', + iconUrl: + 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png', + aggregators: [ + 'airswapLight', + 'bancor', + 'cmc', + 'coinGecko', + 'kleros', + 'oneInch', + 'paraswap', + 'pmm', + 'totle', + 'zapper', + 'zerion', + 'zeroEx', + ], + occurrences: 12, + }, + '0x514910771af9ca656af840dff83e8264ecf986ca': { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + symbol: 'LINK', + decimals: 18, + name: 'Chainlink', + iconUrl: 'https://s3.amazonaws.com/airswap-token-images/LINK.png', + aggregators: [ + 'airswapLight', + 'bancor', + 'cmc', + 'coinGecko', + 'kleros', + 'oneInch', + 'paraswap', + 'pmm', + 'totle', + 'zapper', + 'zerion', + 'zeroEx', + ], + occurrences: 12, + }, + '0x6b3595068778dd592e39a122f4f5a5cf09c90fe2': { + address: '0x6b3595068778dd592e39a122f4f5a5cf09c90fe2', + symbol: 'SUSHI', + decimals: 18, + name: 'SushiSwap', + iconUrl: + 'https://assets.coingecko.com/coins/images/12271/thumb/512x512_Logo_no_chop.png?1606986688', + aggregators: [ + 'bancor', + 'cmc', + 'coinGecko', + 'kleros', + 'oneInch', + 'paraswap', + 'pmm', + 'totle', + 'zapper', + 'zerion', + 'zeroEx', + ], + occurrences: 11, + }, + }, }, appState: { modal: { diff --git a/test/run-jest.sh b/test/run-jest.sh new file mode 100755 index 000000000..40b208732 --- /dev/null +++ b/test/run-jest.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -x +set -e +set -u +set -o pipefail + +concurrently \ + "jest --config=./jest.config.js $*" \ + "jest --config=./development/jest.config.js $*" diff --git a/test/stub/tx-meta-stub.js b/test/stub/tx-meta-stub.js index bb1fc4f99..c33ab758c 100644 --- a/test/stub/tx-meta-stub.js +++ b/test/stub/tx-meta-stub.js @@ -14,7 +14,7 @@ export const txMetaStub = { metamaskNetworkId: '4', status: TRANSACTION_STATUSES.UNAPPROVED, time: 1572395156620, - type: TRANSACTION_TYPES.SENT_ETHER, + type: TRANSACTION_TYPES.SIMPLE_SEND, txParams: { from: '0xf231d46dd78806e1dd93442cf33c7671f8538748', gas: GAS_LIMITS.SIMPLE, @@ -195,7 +195,7 @@ export const txMetaStub = { status: TRANSACTION_STATUSES.SUBMITTED, submittedTime: 1572395158570, time: 1572395156620, - type: TRANSACTION_TYPES.SENT_ETHER, + type: TRANSACTION_TYPES.SIMPLE_SEND, txParams: { from: '0xf231d46dd78806e1dd93442cf33c7671f8538748', gas: GAS_LIMITS.SIMPLE, diff --git a/test/unit-global/frozenPromise.test.js b/test/unit-global/frozenPromise.test.js deleted file mode 100644 index b41362266..000000000 --- a/test/unit-global/frozenPromise.test.js +++ /dev/null @@ -1,53 +0,0 @@ -// Should occur before anything else -import './globalPatch'; -import 'ses/lockdown'; -import '../../app/scripts/lockdown-run'; -import { strict as assert } from 'assert'; /* eslint-disable-line import/first,import/order */ - -describe('Promise global is immutable', function () { - it('throws when reassinging promise (syntax 1)', function () { - try { - // eslint-disable-next-line no-global-assign,no-native-reassign - Promise = {}; - assert.fail('did not throw error'); - } catch (err) { - assert.ok(err, 'did throw error'); - } - }); - - it('throws when reassinging promise (syntax 2)', function () { - try { - global.Promise = {}; - assert.fail('did not throw error'); - } catch (err) { - assert.ok(err, 'did throw error'); - } - }); - - it('throws when mutating existing Promise property', function () { - try { - Promise.all = () => undefined; - assert.fail('did not throw error'); - } catch (err) { - assert.ok(err, 'did throw error'); - } - }); - - it('throws when adding new Promise property', function () { - try { - Promise.foo = 'bar'; - assert.fail('did not throw error'); - } catch (err) { - assert.ok(err, 'did throw error'); - } - }); - - it('throws when deleting Promise from global', function () { - try { - delete global.Promise; - assert.fail('did not throw error'); - } catch (err) { - assert.ok(err, 'did throw error'); - } - }); -}); diff --git a/test/unit-global/globalPatch.js b/test/unit-global/globalPatch.js deleted file mode 100644 index da239953c..000000000 --- a/test/unit-global/globalPatch.js +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line import/unambiguous,node/no-unsupported-features/es-builtins -global.globalThis = global; diff --git a/test/unit-global/protect-intrinsics.test.js b/test/unit-global/protect-intrinsics.test.js new file mode 100644 index 000000000..5075080f9 --- /dev/null +++ b/test/unit-global/protect-intrinsics.test.js @@ -0,0 +1,71 @@ +import 'ses/lockdown'; +import '../../app/scripts/lockdown-run'; +import { strict as assert } from 'assert'; + +// These are Agoric inventions, and we don't care about them. +const ignoreList = new Set([ + 'Compartment', + 'HandledPromise', + 'StaticModuleRecord', +]); + +describe('non-modifiable intrinsics', function () { + const namedIntrinsics = Reflect.ownKeys(new Compartment().globalThis); + + const globalProperties = new Set( + [ + // Added to global scope by ses/dist/lockdown.cjs. + ...namedIntrinsics, + + // TODO: Also include the named platform globals + // This grabs every enumerable property on globalThis. + // ...Object.keys(globalThis), + ].filter((propertyName) => !ignoreList.has(propertyName)), + ); + + globalProperties.forEach((propertyName) => { + it(`intrinsic globalThis["${propertyName}"]`, function () { + const descriptor = Reflect.getOwnPropertyDescriptor( + globalThis, + propertyName, + ); + + assert.ok( + descriptor, + `globalThis["${propertyName}"] should have a descriptor`, + ); + + // As long as Object.isFrozen is the true Object.isFrozen, the object + // it is called with cannot lie about being frozen. + const value = globalThis[propertyName]; + if (value !== globalThis) { + assert.equal( + Object.isFrozen(value), + true, + `value of universal property globalThis["${propertyName}"] should be frozen`, + ); + } + + // The writability of properties with accessors cannot be modified. + if ('set' in descriptor || 'get' in descriptor) { + assert.equal( + descriptor.configurable, + false, + `globalThis["${propertyName}"] should be non-configurable`, + ); + } else { + assert.equal( + descriptor.configurable, + false, + `globalThis["${propertyName}"] should be non-configurable`, + ); + + assert.equal( + descriptor.writable, + false, + `globalThis["${propertyName}"] should be non-writable`, + ); + } + }); + }); +}); diff --git a/ui/components/app/account-list-item/account-list-item.stories.js b/ui/components/app/account-list-item/account-list-item.stories.js new file mode 100644 index 000000000..a6746113d --- /dev/null +++ b/ui/components/app/account-list-item/account-list-item.stories.js @@ -0,0 +1,11 @@ +import React from 'react'; +import AccountListItem from './account-list-item'; + +export default { + title: 'AccountListItem', + id: __filename, +}; + +export const AccountListItemComponent = () => { + return ; +}; diff --git a/ui/components/app/account-menu/account-menu.component.js b/ui/components/app/account-menu/account-menu.component.js index aec8decfb..41c809be8 100644 --- a/ui/components/app/account-menu/account-menu.component.js +++ b/ui/components/app/account-menu/account-menu.component.js @@ -20,6 +20,8 @@ import { import TextField from '../../ui/text-field'; import SearchIcon from '../../ui/search-icon'; +import { isBeta } from '../../../helpers/utils/build-types'; + export function AccountMenuItem(props) { const { icon, children, text, subText, className, onClick } = props; @@ -310,6 +312,13 @@ export default class AccountMenu extends Component { return null; } + let supportText = t('support'); + let supportLink = 'https://support.metamask.io'; + if (isBeta()) { + supportText = t('needHelpSubmitTicket'); + supportLink = 'https://metamask.zendesk.com/hc/en-us/requests/new'; + } + return (
    @@ -410,10 +419,10 @@ export default class AccountMenu extends Component {
    { - global.platform.openTab({ url: 'https://support.metamask.io' }); + global.platform.openTab({ url: supportLink }); }} - icon={{t('support')}} - text={t('support')} + icon={{supportText}} + text={supportText} /> dispatch(toggleAccountMenu()), showAccountDetail: (address) => { dispatch(showAccountDetail(address)); - dispatch(hideSidebar()); dispatch(toggleAccountMenu()); }, lockMetamask: () => { dispatch(lockMetamask()); dispatch(hideWarning()); - dispatch(hideSidebar()); dispatch(toggleAccountMenu()); }, }; diff --git a/ui/components/app/account-menu/index.scss b/ui/components/app/account-menu/index.scss index 39c5b97f9..5b7bda389 100644 --- a/ui/components/app/account-menu/index.scss +++ b/ui/components/app/account-menu/index.scss @@ -9,11 +9,11 @@ min-width: 150px; color: $white; - @media screen and (max-width: 575px) { + @media screen and (max-width: $break-small) { right: calc(((100vw - 100%) / 2) + 8px); } - @media screen and (min-width: 576px) { + @media screen and (min-width: $break-large) { right: calc((100vw - 85vw) / 2); } @@ -33,7 +33,7 @@ position: relative; z-index: 201; - @media screen and (max-width: 575px) { + @media screen and (max-width: $break-small) { padding: 14px; } @@ -127,13 +127,13 @@ overflow-y: auto; position: relative; max-height: 256px; - scrollbar-width: none; + scrollbar-width: auto; &::-webkit-scrollbar { display: none; } - @media screen and (max-width: 575px) { + @media screen and (max-width: $break-small) { max-height: 228px; } @@ -168,7 +168,7 @@ padding: 16px 14px; flex: 0 0 auto; - @media screen and (max-width: 575px) { + @media screen and (max-width: $break-small) { padding: 12px 14px; } diff --git a/ui/components/app/add-token-button/add-token-button.component.js b/ui/components/app/add-token-button/add-token-button.component.js deleted file mode 100644 index b30e3980b..000000000 --- a/ui/components/app/add-token-button/add-token-button.component.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { useHistory } from 'react-router-dom'; -import { useMetricEvent } from '../../../hooks/useMetricEvent'; -import { useI18nContext } from '../../../hooks/useI18nContext'; -import { ADD_TOKEN_ROUTE } from '../../../helpers/constants/routes'; -import Button from '../../ui/button'; - -export default function AddTokenButton() { - const addTokenEvent = useMetricEvent({ - eventOpts: { - category: 'Navigation', - action: 'Token Menu', - name: 'Clicked "Add Token"', - }, - }); - const t = useI18nContext(); - const history = useHistory(); - - return ( -
    - -
    - ); -} diff --git a/ui/components/app/add-token-button/index.js b/ui/components/app/add-token-button/index.js deleted file mode 100644 index 0534cd39f..000000000 --- a/ui/components/app/add-token-button/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './add-token-button.component'; diff --git a/ui/components/app/add-token-button/index.scss b/ui/components/app/add-token-button/index.scss deleted file mode 100644 index e8b2c1a43..000000000 --- a/ui/components/app/add-token-button/index.scss +++ /dev/null @@ -1,6 +0,0 @@ -.add-token-button { - &__button { - max-width: 200px; - margin: 16px auto; - } -} diff --git a/ui/components/app/advanced-gas-controls/advanced-gas-controls.component.js b/ui/components/app/advanced-gas-controls/advanced-gas-controls.component.js index 16f8e82c7..93f3e5ee3 100644 --- a/ui/components/app/advanced-gas-controls/advanced-gas-controls.component.js +++ b/ui/components/app/advanced-gas-controls/advanced-gas-controls.component.js @@ -131,5 +131,5 @@ AdvancedGasControls.propTypes = { maxPriorityFeeFiat: PropTypes.string, maxFeeFiat: PropTypes.string, gasErrors: PropTypes.object, - minimumGasLimit: PropTypes.number, + minimumGasLimit: PropTypes.string, }; diff --git a/ui/components/app/advanced-gas-controls/advanced-gas-controls.stories.js b/ui/components/app/advanced-gas-controls/advanced-gas-controls.stories.js index 150380b1a..7c14aefb0 100644 --- a/ui/components/app/advanced-gas-controls/advanced-gas-controls.stories.js +++ b/ui/components/app/advanced-gas-controls/advanced-gas-controls.stories.js @@ -4,6 +4,7 @@ import AdvancedGasControls from '.'; export default { title: 'Advanced Gas Controls', + id: __filename, }; export const simple = () => { diff --git a/ui/components/app/alerts/unconnected-account-alert/unconnected-account-alert.js b/ui/components/app/alerts/unconnected-account-alert/unconnected-account-alert.js index c73de3f84..1b887bba4 100644 --- a/ui/components/app/alerts/unconnected-account-alert/unconnected-account-alert.js +++ b/ui/components/app/alerts/unconnected-account-alert/unconnected-account-alert.js @@ -15,7 +15,7 @@ import { getSelectedAddress, getSelectedIdentity, } from '../../../../selectors'; -import { isExtensionUrl } from '../../../../helpers/utils/util'; +import { isExtensionUrl, getURLHost } from '../../../../helpers/utils/util'; import Popover from '../../../ui/popover'; import Button from '../../../ui/button'; import Checkbox from '../../../ui/check-box'; @@ -88,7 +88,7 @@ const UnconnectedAccountAlert = () => { return ( { + const t = useI18nContext(); const history = useHistory(); const selectedAccountBalance = useSelector( (state) => getCurrentAccountWithSendEtherInfo(state).balance, @@ -85,12 +95,23 @@ const AssetList = ({ onClickAsset }) => { selectTokenEvent(); }} /> - { - history.push(ADD_TOKEN_ROUTE); - addTokenEvent(); - }} - /> + + + + {t('missingToken')} + + + { + history.push(IMPORT_TOKEN_ROUTE); + addTokenEvent(); + }} + /> + ); }; diff --git a/ui/components/app/confirm-page-container/confirm-page-container-container.test.js b/ui/components/app/confirm-page-container/confirm-page-container-container.test.js new file mode 100644 index 000000000..9ddf77de8 --- /dev/null +++ b/ui/components/app/confirm-page-container/confirm-page-container-container.test.js @@ -0,0 +1,172 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import sinon from 'sinon'; +import { Provider } from 'react-redux'; +import SenderToRecipient from '../../ui/sender-to-recipient'; +import { mountWithRouter } from '../../../../test/lib/render-helpers'; +import Dialog from '../../ui/dialog'; +import ConfirmPageContainer, { + ConfirmPageContainerHeader, + ConfirmPageContainerNavigation, +} from '.'; + +describe('Confirm Page Container Container Test', () => { + let wrapper; + + const mockStore = { + metamask: { + provider: { + type: 'test', + }, + preferences: { + useNativeCurrencyAsPrimaryCurrency: true, + }, + accounts: { + '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5': { + address: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', + balance: '0x03', + }, + }, + cachedBalances: {}, + selectedAddress: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', + addressBook: [], + chainId: 'test', + }, + }; + + const store = configureMockStore()(mockStore); + + const props = { + fromAddress: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', + toAddress: '0x7a1A4Ad9cc746a70ee58568466f7996dD0aCE4E8', + origin: 'testOrigin', // required + onNextTx: sinon.spy(), + // Footer + onCancelAll: sinon.spy(), + onCancel: sinon.spy(), + onSubmit: sinon.spy(), + handleCloseEditGas: sinon.spy(), + // Gas Popover + currentTransaction: {}, + showAddToAddressBookModal: sinon.spy(), + contact: undefined, + isOwnedAccount: false, + }; + + beforeAll(() => { + wrapper = mountWithRouter( + + , + , + store, + ); + }); + + it('should render a confirm page container component', () => { + const pageContainer = wrapper.find('.page-container'); + expect(pageContainer).toHaveLength(1); + expect(pageContainer.getElements()[0].props.className).toStrictEqual( + 'page-container', + ); + }); + + it('should render navigation', () => { + expect(wrapper.find(ConfirmPageContainerNavigation)).toHaveLength(1); + }); + + it('should render header', () => { + expect(wrapper.find(ConfirmPageContainerHeader)).toHaveLength(1); + expect( + wrapper.find(ConfirmPageContainerHeader).getElements()[0].props + .accountAddress, + ).toStrictEqual('0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5'); + }); + + it('should render sender to recipient in header', () => { + expect(wrapper.find(SenderToRecipient)).toHaveLength(1); + expect( + wrapper.find(SenderToRecipient).getElements()[0].props.senderAddress, + ).toStrictEqual('0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5'); + expect( + wrapper.find(SenderToRecipient).getElements()[0].props.recipientAddress, + ).toStrictEqual('0x7a1A4Ad9cc746a70ee58568466f7996dD0aCE4E8'); + }); + + it('should render recipient as address', () => { + const recipientWithAddress = wrapper.find( + '.sender-to-recipient__party--recipient-with-address', + ); + expect(recipientWithAddress).toHaveLength(1); + + expect(wrapper.find('.sender-to-recipient__name')).toHaveLength(2); + }); + + it('should render add address to address book dialog', () => { + expect(wrapper.find(Dialog)).toHaveLength(1); + expect(wrapper.find(Dialog).getElements()[0].props.children).toStrictEqual( + 'newAccountDetectedDialogMessage', + ); + }); + + it('should simulate click on Dialog', () => { + const DialogWrapper = wrapper.find(Dialog); + DialogWrapper.first().simulate('click'); + expect(props.showAddToAddressBookModal.calledOnce).toStrictEqual(true); + }); + + it('should not show add to address dialog if contact is not undefined', () => { + props.contact = { + address: '0x7a1A4Ad9cc746a70ee58568466f7996dD0aCE4E8', + name: 'test saved name', + isEns: false, + chainId: 'test', + }; + + const wrapper2 = mountWithRouter( + + , + , + store, + ); + + expect(wrapper2.find(Dialog)).toHaveLength(0); + }); + + it('should render recipient as name', () => { + const wrapper2 = mountWithRouter( + + , + , + store, + ); + + const recipientWithAddress = wrapper2.find( + '.sender-to-recipient__party--recipient-with-address', + ); + expect(recipientWithAddress).toHaveLength(1); + + expect(wrapper.find('.sender-to-recipient__name')).toHaveLength(2); + }); + + it('should simulate click reject button', () => { + expect(wrapper.find('button.page-container__footer-button')).toHaveLength( + 2, + ); + wrapper + .find('button.page-container__footer-button') + .first() + .simulate('click'); + expect(props.onCancel.calledOnce).toStrictEqual(true); + }); + + it('should simulate click submit button', () => { + expect(wrapper.find('button.page-container__footer-button')).toHaveLength( + 2, + ); + wrapper + .find('button.page-container__footer-button') + .at(1) + .simulate('click'); + expect(props.onSubmit.calledOnce).toStrictEqual(true); + }); +}); diff --git a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js index 28d5f578e..0ab2af228 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js @@ -20,7 +20,6 @@ export default class ConfirmPageContainerContent extends Component { hideSubtitle: PropTypes.bool, identiconAddress: PropTypes.string, nonce: PropTypes.string, - assetImage: PropTypes.string, subtitleComponent: PropTypes.node, title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), titleComponent: PropTypes.node, @@ -77,7 +76,6 @@ export default class ConfirmPageContainerContent extends Component { hideSubtitle, identiconAddress, nonce, - assetImage, detailsComponent, dataComponent, warning, @@ -111,7 +109,6 @@ export default class ConfirmPageContainerContent extends Component { hideSubtitle={hideSubtitle} identiconAddress={identiconAddress} nonce={nonce} - assetImage={assetImage} origin={origin} /> {this.renderContent()} diff --git a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js index cd33ee357..25ab144a0 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js @@ -13,7 +13,6 @@ const ConfirmPageContainerSummary = (props) => { className, identiconAddress, nonce, - assetImage, origin, } = props; @@ -36,7 +35,6 @@ const ConfirmPageContainerSummary = (props) => { className="confirm-page-container-summary__identicon" diameter={36} address={identiconAddress} - image={assetImage} /> )}
    @@ -61,7 +59,6 @@ ConfirmPageContainerSummary.propTypes = { className: PropTypes.string, identiconAddress: PropTypes.string, nonce: PropTypes.string, - assetImage: PropTypes.string, origin: PropTypes.string.isRequired, }; diff --git a/ui/components/app/confirm-page-container/confirm-page-container.component.js b/ui/components/app/confirm-page-container/confirm-page-container.component.js index 9cc2e4286..491235122 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container.component.js @@ -4,6 +4,7 @@ import SenderToRecipient from '../../ui/sender-to-recipient'; import { PageContainerFooter } from '../../ui/page-container'; import EditGasPopover from '../edit-gas-popover'; import { EDIT_GAS_MODES } from '../../../../shared/constants/gas'; +import Dialog from '../../ui/dialog'; import { ConfirmPageContainerHeader, ConfirmPageContainerContent, @@ -41,7 +42,6 @@ export default class ConfirmPageContainer extends Component { detailsComponent: PropTypes.node, identiconAddress: PropTypes.string, nonce: PropTypes.string, - assetImage: PropTypes.string, warning: PropTypes.string, unapprovedTxCount: PropTypes.number, origin: PropTypes.string.isRequired, @@ -66,6 +66,9 @@ export default class ConfirmPageContainer extends Component { handleCloseEditGas: PropTypes.func, // Gas Popover currentTransaction: PropTypes.object.isRequired, + showAddToAddressBookModal: PropTypes.func, + contact: PropTypes.object, + isOwnedAccount: PropTypes.bool, }; render() { @@ -95,7 +98,6 @@ export default class ConfirmPageContainer extends Component { identiconAddress, nonce, unapprovedTxCount, - assetImage, warning, totalTx, positionOfCurrentTx, @@ -114,8 +116,13 @@ export default class ConfirmPageContainer extends Component { editingGas, handleCloseEditGas, currentTransaction, + showAddToAddressBookModal, + contact = {}, + isOwnedAccount, } = this.props; - const renderAssetImage = contentComponent || !identiconAddress; + + const showAddToAddressDialog = + !contact.name && toAddress && !isOwnedAccount && !hideSenderToRecipient; return (
    @@ -145,10 +152,20 @@ export default class ConfirmPageContainer extends Component { recipientAddress={toAddress} recipientEns={toEns} recipientNickname={toNickname} - assetImage={renderAssetImage ? assetImage : undefined} /> )} +
    + {showAddToAddressDialog && ( + showAddToAddressBookModal()} + > + {this.context.t('newAccountDetectedDialogMessage')} + + )} +
    {contentComponent || ( accountWithLabel.address) + .includes(to), + to, + }; +} + +function mapDispatchToProps(dispatch) { + return { + showAddToAddressBookModal: (recipient) => + dispatch( + actions.showModal({ + name: 'ADD_TO_ADDRESSBOOK', + recipient, + }), + ), + }; +} + +function mergeProps(stateProps, dispatchProps, ownProps) { + const { to, ...restStateProps } = stateProps; + return { + ...ownProps, + ...restStateProps, + showAddToAddressBookModal: () => + dispatchProps.showAddToAddressBookModal(to), + }; +} + +export default connect( + mapStateToProps, + mapDispatchToProps, + mergeProps, +)(ConfirmPageContainer); diff --git a/ui/components/app/confirm-page-container/index.js b/ui/components/app/confirm-page-container/index.js index d9bc6f5a8..955ef1bb8 100644 --- a/ui/components/app/confirm-page-container/index.js +++ b/ui/components/app/confirm-page-container/index.js @@ -1,4 +1,4 @@ -export { default } from './confirm-page-container.component'; +export { default } from './confirm-page-container.container'; export { default as ConfirmPageContainerHeader } from './confirm-page-container-header'; export { default as ConfirmDetailRow } from './confirm-detail-row'; export { default as ConfirmPageContainerNavigation } from './confirm-page-container-navigation'; diff --git a/ui/components/app/contact-list/contact-list.component.js b/ui/components/app/contact-list/contact-list.component.js index 04e3a3abd..713edfcfb 100644 --- a/ui/components/app/contact-list/contact-list.component.js +++ b/ui/components/app/contact-list/contact-list.component.js @@ -1,5 +1,6 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; +import { sortBy } from 'lodash'; import Button from '../../ui/button'; import RecipientGroup from './recipient-group/recipient-group.component'; @@ -50,34 +51,36 @@ export default class ContactList extends PureComponent { } renderAddressBook() { - const contacts = this.props.searchForContacts(); + const unsortedContactsByLetter = this.props + .searchForContacts() + .reduce((obj, contact) => { + const firstLetter = contact.name[0].toUpperCase(); + return { + ...obj, + [firstLetter]: [...(obj[firstLetter] || []), contact], + }; + }, {}); - const contactGroups = contacts.reduce((acc, contact) => { - const firstLetter = contact.name.slice(0, 1).toUpperCase(); - acc[firstLetter] = acc[firstLetter] || []; - const bucket = acc[firstLetter]; - bucket.push(contact); - return acc; - }, {}); + const letters = Object.keys(unsortedContactsByLetter).sort(); - return Object.entries(contactGroups) - .sort(([letter1], [letter2]) => { - if (letter1 > letter2) { - return 1; - } else if (letter1 === letter2) { - return 0; - } - return -1; - }) - .map(([letter, groupItems]) => ( - - )); + const sortedContactGroups = letters.map((letter) => { + return [ + letter, + sortBy(unsortedContactsByLetter[letter], (contact) => { + return contact.name.toLowerCase(); + }), + ]; + }); + + return sortedContactGroups.map(([letter, groupItems]) => ( + + )); } renderMyAccounts() { diff --git a/ui/components/app/contact-list/contact-list.test.js b/ui/components/app/contact-list/contact-list.test.js new file mode 100644 index 000000000..71b638dee --- /dev/null +++ b/ui/components/app/contact-list/contact-list.test.js @@ -0,0 +1,58 @@ +import React from 'react'; +import { within } from '@testing-library/react'; +import configureMockStore from 'redux-mock-store'; +import { renderWithProvider } from '../../../../test/jest/rendering'; +import ContactList from '.'; + +describe('Contact List', () => { + const store = configureMockStore([])({ metamask: {} }); + + describe('given searchForContacts', () => { + const selectRecipient = () => null; + const selectedAddress = null; + + it('sorts contacts by name within each letter group', () => { + const { getAllByTestId } = renderWithProvider( + { + return [ + { + name: 'Al', + address: '0x0000000000000000000000000000000000000000', + }, + { + name: 'aa', + address: '0x0000000000000000000000000000000000000001', + }, + { + name: 'Az', + address: '0x0000000000000000000000000000000000000002', + }, + { + name: 'bbb', + address: '0x0000000000000000000000000000000000000003', + }, + ]; + }} + selectRecipient={selectRecipient} + selectedAddress={selectedAddress} + />, + store, + ); + + const recipientGroups = getAllByTestId('recipient-group'); + expect(within(recipientGroups[0]).getByText('A')).toBeInTheDocument(); + const recipientsInA = within(recipientGroups[0]).getAllByTestId( + 'recipient', + ); + expect(recipientsInA[0]).toHaveTextContent('aa0x0000...0001'); + expect(recipientsInA[1]).toHaveTextContent('Al0x0000...0000'); + expect(recipientsInA[2]).toHaveTextContent('Az0x0000...0002'); + expect(within(recipientGroups[1]).getByText('B')).toBeInTheDocument(); + const recipientsInB = within(recipientGroups[1]).getAllByTestId( + 'recipient', + ); + expect(recipientsInB[0]).toHaveTextContent('bbb0x0000...0003'); + }); + }); +}); diff --git a/ui/components/app/contact-list/recipient-group/recipient-group.component.js b/ui/components/app/contact-list/recipient-group/recipient-group.component.js index d57854917..a8fd589db 100644 --- a/ui/components/app/contact-list/recipient-group/recipient-group.component.js +++ b/ui/components/app/contact-list/recipient-group/recipient-group.component.js @@ -19,7 +19,10 @@ export default function RecipientGroup({ } return ( -
    +
    {label && (
    {label} @@ -41,7 +44,10 @@ export default function RecipientGroup({ })} > -
    +
    {name || ellipsify(address)}
    diff --git a/ui/components/app/edit-gas-display-education/edit-gas-display-education.stories.js b/ui/components/app/edit-gas-display-education/edit-gas-display-education.stories.js index aed0fa9c8..f53cfd344 100644 --- a/ui/components/app/edit-gas-display-education/edit-gas-display-education.stories.js +++ b/ui/components/app/edit-gas-display-education/edit-gas-display-education.stories.js @@ -3,6 +3,7 @@ import EditGasDisplayEducation from '.'; export default { title: 'Edit Gas Display', + id: __filename, }; export const basic = () => { diff --git a/ui/components/app/edit-gas-display/edit-gas-display.component.js b/ui/components/app/edit-gas-display/edit-gas-display.component.js index ad41a8c11..ff1fa5e68 100644 --- a/ui/components/app/edit-gas-display/edit-gas-display.component.js +++ b/ui/components/app/edit-gas-display/edit-gas-display.component.js @@ -1,4 +1,4 @@ -import React, { useContext, useState } from 'react'; +import React, { useContext, useLayoutEffect, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; @@ -70,6 +70,8 @@ export default function EditGasDisplay({ txParamsHaveBeenCustomized, }) { const t = useContext(I18nContext); + const scrollRef = useRef(null); + const isMainnet = useSelector(getIsMainnet); const networkAndAccountSupport1559 = useSelector( checkNetworkAndAccountSupports1559, @@ -87,6 +89,12 @@ export default function EditGasDisplay({ showAdvancedInlineGasIfPossible, ); + useLayoutEffect(() => { + if (showAdvancedForm && scrollRef.current) { + scrollRef.current.scrollIntoView(); + } + }, [showAdvancedForm]); + const dappSuggestedAndTxParamGasFeesAreTheSame = areDappSuggestedAndTxParamGasFeesTheSame( transaction, ); @@ -161,19 +169,23 @@ export default function EditGasDisplay({ } detail={ networkAndAccountSupport1559 && - estimatedMaximumFiat !== undefined && - t('editGasTotalBannerSubtitle', [ - - {estimatedMaximumFiat} - , - - {estimatedMaximumNative} - , - ]) + estimatedMaximumFiat !== undefined && ( + <> + + {t('editGasSubTextFeeLabel')} + + + {estimatedMaximumFiat} + + + {`(${estimatedMaximumNative})`} + + + ) } timing={ hasGasErrors === false && ( @@ -281,6 +293,7 @@ export default function EditGasDisplay({
    )} +
    ); } @@ -315,7 +328,7 @@ EditGasDisplay.propTypes = { gasErrors: PropTypes.object, gasWarnings: PropTypes.object, onManualChange: PropTypes.func, - minimumGasLimit: PropTypes.number, + minimumGasLimit: PropTypes.string, balanceError: PropTypes.bool, estimatesUnavailableWarning: PropTypes.bool, hasGasErrors: PropTypes.bool, diff --git a/ui/components/app/edit-gas-display/edit-gas-display.stories.js b/ui/components/app/edit-gas-display/edit-gas-display.stories.js index 6dd89f931..14724468f 100644 --- a/ui/components/app/edit-gas-display/edit-gas-display.stories.js +++ b/ui/components/app/edit-gas-display/edit-gas-display.stories.js @@ -3,6 +3,7 @@ import EditGasDisplay from '.'; export default { title: 'Edit Gas Display', + id: __filename, }; export const basic = () => { diff --git a/ui/components/app/edit-gas-display/index.scss b/ui/components/app/edit-gas-display/index.scss index 2f6c1abf0..d109003ec 100644 --- a/ui/components/app/edit-gas-display/index.scss +++ b/ui/components/app/edit-gas-display/index.scss @@ -67,4 +67,9 @@ position: relative; margin-top: 4px; } + + &__scroll-bottom { + margin-bottom: -20px; + margin-top: 20px; + } } diff --git a/ui/components/app/edit-gas-popover/edit-gas-popover.component.js b/ui/components/app/edit-gas-popover/edit-gas-popover.component.js index 1aef021eb..64280984c 100644 --- a/ui/components/app/edit-gas-popover/edit-gas-popover.component.js +++ b/ui/components/app/edit-gas-popover/edit-gas-popover.component.js @@ -22,7 +22,6 @@ import { createCancelTransaction, createSpeedUpTransaction, hideModal, - hideSidebar, updateTransaction, updateCustomSwapsEIP1559GasParams, updateSwapsUserFeeLevel, @@ -43,7 +42,6 @@ export default function EditGasPopover({ }) { const t = useContext(I18nContext); const dispatch = useDispatch(); - const showSidebar = useSelector((state) => state.appState.sidebar.isOpen); const networkAndAccountSupport1559 = useSelector( checkNetworkAndAccountSupports1559, ); @@ -117,19 +115,17 @@ export default function EditGasPopover({ /** * Temporary placeholder, this should be managed by the parent component but - * we will be extracting this component from the hard to maintain modal/ - * sidebar component. For now this is just to be able to appropriately close + * we will be extracting this component from the hard to maintain modal + * component. For now this is just to be able to appropriately close * the modal in testing */ const closePopover = useCallback(() => { if (onClose) { onClose(); - } else if (showSidebar) { - dispatch(hideSidebar()); } else { dispatch(hideModal()); } - }, [showSidebar, onClose, dispatch]); + }, [onClose, dispatch]); const onSubmit = useCallback(() => { if (!updatedTransaction || !mode) { diff --git a/ui/components/app/edit-gas-popover/edit-gas-popover.stories.js b/ui/components/app/edit-gas-popover/edit-gas-popover.stories.js index 76c7dafae..93d7cc830 100644 --- a/ui/components/app/edit-gas-popover/edit-gas-popover.stories.js +++ b/ui/components/app/edit-gas-popover/edit-gas-popover.stories.js @@ -3,6 +3,7 @@ import EditGasPopover from '.'; export default { title: 'Edit Gas Display Popover', + id: __filename, }; export const basic = () => { diff --git a/ui/components/app/edit-gas-popover/index.scss b/ui/components/app/edit-gas-popover/index.scss index c34c68fb7..a3fe4b5d9 100644 --- a/ui/components/app/edit-gas-popover/index.scss +++ b/ui/components/app/edit-gas-popover/index.scss @@ -1,6 +1,6 @@ .edit-gas-popover { &__wrapper { - @media screen and (min-width: 576px) { + @media screen and (min-width: $break-large) { max-height: 84vh; } } diff --git a/ui/components/app/gas-customization/advanced-gas-inputs/index.scss b/ui/components/app/gas-customization/advanced-gas-inputs/index.scss index 1a747dcb4..fbb1a35ad 100644 --- a/ui/components/app/gas-customization/advanced-gas-inputs/index.scss +++ b/ui/components/app/gas-customization/advanced-gas-inputs/index.scss @@ -18,7 +18,7 @@ justify-content: space-between; align-items: center; - @media screen and (max-width: 576px) { + @media screen and (max-width: $break-small) { @include H8; } 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 37e853cd1..52f2b0fb4 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 @@ -166,7 +166,6 @@ describe('gas-modal-page-container container', () => { updateTransactionGasFees: sinon.spy(), someOtherDispatchProp: sinon.spy(), createSpeedUpTransaction: sinon.spy(), - hideSidebar: sinon.spy(), hideModal: sinon.spy(), cancelAndClose: sinon.spy(), }; @@ -268,7 +267,6 @@ describe('gas-modal-page-container container', () => { expect(dispatchProps.cancelAndClose.callCount).toStrictEqual(1); expect(dispatchProps.createSpeedUpTransaction.callCount).toStrictEqual(1); - expect(dispatchProps.hideSidebar.callCount).toStrictEqual(1); }); }); }); 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 571544cf0..3d77f1214 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 @@ -4,7 +4,6 @@ import { hideModal, createRetryTransaction, createSpeedUpTransaction, - hideSidebar, } from '../../../../store/actions'; import { setCustomGasPrice, @@ -223,7 +222,6 @@ const mapDispatchToProps = (dispatch) => { createSpeedUpTransaction: (txId, customGasSettings) => { return dispatch(createSpeedUpTransaction(txId, customGasSettings)); }, - hideSidebar: () => dispatch(hideSidebar()), }; }; @@ -246,7 +244,6 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { createSpeedUpTransaction: dispatchCreateSpeedUpTransaction, createRetryTransaction: dispatchCreateRetryTransaction, updateTransactionGasFees: dispatchUpdateTransactionGasFees, - hideSidebar: dispatchHideSidebar, cancelAndClose: dispatchCancelAndClose, hideModal: dispatchHideModal, ...otherDispatchProps @@ -258,7 +255,6 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { ...ownProps, onSubmit: (gasLimit, gasPrice) => { if (ownProps.onSubmit) { - dispatchHideSidebar(); dispatchCancelAndClose(); ownProps.onSubmit({ gasLimit, gasPrice }); return; @@ -274,11 +270,9 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { dispatchCancelAndClose(); } else if (isSpeedUp) { dispatchCreateSpeedUpTransaction(txId, { gasPrice, gasLimit }); - dispatchHideSidebar(); dispatchCancelAndClose(); } else if (isRetry) { dispatchCreateRetryTransaction(txId, { gasPrice, gasLimit }); - dispatchHideSidebar(); dispatchCancelAndClose(); } else { dispatchSetGasData(gasLimit, gasPrice); @@ -293,9 +287,6 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { }, cancelAndClose: () => { dispatchCancelAndClose(); - if (isSpeedUp || isRetry) { - dispatchHideSidebar(); - } }, disableSave: insufficientBalance || diff --git a/ui/components/app/gas-customization/gas-price-button-group/index.scss b/ui/components/app/gas-customization/gas-price-button-group/index.scss index 06d59e74f..6c321da6d 100644 --- a/ui/components/app/gas-customization/gas-price-button-group/index.scss +++ b/ui/components/app/gas-customization/gas-price-button-group/index.scss @@ -93,7 +93,7 @@ padding-bottom: 2px; - @media screen and (max-width: 575px) { + @media screen and (max-width: $break-small) { @include H8; } } @@ -103,7 +103,7 @@ padding-bottom: 2px; - @media screen and (max-width: 575px) { + @media screen and (max-width: $break-small) { @include H8; } } diff --git a/ui/components/app/gas-timing/gas-timing.component.js b/ui/components/app/gas-timing/gas-timing.component.js index 4f34141f2..0f28ea7dc 100644 --- a/ui/components/app/gas-timing/gas-timing.component.js +++ b/ui/components/app/gas-timing/gas-timing.component.js @@ -126,7 +126,6 @@ export default function GasTiming({ let text = ''; let attitude = 'positive'; - let fontWeight = FONT_WEIGHT.NORMAL; // Anything medium or faster is positive if ( @@ -159,7 +158,6 @@ export default function GasTiming({ customEstimatedTime === 'unknown' || customEstimatedTime?.upperTimeBound === 'unknown' ) { - fontWeight = FONT_WEIGHT.BOLD; text = unknownProcessingTimeText; } else { text = t('gasTimingNegative', [ @@ -167,16 +165,23 @@ export default function GasTiming({ ]); } } else { - text = t('gasTimingNegative', [ - toHumanReadableTime(low.maxWaitTimeEstimate, t), - ]); + text = ( + <> + {t('gasTimingNegative', [ + toHumanReadableTime(low.maxWaitTimeEstimate, t), + ])} + + + ); } } return ( + + {' or '} + + + ); +} diff --git a/ui/components/app/import-token-link/index.js b/ui/components/app/import-token-link/index.js new file mode 100644 index 000000000..214bd0d84 --- /dev/null +++ b/ui/components/app/import-token-link/index.js @@ -0,0 +1 @@ +export { default } from './import-token-link.component'; diff --git a/ui/components/app/import-token-link/index.scss b/ui/components/app/import-token-link/index.scss new file mode 100644 index 000000000..83586363a --- /dev/null +++ b/ui/components/app/import-token-link/index.scss @@ -0,0 +1,8 @@ +.import-token-link { + & &__link { + @include H6; + + display: inline; + padding: 0 0 16px; + } +} diff --git a/ui/components/app/menu-bar/account-options-menu.js b/ui/components/app/menu-bar/account-options-menu.js index fa2d1b377..ca9b0bfab 100644 --- a/ui/components/app/menu-bar/account-options-menu.js +++ b/ui/components/app/menu-bar/account-options-menu.js @@ -6,6 +6,7 @@ import { getAccountLink } from '@metamask/etherscan-link'; import { showModal } from '../../../store/actions'; import { CONNECTED_ROUTE } from '../../../helpers/constants/routes'; +import { getURLHostName } from '../../../helpers/utils/util'; import { Menu, MenuItem } from '../../ui/menu'; import { getCurrentChainId, @@ -33,14 +34,7 @@ export default function AccountOptionsMenu({ anchorElement, onClose }) { const { address } = selectedIdentity; const addressLink = getAccountLink(address, chainId, rpcPrefs); const { blockExplorerUrl } = rpcPrefs; - - const getBlockExplorerUrlHost = () => { - try { - return new URL(blockExplorerUrl)?.hostname; - } catch (err) { - return ''; - } - }; + const blockExplorerUrlSubTitle = getURLHostName(blockExplorerUrl); const openFullscreenEvent = useMetricEvent({ eventOpts: { @@ -71,12 +65,11 @@ export default function AccountOptionsMenu({ anchorElement, onClose }) { properties: { link_type: 'Account Tracker', action: 'Account Options', - block_explorer_domain: addressLink ? new URL(addressLink)?.hostname : '', + block_explorer_domain: getURLHostName(addressLink), }, }); const isRemovable = keyring.type !== 'HD Key Tree'; - const blockExplorerUrlSubTitle = getBlockExplorerUrlHost(); return ( diff --git a/ui/components/app/modals/metametrics-opt-in-modal/index.scss b/ui/components/app/modals/metametrics-opt-in-modal/index.scss index 08dc6cc15..cabba027a 100644 --- a/ui/components/app/modals/metametrics-opt-in-modal/index.scss +++ b/ui/components/app/modals/metametrics-opt-in-modal/index.scss @@ -5,7 +5,7 @@ margin-right: 0%; max-height: 75vh; - @media screen and (max-width: 575px) { + @media screen and (max-width: $break-small) { max-height: 100vh; } } @@ -20,7 +20,7 @@ } .metametrics-opt-in__footer { - @media screen and (max-width: 575px) { + @media screen and (max-width: $break-small) { margin-top: 10px; justify-content: center; margin-left: 2%; 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 b8b834de6..fac593717 100644 --- a/ui/components/app/modals/qr-scanner/qr-scanner.component.js +++ b/ui/components/app/modals/qr-scanner/qr-scanner.component.js @@ -7,6 +7,7 @@ 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 { getURL } from '../../../../helpers/utils/util'; import PageContainerFooter from '../../../ui/page-container/page-container-footer/page-container-footer.component'; const READY_STATE = { @@ -68,8 +69,8 @@ export default class QrScanner extends Component { !environmentReady && getEnvironmentType() !== ENVIRONMENT_TYPE_FULLSCREEN ) { - const currentUrl = new URL(window.location.href); - const currentHash = currentUrl.hash; + const currentUrl = getURL(window.location.href); + const currentHash = currentUrl?.hash; const currentRoute = currentHash ? currentHash.substring(1) : null; global.platform.openExtensionInBrowser(currentRoute); } diff --git a/ui/components/app/multiple-notifications/index.scss b/ui/components/app/multiple-notifications/index.scss index 01d3af889..2427f7cbe 100644 --- a/ui/components/app/multiple-notifications/index.scss +++ b/ui/components/app/multiple-notifications/index.scss @@ -8,7 +8,7 @@ right: 0; margin: 8px; - @media screen and (max-width: 576px) { + @media screen and (max-width: $break-small) { width: 340px; } diff --git a/ui/components/app/network-display/index.scss b/ui/components/app/network-display/index.scss index 78fe71f56..51ce5bcdc 100644 --- a/ui/components/app/network-display/index.scss +++ b/ui/components/app/network-display/index.scss @@ -35,6 +35,11 @@ background-color: lighten($dodger-blue, 35%); } + &.chip { + margin: 0; + max-width: 100%; + } + & .chip__label { overflow: hidden; text-overflow: ellipsis; diff --git a/ui/components/app/permission-page-container/index.scss b/ui/components/app/permission-page-container/index.scss index 60e2e3e3b..ffd703e8e 100644 --- a/ui/components/app/permission-page-container/index.scss +++ b/ui/components/app/permission-page-container/index.scss @@ -10,7 +10,7 @@ justify-content: space-between; } - @media screen and (min-width: 576px) { + @media screen and (min-width: $break-large) { width: 426px; flex: 1; @@ -101,7 +101,7 @@ align-items: center; margin-top: 12px; - @media screen and (min-width: 576px) { + @media screen and (min-width: $break-large) { border-top: none; } @@ -115,7 +115,7 @@ } } - @media screen and (max-width: 575px) { + @media screen and (max-width: $break-small) { &__title { position: initial; } diff --git a/ui/components/app/sidebars/index.js b/ui/components/app/sidebars/index.js deleted file mode 100644 index d5d5026c6..000000000 --- a/ui/components/app/sidebars/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './sidebar.component'; diff --git a/ui/components/app/sidebars/index.scss b/ui/components/app/sidebars/index.scss deleted file mode 100644 index d2d224eb0..000000000 --- a/ui/components/app/sidebars/index.scss +++ /dev/null @@ -1,81 +0,0 @@ -@import 'sidebar-content'; - -.sidebar-right-enter { - transition: transform 300ms ease-in-out; - transform: translateX(-100%); -} - -.sidebar-right-enter.sidebar-right-enter-active { - transition: transform 300ms ease-in-out; - transform: translateX(0%); -} - -.sidebar-right-leave { - transition: transform 200ms ease-out; - transform: translateX(0%); -} - -.sidebar-right-leave.sidebar-right-leave-active { - transition: transform 200ms ease-out; - transform: translateX(-100%); -} - -.sidebar-left-enter { - transition: transform 300ms ease-in-out; - transform: translateX(100%); -} - -.sidebar-left-enter.sidebar-left-enter-active { - transition: transform 300ms ease-in-out; - transform: translateX(0%); -} - -.sidebar-left-leave { - transition: transform 200ms ease-out; - transform: translateX(0%); -} - -.sidebar-left-leave.sidebar-left-leave-active { - transition: transform 200ms ease-out; - transform: translateX(100%); -} - -.sidebar-left { - flex: 1 0 230px; - background: rgb(250, 250, 250); - z-index: $sidebar-z-index; - position: fixed; - left: 15%; - right: 0; - bottom: 0; - opacity: 1; - visibility: visible; - will-change: transform; - overflow-y: auto; - box-shadow: rgba(0, 0, 0, 0.15) 2px 2px 4px; - width: 85%; - height: 100%; - - @media screen and (min-width: 769px) { - width: 408px; - left: calc(100% - 408px); - } - - @media screen and (max-width: $break-small) { - width: 100%; - left: 0%; - } -} - -.sidebar-overlay { - z-index: $sidebar-overlay-z-index; - position: fixed; - height: 100%; - width: 100%; - left: 0; - right: 0; - bottom: 0; - opacity: 1; - visibility: visible; - background-color: rgba(0, 0, 0, 0.3); -} diff --git a/ui/components/app/sidebars/sidebar-content.scss b/ui/components/app/sidebars/sidebar-content.scss deleted file mode 100644 index d8bb0a5d0..000000000 --- a/ui/components/app/sidebars/sidebar-content.scss +++ /dev/null @@ -1,104 +0,0 @@ -.sidebar-left { - display: flex; - - .gas-modal-page-container { - display: flex; - - .page-container { - flex: 1; - max-width: 100%; - - &__content { - display: flex; - overflow-y: initial; - } - - @media screen and (max-width: $break-small) { - max-width: 344px; - min-height: auto; - } - - @media screen and (min-width: $break-small) { - max-height: none; - } - } - - .page-container__bottom { - display: flex; - flex-direction: column; - height: 100%; - } - - .page-container__content { - overflow-y: inherit; - } - - .basic-tab-content { - height: auto; - margin-bottom: 0; - border-bottom: 1px solid #d2d8dd; - flex: 1 1 70%; - - @media screen and (max-width: $break-small) { - padding-left: 14px; - padding-bottom: 21px; - } - - .gas-price-button-group--alt { - @media screen and (max-width: $break-small) { - max-width: 318px; - - &__time-estimate { - @include H7; - } - } - } - } - - .advanced-tab { - @media screen and (min-width: $break-small) { - flex: 1 1 70%; - } - - &__fee-chart { - height: 320px; - - @media screen and (max-width: $break-small) { - height: initial; - } - } - - &__fee-chart__speed-buttons { - bottom: 77px; - - @media screen and (max-width: $break-small) { - display: none; - } - } - } - - .gas-modal-content { - display: flex; - flex-direction: column; - width: 100%; - - &__info-row-wrapper { - display: flex; - - @media screen and (min-width: $break-small) { - flex: 1 1 30%; - } - } - - &__info-row { - height: 170px; - - @media screen and (max-width: $break-small) { - height: initial; - display: flex; - justify-content: center; - } - } - } - } -} diff --git a/ui/components/app/sidebars/sidebar.component.js b/ui/components/app/sidebars/sidebar.component.js deleted file mode 100644 index 4ed71461b..000000000 --- a/ui/components/app/sidebars/sidebar.component.js +++ /dev/null @@ -1,79 +0,0 @@ -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 = { - sidebarOpen: PropTypes.bool, - hideSidebar: PropTypes.func, - sidebarShouldClose: PropTypes.bool, - transitionName: PropTypes.string, - type: PropTypes.string, - sidebarProps: PropTypes.object, - onOverlayClose: PropTypes.func, - }; - - static contextTypes = { - t: PropTypes.func, - }; - - renderOverlay() { - const { onOverlayClose } = this.props; - - return ( -
    { - onOverlayClose?.(); - this.props.hideSidebar(); - }} - /> - ); - } - - renderSidebarContent() { - const { type, sidebarProps = {} } = this.props; - const { transaction = {}, onSubmit, hideBasic } = sidebarProps; - switch (type) { - case 'customize-gas': - return ( -
    - -
    - ); - default: - return null; - } - } - - componentDidUpdate(prevProps) { - if (!prevProps.sidebarShouldClose && this.props.sidebarShouldClose) { - this.props.hideSidebar(); - } - } - - render() { - const { transitionName, sidebarOpen, sidebarShouldClose } = this.props; - - const showSidebar = sidebarOpen && !sidebarShouldClose; - - return ( -
    - - {showSidebar ? this.renderSidebarContent() : null} - - {showSidebar ? this.renderOverlay() : null} -
    - ); - } -} diff --git a/ui/components/app/sidebars/sidebar.component.test.js b/ui/components/app/sidebars/sidebar.component.test.js deleted file mode 100644 index 90ecb5ab5..000000000 --- a/ui/components/app/sidebars/sidebar.component.test.js +++ /dev/null @@ -1,108 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import sinon from 'sinon'; -import ReactCSSTransitionGroup from 'react-transition-group/CSSTransitionGroup'; -import CustomizeGas from '../gas-customization/gas-modal-page-container'; -import Sidebar from './sidebar.component'; - -const propsMethodSpies = { - hideSidebar: sinon.spy(), -}; - -describe('Sidebar Component', () => { - let wrapper; - - beforeEach(() => { - wrapper = shallow( - , - ); - }); - - afterEach(() => { - propsMethodSpies.hideSidebar.resetHistory(); - }); - - describe('renderOverlay', () => { - let renderOverlay; - - beforeEach(() => { - renderOverlay = shallow(wrapper.instance().renderOverlay()); - }); - - it('should render a overlay element', () => { - expect(renderOverlay.hasClass('sidebar-overlay')).toStrictEqual(true); - }); - - it('should pass the correct onClick function to the element', () => { - expect(propsMethodSpies.hideSidebar.callCount).toStrictEqual(0); - renderOverlay.props().onClick(); - expect(propsMethodSpies.hideSidebar.callCount).toStrictEqual(1); - }); - }); - - describe('renderSidebarContent', () => { - let renderSidebarContent; - - beforeEach(() => { - renderSidebarContent = wrapper.instance().renderSidebarContent(); - }); - - it('should render sidebar content with the type customize-gas', () => { - renderSidebarContent = wrapper.instance().renderSidebarContent(); - const renderedSidebarContent = shallow(renderSidebarContent); - expect(renderedSidebarContent.hasClass('sidebar-left')).toStrictEqual( - true, - ); - expect(renderedSidebarContent.childAt(0).is(CustomizeGas)).toStrictEqual( - true, - ); - }); - - it('should not render with an unrecognized type', () => { - wrapper.setProps({ type: 'foobar' }); - renderSidebarContent = wrapper.instance().renderSidebarContent(); - expect(renderSidebarContent).toBeNull(); - }); - }); - - describe('render', () => { - it('should render a div with one child', () => { - expect(wrapper.is('div')).toStrictEqual(true); - expect(wrapper.children()).toHaveLength(1); - }); - - it('should render the ReactCSSTransitionGroup without any children', () => { - expect( - wrapper.children().at(0).is(ReactCSSTransitionGroup), - ).toStrictEqual(true); - expect(wrapper.children().at(0).children()).toHaveLength(0); - }); - - it('should render sidebar content and the overlay if sidebarOpen is true', () => { - wrapper.setProps({ sidebarOpen: true }); - expect(wrapper.children()).toHaveLength(2); - expect( - wrapper.children().at(1).hasClass('sidebar-overlay'), - ).toStrictEqual(true); - expect(wrapper.children().at(0).children()).toHaveLength(1); - expect( - wrapper.children().at(0).children().at(0).hasClass('sidebar-left'), - ).toStrictEqual(true); - expect( - wrapper - .children() - .at(0) - .children() - .at(0) - .children() - .at(0) - .is(CustomizeGas), - ).toBe(true); - }); - }); -}); diff --git a/ui/components/app/signature-request-original/index.scss b/ui/components/app/signature-request-original/index.scss index 3d90271ed..80849087e 100644 --- a/ui/components/app/signature-request-original/index.scss +++ b/ui/components/app/signature-request-original/index.scss @@ -182,6 +182,7 @@ &__notice { color: $dusty-gray; + padding: 0 10px; } &__warning { @@ -228,6 +229,7 @@ cursor: pointer; text-decoration: underline; color: $primary-blue; + margin-inline-start: 3px; } &__footer { diff --git a/ui/components/app/signature-request/index.scss b/ui/components/app/signature-request/index.scss index f4df222a1..de89e8116 100644 --- a/ui/components/app/signature-request/index.scss +++ b/ui/components/app/signature-request/index.scss @@ -8,7 +8,7 @@ flex-direction: column; min-width: 0; - @media screen and (min-width: 576px) { + @media screen and (min-width: $break-large) { flex: initial; } } diff --git a/ui/components/app/signature-request/signature-request-message/index.scss b/ui/components/app/signature-request/signature-request-message/index.scss index 9b03134c7..3652addd1 100644 --- a/ui/components/app/signature-request/signature-request-message/index.scss +++ b/ui/components/app/signature-request/signature-request-message/index.scss @@ -30,7 +30,7 @@ padding-left: 12px; padding-right: 12px; - @media screen and (min-width: 576px) { + @media screen and (min-width: $break-large) { width: auto; } } diff --git a/ui/components/app/signature-request/signature-request.stories.js b/ui/components/app/signature-request/signature-request.stories.js index 1590f84e1..b13dc3679 100644 --- a/ui/components/app/signature-request/signature-request.stories.js +++ b/ui/components/app/signature-request/signature-request.stories.js @@ -10,6 +10,7 @@ const containerStyle = { export default { title: 'Signature Request', + id: __filename, }; export const FirstLook = () => { diff --git a/ui/components/app/tab-bar/index.scss b/ui/components/app/tab-bar/index.scss index 1e20d1fed..c6bac6edc 100644 --- a/ui/components/app/tab-bar/index.scss +++ b/ui/components/app/tab-bar/index.scss @@ -19,7 +19,7 @@ background-color: unset; text-align: start; - @media screen and (min-width: 576px) { + @media screen and (min-width: $break-large) { &:hover { opacity: 0.4; } @@ -29,7 +29,7 @@ } } - @media screen and (max-width: 575px) { + @media screen and (max-width: $break-small) { @include H4; padding: 24px; @@ -44,7 +44,7 @@ &__description { display: none; - @media screen and (max-width: 575px) { + @media screen and (max-width: $break-small) { @include H6; display: block; @@ -58,7 +58,7 @@ &__caret { display: none; - @media screen and (max-width: 575px) { + @media screen and (max-width: $break-small) { display: block; background-image: url('/images/caret-right.svg'); width: 36px; diff --git a/ui/components/app/token-cell/token-cell.js b/ui/components/app/token-cell/token-cell.js index 18a7cd698..8be05c823 100644 --- a/ui/components/app/token-cell/token-cell.js +++ b/ui/components/app/token-cell/token-cell.js @@ -21,7 +21,6 @@ export default function TokenCell({ const t = useI18nContext(); const formattedFiat = useTokenFiatAmount(address, string, symbol); - const warning = balanceError ? ( {t('troubleTokenBalances')} diff --git a/ui/components/app/token-cell/token-cell.test.js b/ui/components/app/token-cell/token-cell.test.js index 12b3ae973..a118e912b 100644 --- a/ui/components/app/token-cell/token-cell.test.js +++ b/ui/components/app/token-cell/token-cell.test.js @@ -27,11 +27,6 @@ describe('Token Cell', () => { type: 'mainnet', }, }, - appState: { - sidebar: { - isOpen: true, - }, - }, }; const middlewares = [thunk]; diff --git a/ui/components/app/token-list/token-list.js b/ui/components/app/token-list/token-list.js index 9d0fdc77e..5ff3eb382 100644 --- a/ui/components/app/token-list/token-list.js +++ b/ui/components/app/token-list/token-list.js @@ -6,15 +6,11 @@ import { useSelector } from 'react-redux'; import TokenCell from '../token-cell'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { useTokenTracker } from '../../../hooks/useTokenTracker'; -import { - getAssetImages, - getShouldHideZeroBalanceTokens, -} from '../../../selectors'; +import { getShouldHideZeroBalanceTokens } from '../../../selectors'; import { getTokens } from '../../../ducks/metamask/metamask'; export default function TokenList({ onTokenClick }) { const t = useI18nContext(); - const assetImages = useSelector(getAssetImages); const shouldHideZeroBalanceTokens = useSelector( getShouldHideZeroBalanceTokens, ); @@ -46,7 +42,6 @@ export default function TokenList({ onTokenClick }) { return (
    {tokensWithBalances.map((tokenData, index) => { - tokenData.image = assetImages[tokenData.address]; return ; })}
    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 2e7cb77d3..6162f724d 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 @@ -7,7 +7,7 @@ import { getEthConversionFromWeiHex, getValueFromWeiHex, } from '../../../helpers/utils/conversions.util'; -import { formatDate } from '../../../helpers/utils/util'; +import { formatDate, getURLHostName } from '../../../helpers/utils/util'; import TransactionActivityLogIcon from './transaction-activity-log-icon'; import { CONFIRMED_STATUS } from './transaction-activity-log.constants'; @@ -41,9 +41,7 @@ export default class TransactionActivityLog extends PureComponent { properties: { link_type: 'Transaction Block Explorer', action: 'Activity Details', - block_explorer_domain: etherscanUrl - ? new URL(etherscanUrl)?.hostname - : '', + block_explorer_domain: getURLHostName(etherscanUrl), }, }); diff --git a/ui/components/app/transaction-breakdown/transaction-breakdown-row/transaction-breakdown-row.component.js b/ui/components/app/transaction-breakdown/transaction-breakdown-row/transaction-breakdown-row.component.js index 2668811b1..99c2b1e30 100644 --- a/ui/components/app/transaction-breakdown/transaction-breakdown-row/transaction-breakdown-row.component.js +++ b/ui/components/app/transaction-breakdown/transaction-breakdown-row/transaction-breakdown-row.component.js @@ -13,9 +13,22 @@ export default class TransactionBreakdownRow extends PureComponent { const { title, children, className } = this.props; return ( -
    -
    {title}
    -
    {children}
    +
    +
    + {title} +
    +
    + {children} +
    ); } diff --git a/ui/components/app/transaction-breakdown/transaction-breakdown.component.js b/ui/components/app/transaction-breakdown/transaction-breakdown.component.js index f0a314c46..d079db4e7 100644 --- a/ui/components/app/transaction-breakdown/transaction-breakdown.component.js +++ b/ui/components/app/transaction-breakdown/transaction-breakdown.component.js @@ -26,6 +26,7 @@ export default class TransactionBreakdown extends PureComponent { isTokenApprove: PropTypes.bool, gas: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), gasPrice: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + maxFeePerGas: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), gasUsed: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), totalInHex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), baseFee: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), @@ -43,6 +44,7 @@ export default class TransactionBreakdown extends PureComponent { const { gas, gasPrice, + maxFeePerGas, primaryCurrency, className, nonce, @@ -98,40 +100,32 @@ export default class TransactionBreakdown extends PureComponent { /> )} - {isEIP1559Transaction && ( + {isEIP1559Transaction && typeof baseFee !== 'undefined' ? ( - {typeof baseFee === 'undefined' ? ( - '?' - ) : ( - - )} + - )} - {isEIP1559Transaction && ( + ) : null} + {isEIP1559Transaction && typeof priorityFee !== 'undefined' ? ( - {typeof priorityFee === 'undefined' ? ( - '?' - ) : ( - - )} + - )} + ) : null} {!isEIP1559Transaction && ( {typeof gasPrice === 'undefined' ? ( @@ -169,6 +163,25 @@ export default class TransactionBreakdown extends PureComponent { )} )} + {isEIP1559Transaction && ( + + + {showFiat && ( + + )} + + )} { - it('should render properly', () => { - const transaction = { - history: [], - id: 1, - status: TRANSACTION_STATUSES.CONFIRMED, - txParams: { - from: '0x1', - gas: GAS_LIMITS.SIMPLE, - gasPrice: '0x3b9aca00', - nonce: '0xa4', - to: '0x2', - value: '0x2386f26fc10000', - }, - }; - - const wrapper = shallow( - , - { context: { t: (str1, str2) => (str2 ? str1 + str2 : str1) } }, - ); - - expect(wrapper.hasClass('transaction-breakdown')).toStrictEqual(true); - expect(wrapper.hasClass('test-class')).toStrictEqual(true); - }); -}); diff --git a/ui/components/app/transaction-breakdown/transaction-breakdown.container.js b/ui/components/app/transaction-breakdown/transaction-breakdown.container.js index e07c2eb80..1b26a57b8 100644 --- a/ui/components/app/transaction-breakdown/transaction-breakdown.container.js +++ b/ui/components/app/transaction-breakdown/transaction-breakdown.container.js @@ -10,7 +10,7 @@ import TransactionBreakdown from './transaction-breakdown.component'; const mapStateToProps = (state, ownProps) => { const { transaction, isTokenApprove } = ownProps; const { - txParams: { gas, gasPrice, value } = {}, + txParams: { gas, gasPrice, maxFeePerGas, value } = {}, txReceipt: { gasUsed, effectiveGasPrice } = {}, baseFeePerGas, } = transaction; @@ -41,6 +41,7 @@ const mapStateToProps = (state, ownProps) => { totalInHex, gas, gasPrice, + maxFeePerGas, gasUsed, isTokenApprove, hexGasTotal, diff --git a/ui/components/app/transaction-breakdown/transaction-breakdown.test.js b/ui/components/app/transaction-breakdown/transaction-breakdown.test.js new file mode 100644 index 000000000..6fec2e3ca --- /dev/null +++ b/ui/components/app/transaction-breakdown/transaction-breakdown.test.js @@ -0,0 +1,97 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import { within } from '@testing-library/react'; +import { renderWithProvider } from '../../../../test/jest/rendering'; +import TransactionBreakdown from '.'; + +function getActualDataFrom(transactionBreakdownRows) { + return transactionBreakdownRows.map((transactionBreakdownRow) => { + const title = within(transactionBreakdownRow).getByTestId( + 'transaction-breakdown-row-title', + ); + const value = within(transactionBreakdownRow).getByTestId( + 'transaction-breakdown-row-value', + ); + return [title.textContent, value.textContent]; + }); +} + +describe('TransactionBreakdown', () => { + const store = configureMockStore()({ + metamask: { + nativeCurrency: null, + preferences: {}, + provider: { + chainId: null, + }, + }, + }); + + describe('with a typical non-EIP-1559 transaction', () => { + it('renders properly', () => { + const { getAllByTestId } = renderWithProvider( + , + store, + ); + + expect( + getActualDataFrom(getAllByTestId('transaction-breakdown-row')), + ).toStrictEqual([ + ['Nonce', '29'], + ['Amount', '-0.01 ETH'], + ['Gas Limit (units)', '46890'], + ['Gas price', '2.467043803'], + ['Total', '0.010116ETH'], + ]); + }); + }); + + describe('with a typical EIP-1559 transaction', () => { + it('renders properly', () => { + const { getAllByTestId } = renderWithProvider( + , + store, + ); + + expect( + getActualDataFrom(getAllByTestId('transaction-breakdown-row')), + ).toStrictEqual([ + ['Nonce', '29'], + ['Amount', '-0.01 ETH'], + ['Gas Limit (units)', '46890'], + ['Gas Used (units)', '31260'], + ['Base Fee (GWEI)', '0.000000007'], + ['Priority Fee (GWEI)', '2.467043796'], + ['Total Gas Fee', '0.000077ETH'], + ['Max Fee Per Gas', '0.000000003ETH'], + ['Total', '0.010077ETH'], + ]); + }); + }); +}); diff --git a/ui/components/app/transaction-detail-item/transaction-detail-item.stories.js b/ui/components/app/transaction-detail-item/transaction-detail-item.stories.js index e8ca87cf7..bb98c0bfb 100644 --- a/ui/components/app/transaction-detail-item/transaction-detail-item.stories.js +++ b/ui/components/app/transaction-detail-item/transaction-detail-item.stories.js @@ -5,6 +5,7 @@ import TransactionDetailItem from '.'; export default { title: 'Transaction Detail Item', + id: __filename, }; export const basic = () => { diff --git a/ui/components/app/transaction-detail/transaction-detail.stories.js b/ui/components/app/transaction-detail/transaction-detail.stories.js index 193090c25..b0cbfeb31 100644 --- a/ui/components/app/transaction-detail/transaction-detail.stories.js +++ b/ui/components/app/transaction-detail/transaction-detail.stories.js @@ -6,6 +6,7 @@ import TransactionDetail from '.'; export default { title: 'Transaction Detail', + id: __filename, }; const rows = [ 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 973b2d0c9..1e0266573 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 @@ -13,6 +13,7 @@ import CancelButton from '../cancel-button'; import Popover from '../../ui/popover'; import { SECOND } from '../../../../shared/constants/time'; import { TRANSACTION_TYPES } from '../../../../shared/constants/transaction'; +import { getURLHostName } from '../../../helpers/utils/util'; export default class TransactionListItemDetails extends PureComponent { static contextTypes = { @@ -65,9 +66,7 @@ export default class TransactionListItemDetails extends PureComponent { properties: { link_type: 'Transaction Block Explorer', action: 'Transaction Details', - block_explorer_domain: blockExplorerLink - ? new URL(blockExplorerLink)?.hostname - : '', + block_explorer_domain: getURLHostName(blockExplorerLink), }, }); diff --git a/ui/components/app/transaction-list-item/transaction-list-item.component.test.js b/ui/components/app/transaction-list-item/transaction-list-item.component.test.js index 0d7da539e..de56d25ca 100644 --- a/ui/components/app/transaction-list-item/transaction-list-item.component.test.js +++ b/ui/components/app/transaction-list-item/transaction-list-item.component.test.js @@ -16,7 +16,7 @@ import { import { useGasFeeEstimates } from '../../../hooks/useGasFeeEstimates'; import { GAS_ESTIMATE_TYPES } from '../../../../shared/constants/gas'; -import TransactionListItem from './transaction-list-item.component'; +import TransactionListItem from '.'; const FEE_MARKET_ESTIMATE_RETURN_VALUE = { gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, @@ -50,7 +50,7 @@ jest.mock('react-redux', () => { return { ...actual, useSelector: jest.fn(), - useDispatch: jest.fn(), + useDispatch: () => jest.fn(), }; }); @@ -63,6 +63,14 @@ setBackgroundConnection({ getGasFeeEstimatesAndStartPolling: jest.fn(), }); +jest.mock('react', () => { + const originReact = jest.requireActual('react'); + return { + ...originReact, + useLayoutEffect: jest.fn(), + }; +}); + const generateUseSelectorRouter = (opts) => (selector) => { if (selector === getConversionRate) { return 1; @@ -85,18 +93,18 @@ const generateUseSelectorRouter = (opts) => (selector) => { }; describe('TransactionListItem', () => { - describe('when account has insufficient balance to cover gas', function () { - beforeAll(function () { + describe('when account has insufficient balance to cover gas', () => { + beforeAll(() => { useGasFeeEstimates.mockImplementation( () => FEE_MARKET_ESTIMATE_RETURN_VALUE, ); }); - afterAll(function () { + afterAll(() => { useGasFeeEstimates.restore(); }); - it(`should indicate account has insufficient funds to cover gas price for cancellation of pending transaction`, function () { + it(`should indicate account has insufficient funds to cover gas price for cancellation of pending transaction`, () => { useSelector.mockImplementation( generateUseSelectorRouter({ balance: '0x3', @@ -108,7 +116,7 @@ describe('TransactionListItem', () => { expect(queryByTestId('not-enough-gas__tooltip')).toBeInTheDocument(); }); - it('should not disable "cancel" button when user has sufficient funds', function () { + it('should not disable "cancel" button when user has sufficient funds', () => { useSelector.mockImplementation( generateUseSelectorRouter({ balance: '2AA1EFB94E0000', @@ -120,7 +128,7 @@ describe('TransactionListItem', () => { expect(queryByTestId('not-enough-gas__tooltip')).not.toBeInTheDocument(); }); - it(`should open the edit gas popover when cancel is clicked`, function () { + it(`should open the edit gas popover when cancel is clicked`, () => { useSelector.mockImplementation( generateUseSelectorRouter({ balance: '2AA1EFB94E0000', diff --git a/ui/components/app/transaction-list/transaction-list.component.js b/ui/components/app/transaction-list/transaction-list.component.js index bfd294000..7a86e1513 100644 --- a/ui/components/app/transaction-list/transaction-list.component.js +++ b/ui/components/app/transaction-list/transaction-list.component.js @@ -12,6 +12,7 @@ import Button from '../../ui/button'; import { TOKEN_CATEGORY_HASH } from '../../../helpers/constants/transactions'; import { SWAPS_CHAINID_CONTRACT_ADDRESS_MAP } from '../../../../shared/constants/swaps'; import { TRANSACTION_TYPES } from '../../../../shared/constants/transaction'; +import { isEqualCaseInsensitive } from '../../../helpers/utils/util'; const PAGE_INCREMENT = 10; @@ -28,7 +29,7 @@ const getTransactionGroupRecipientAddressFilter = ( ) => { return ({ initialTransaction: { txParams } }) => { return ( - txParams?.to === recipientAddress || + isEqualCaseInsensitive(txParams?.to, recipientAddress) || (txParams?.to === SWAPS_CHAINID_CONTRACT_ADDRESS_MAP[chainId] && txParams.data.match(recipientAddress.slice(2))) ); diff --git a/ui/components/app/transaction-list/transaction-list.stories.js b/ui/components/app/transaction-list/transaction-list.stories.js new file mode 100644 index 000000000..eea8cc97f --- /dev/null +++ b/ui/components/app/transaction-list/transaction-list.stories.js @@ -0,0 +1,20 @@ +/* eslint-disable react/prop-types */ +import React from 'react'; +import TransactionList from '.'; + +export default { + title: 'Transaction List', + id: __filename, +}; + +const PageSet = ({ children }) => { + return children; +}; + +export const TxList = () => { + return ( + + + + ); +}; diff --git a/ui/components/app/transaction-total-banner/index.scss b/ui/components/app/transaction-total-banner/index.scss index 42979624c..1bb2f0674 100644 --- a/ui/components/app/transaction-total-banner/index.scss +++ b/ui/components/app/transaction-total-banner/index.scss @@ -4,4 +4,8 @@ &__detail { padding-bottom: 4px; } + + span { + margin-inline-start: 2px; + } } diff --git a/ui/components/app/transaction-total-banner/transaction-total-banner.stories.js b/ui/components/app/transaction-total-banner/transaction-total-banner.stories.js index 46838ae37..c8849c8cf 100644 --- a/ui/components/app/transaction-total-banner/transaction-total-banner.stories.js +++ b/ui/components/app/transaction-total-banner/transaction-total-banner.stories.js @@ -3,6 +3,7 @@ import TransactionTotalBanner from '.'; export default { title: 'Transaction Total Banner', + id: __filename, }; export const basic = () => { diff --git a/ui/components/app/wallet-overview/eth-overview.js b/ui/components/app/wallet-overview/eth-overview.js index 812b3bc90..ef822ca05 100644 --- a/ui/components/app/wallet-overview/eth-overview.js +++ b/ui/components/app/wallet-overview/eth-overview.js @@ -152,7 +152,7 @@ const EthOverview = ({ className }) => { label={t('swap')} tooltipRender={(contents) => ( diff --git a/ui/components/app/wallet-overview/token-overview.js b/ui/components/app/wallet-overview/token-overview.js index e40003067..0b5476261 100644 --- a/ui/components/app/wallet-overview/token-overview.js +++ b/ui/components/app/wallet-overview/token-overview.js @@ -20,7 +20,6 @@ import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount'; import { ASSET_TYPES, updateSendAsset } from '../../../ducks/send'; import { setSwapsFromToken } from '../../../ducks/swaps/swaps'; import { - getAssetImages, getCurrentKeyring, getIsSwapsChain, } from '../../../selectors/selectors'; @@ -42,8 +41,6 @@ const TokenOverview = ({ className, token }) => { }, }); const history = useHistory(); - const assetImages = useSelector(getAssetImages); - const keyring = useSelector(getCurrentKeyring); const usingHardwareWallet = keyring.type.search('Hardware') !== -1; const { tokensWithBalances } = useTokenTracker([token]); @@ -109,7 +106,7 @@ const TokenOverview = ({ className, token }) => { dispatch( setSwapsFromToken({ ...token, - iconUrl: assetImages[token.address], + iconUrl: token.image, balance, string: balanceToRender, }), @@ -124,7 +121,7 @@ const TokenOverview = ({ className, token }) => { label={t('swap')} tooltipRender={(contents) => ( @@ -136,11 +133,7 @@ const TokenOverview = ({ className, token }) => { } className={className} icon={ - + } /> ); @@ -152,6 +145,7 @@ TokenOverview.propTypes = { address: PropTypes.string.isRequired, decimals: PropTypes.number, symbol: PropTypes.string, + image: PropTypes.string, isERC721: PropTypes.bool, }).isRequired, }; diff --git a/ui/components/app/whats-new-popup/index.scss b/ui/components/app/whats-new-popup/index.scss index c00568ab2..6f8a2100a 100644 --- a/ui/components/app/whats-new-popup/index.scss +++ b/ui/components/app/whats-new-popup/index.scss @@ -76,12 +76,12 @@ } .popover-wrap.whats-new-popup__popover { - @media screen and (min-width: 576px) { + @media screen and (min-width: $break-large) { max-height: 600px; width: 500px; } - @media screen and (max-width: 575px) { + @media screen and (max-width: $break-small) { max-height: 568px; } } diff --git a/ui/components/ui/actionable-message/actionable-message.stories.js b/ui/components/ui/actionable-message/actionable-message.stories.js index 00643bad3..edb6aa823 100644 --- a/ui/components/ui/actionable-message/actionable-message.stories.js +++ b/ui/components/ui/actionable-message/actionable-message.stories.js @@ -5,6 +5,7 @@ import ActionableMessage from '.'; export default { title: 'ActionableMessage', + id: __filename, }; export const NoAction = () => ( diff --git a/ui/components/ui/actionable-message/index.scss b/ui/components/ui/actionable-message/index.scss index a4c03ca22..cbb1f8ba6 100644 --- a/ui/components/ui/actionable-message/index.scss +++ b/ui/components/ui/actionable-message/index.scss @@ -17,6 +17,10 @@ justify-content: normal; } + &--with-icon.actionable-message--with-right-button { + padding-left: 32px; + } + svg { width: 16px; height: 16px; diff --git a/ui/components/ui/alert-circle-icon/alert-circle-icon.stories.js b/ui/components/ui/alert-circle-icon/alert-circle-icon.stories.js index e4f0ec846..4053d9661 100644 --- a/ui/components/ui/alert-circle-icon/alert-circle-icon.stories.js +++ b/ui/components/ui/alert-circle-icon/alert-circle-icon.stories.js @@ -3,6 +3,7 @@ import AlertCircleIcon from './alert-circle-icon.component'; export default { title: 'AlertCircleIcon', + id: __filename, }; export const dangerCircleIcon = () => ; diff --git a/ui/components/ui/box/box.stories.js b/ui/components/ui/box/box.stories.js index ba41f454f..25b17838b 100644 --- a/ui/components/ui/box/box.stories.js +++ b/ui/components/ui/box/box.stories.js @@ -13,6 +13,7 @@ import Box from './box'; export default { title: 'Box', + id: __filename, }; const sizeKnobOptions = [undefined, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; diff --git a/ui/components/ui/button-group/button-group.stories.js b/ui/components/ui/button-group/button-group.stories.js index 29b7804d4..d71172bd0 100644 --- a/ui/components/ui/button-group/button-group.stories.js +++ b/ui/components/ui/button-group/button-group.stories.js @@ -7,6 +7,7 @@ import ButtonGroup from '.'; export default { title: 'ButtonGroup', + id: __filename, }; export const withButtons = () => ( diff --git a/ui/components/ui/button/button.stories.js b/ui/components/ui/button/button.stories.js index f2bfe3838..ea526c957 100644 --- a/ui/components/ui/button/button.stories.js +++ b/ui/components/ui/button/button.stories.js @@ -5,6 +5,7 @@ import Button from '.'; export default { title: 'Button', + id: __filename, }; export const primaryType = () => ( diff --git a/ui/components/ui/callout/callout.stories.js b/ui/components/ui/callout/callout.stories.js index 7e2a54691..241fe34f9 100644 --- a/ui/components/ui/callout/callout.stories.js +++ b/ui/components/ui/callout/callout.stories.js @@ -11,6 +11,7 @@ import Callout from './callout'; export default { title: 'Callout', + id: __filename, }; export const persistentCallout = () => ( diff --git a/ui/components/ui/check-box/check-box.stories.js b/ui/components/ui/check-box/check-box.stories.js index 65e8d7281..6894e46d1 100644 --- a/ui/components/ui/check-box/check-box.stories.js +++ b/ui/components/ui/check-box/check-box.stories.js @@ -9,6 +9,7 @@ import CheckBox, { export default { title: 'Check Box', + id: __filename, }; const checkboxOptions = { diff --git a/ui/components/ui/chip/chip.stories.js b/ui/components/ui/chip/chip.stories.js index c05cf7bbd..c1cb04473 100644 --- a/ui/components/ui/chip/chip.stories.js +++ b/ui/components/ui/chip/chip.stories.js @@ -10,6 +10,7 @@ import Chip from '.'; export default { title: 'Chip', + id: __filename, }; export const Plain = ({ diff --git a/ui/components/ui/circle-icon/circle-icon.stories.js b/ui/components/ui/circle-icon/circle-icon.stories.js index 45b95dbd7..786efaa1a 100644 --- a/ui/components/ui/circle-icon/circle-icon.stories.js +++ b/ui/components/ui/circle-icon/circle-icon.stories.js @@ -3,6 +3,7 @@ import CircleIcon from './circle-icon.component'; export default { title: 'CircleIcon', + id: __filename, }; export const basicCircleIcon = () => ( diff --git a/ui/components/ui/color-indicator/color-indicator.stories.js b/ui/components/ui/color-indicator/color-indicator.stories.js index 81ab5f260..828d23570 100644 --- a/ui/components/ui/color-indicator/color-indicator.stories.js +++ b/ui/components/ui/color-indicator/color-indicator.stories.js @@ -5,6 +5,7 @@ import ColorIndicator from './color-indicator'; export default { title: 'ColorIndicator', + id: __filename, }; export const colorIndicator = () => ( diff --git a/ui/components/ui/currency-display/currency-display.component.js b/ui/components/ui/currency-display/currency-display.component.js index 69f884c1c..8d8581ed0 100644 --- a/ui/components/ui/currency-display/currency-display.component.js +++ b/ui/components/ui/currency-display/currency-display.component.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { GWEI } from '../../../helpers/constants/common'; +import { ETH, GWEI } from '../../../helpers/constants/common'; import { useCurrencyDisplay } from '../../../hooks/useCurrencyDisplay'; export default function CurrencyDisplay({ @@ -53,7 +53,7 @@ CurrencyDisplay.propTypes = { 'className': PropTypes.string, 'currency': PropTypes.string, 'data-testid': PropTypes.string, - 'denomination': PropTypes.oneOf([GWEI]), + 'denomination': PropTypes.oneOf([GWEI, ETH]), 'displayValue': PropTypes.string, 'hideLabel': PropTypes.bool, 'hideTitle': PropTypes.bool, diff --git a/ui/components/ui/definition-list/definition-list.stories.js b/ui/components/ui/definition-list/definition-list.stories.js index 9760d159e..36bf5a6cf 100644 --- a/ui/components/ui/definition-list/definition-list.stories.js +++ b/ui/components/ui/definition-list/definition-list.stories.js @@ -9,6 +9,7 @@ import DefinitionList from './definition-list'; export default { title: 'Definition List', + id: __filename, }; const basic = { diff --git a/ui/components/ui/dropdown/dropdown.stories.js b/ui/components/ui/dropdown/dropdown.stories.js index d12354e20..4eb0430eb 100644 --- a/ui/components/ui/dropdown/dropdown.stories.js +++ b/ui/components/ui/dropdown/dropdown.stories.js @@ -5,6 +5,7 @@ import Dropdown from '.'; export default { title: 'Dropdown', + id: __filename, }; const unnamedOptions = [...Array(10).keys()].map((index) => { diff --git a/ui/components/ui/error-message/error-message.stories.js b/ui/components/ui/error-message/error-message.stories.js index 64114ea0f..c803ae7b3 100644 --- a/ui/components/ui/error-message/error-message.stories.js +++ b/ui/components/ui/error-message/error-message.stories.js @@ -4,6 +4,7 @@ import ErrorMessage from '.'; export default { title: 'ErrorMessage', + id: __filename, }; export const primaryType = () => ( diff --git a/ui/components/ui/form-field/form-field.stories.js b/ui/components/ui/form-field/form-field.stories.js index b78c908fe..d555690eb 100644 --- a/ui/components/ui/form-field/form-field.stories.js +++ b/ui/components/ui/form-field/form-field.stories.js @@ -6,6 +6,7 @@ import FormField from '.'; export default { title: 'FormField', + id: __filename, }; export const Plain = ({ ...props }) => { diff --git a/ui/components/ui/icon/icon.stories.js b/ui/components/ui/icon/icon.stories.js index 6f2e35ddf..104dc1c0f 100644 --- a/ui/components/ui/icon/icon.stories.js +++ b/ui/components/ui/icon/icon.stories.js @@ -12,6 +12,7 @@ import InfoIconInverted from './info-icon-inverted.component'; export default { title: 'Icon', + id: __filename, }; export const copy = () => ( diff --git a/ui/components/ui/identicon/identicon.component.js b/ui/components/ui/identicon/identicon.component.js index dd6eb5beb..8215690d0 100644 --- a/ui/components/ui/identicon/identicon.component.js +++ b/ui/components/ui/identicon/identicon.component.js @@ -1,9 +1,6 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import contractMap from '@metamask/contract-metadata'; -import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; - import Jazzicon from '../jazzicon'; import BlockieIdenticon from './blockieIdenticon'; @@ -23,6 +20,8 @@ export default class Identicon extends PureComponent { useBlockie: PropTypes.bool, alt: PropTypes.string, imageBorder: PropTypes.bool, + useTokenDetection: PropTypes.bool, + tokenList: PropTypes.object, }; static defaultProps = { @@ -33,6 +32,7 @@ export default class Identicon extends PureComponent { image: undefined, useBlockie: false, alt: '', + tokenList: {}, }; renderImage() { @@ -51,8 +51,14 @@ export default class Identicon extends PureComponent { } renderJazzicon() { - const { address, className, diameter, alt } = this.props; - + const { + address, + className, + diameter, + alt, + useTokenDetection, + tokenList, + } = this.props; return ( ); } @@ -78,16 +86,25 @@ export default class Identicon extends PureComponent { } render() { - const { address, image, useBlockie, addBorder, diameter } = this.props; - + const { + address, + image, + useBlockie, + addBorder, + diameter, + useTokenDetection, + tokenList, + } = this.props; if (image) { return this.renderImage(); } if (address) { - const checksummedAddress = toChecksumHexAddress(address); - - if (checksummedAddress && contractMap[checksummedAddress]?.logo) { + // token from dynamic api list is fetched when useTokenDetection is true + // And since the token.address from allTokens is checksumaddress + // tokenAddress have to be changed to lowercase when we are using dynamic list + const tokenAddress = useTokenDetection ? address.toLowerCase() : address; + if (tokenAddress && tokenList[tokenAddress]?.iconUrl) { return this.renderJazzicon(); } diff --git a/ui/components/ui/identicon/identicon.container.js b/ui/components/ui/identicon/identicon.container.js index 2ed017abb..b000eecfe 100644 --- a/ui/components/ui/identicon/identicon.container.js +++ b/ui/components/ui/identicon/identicon.container.js @@ -3,11 +3,13 @@ import Identicon from './identicon.component'; const mapStateToProps = (state) => { const { - metamask: { useBlockie }, + metamask: { useBlockie, useTokenDetection, tokenList }, } = state; return { useBlockie, + useTokenDetection, + tokenList, }; }; diff --git a/ui/components/ui/identicon/identicon.stories.js b/ui/components/ui/identicon/identicon.stories.js index a71928bfd..b7103ff0f 100644 --- a/ui/components/ui/identicon/identicon.stories.js +++ b/ui/components/ui/identicon/identicon.stories.js @@ -2,7 +2,10 @@ import React from 'react'; import { text, boolean, number } from '@storybook/addon-knobs'; import Identicon from './identicon.component'; -export default { title: 'Identicon' }; +export default { + title: 'Identicon', + id: __filename, +}; const diameterOptions = { range: true, diff --git a/ui/components/ui/info-tooltip/info-tooltip.stories.js b/ui/components/ui/info-tooltip/info-tooltip.stories.js index 77756f9b3..22044191f 100644 --- a/ui/components/ui/info-tooltip/info-tooltip.stories.js +++ b/ui/components/ui/info-tooltip/info-tooltip.stories.js @@ -4,6 +4,7 @@ import InfoTooltip from './info-tooltip'; export default { title: 'InfoTooltip', + id: __filename, }; export const Top = () => ( diff --git a/ui/components/ui/jazzicon/jazzicon.component.js b/ui/components/ui/jazzicon/jazzicon.component.js index b0a35cb6f..f5261f832 100644 --- a/ui/components/ui/jazzicon/jazzicon.component.js +++ b/ui/components/ui/jazzicon/jazzicon.component.js @@ -15,6 +15,8 @@ export default class Jazzicon extends PureComponent { className: PropTypes.string, diameter: PropTypes.number, style: PropTypes.object, + useTokenDetection: PropTypes.bool, + tokenList: PropTypes.object, }; static defaultProps = { @@ -46,8 +48,13 @@ export default class Jazzicon extends PureComponent { } appendJazzicon() { - const { address, diameter } = this.props; - const image = iconFactory.iconForAddress(address, diameter); + const { address, diameter, useTokenDetection, tokenList } = this.props; + const image = iconFactory.iconForAddress( + address, + diameter, + useTokenDetection, + tokenList, + ); this.container.current.appendChild(image); } diff --git a/ui/components/ui/list-item/index.scss b/ui/components/ui/list-item/index.scss index dc58b90b5..ae2a04b80 100644 --- a/ui/components/ui/list-item/index.scss +++ b/ui/components/ui/list-item/index.scss @@ -97,13 +97,13 @@ white-space: nowrap; } - @media (max-width: 575px) { + @media (max-width: $break-small) { &__mid-content { display: none; } } - @media (min-width: 576px) { + @media (min-width: $break-large) { grid-template-areas: 'icon head head head head mid mid mid mid right right right' 'icon sub sub sub sub mid mid mid mid right right right' @@ -115,7 +115,7 @@ grid-template-areas: 'icon head head head head head head head right right right right'; align-items: center; - @media (min-width: 576px) { + @media (min-width: $break-large) { grid-template-areas: 'icon head head head head mid mid mid mid right right right'; } } diff --git a/ui/components/ui/list-item/list-item.stories.js b/ui/components/ui/list-item/list-item.stories.js index 93054a267..95b0746b3 100644 --- a/ui/components/ui/list-item/list-item.stories.js +++ b/ui/components/ui/list-item/list-item.stories.js @@ -11,6 +11,7 @@ import ListItem from './list-item.component'; export default { title: 'ListItem', + id: __filename, }; function Currencies({ primary, secondary }) { diff --git a/ui/components/ui/mascot/mascot.component.js b/ui/components/ui/mascot/mascot.component.js index 2002f15e6..ee6e03096 100644 --- a/ui/components/ui/mascot/mascot.component.js +++ b/ui/components/ui/mascot/mascot.component.js @@ -3,6 +3,8 @@ import React, { createRef, Component } from 'react'; import MetaMaskLogo from '@metamask/logo'; import { debounce } from 'lodash'; +import { getBuildSpecificAsset } from '../../../helpers/utils/build-types'; + const directionTargetGenerator = ({ top, left, height, width }) => { const horizontalMiddle = left + width / 2; const verticalMiddle = top + height / 2; @@ -43,6 +45,7 @@ export default class Mascot extends Component { pxNotRatio: true, width, height, + meshJson: getBuildSpecificAsset('foxMeshJson'), }); this.mascotContainer = createRef(); diff --git a/ui/components/ui/mascot/mascot.stories.js b/ui/components/ui/mascot/mascot.stories.js index aae108b05..5282e68a7 100644 --- a/ui/components/ui/mascot/mascot.stories.js +++ b/ui/components/ui/mascot/mascot.stories.js @@ -22,6 +22,7 @@ const buttonStyle = { export default { title: 'Mascot', + id: __filename, }; export function Demo() { diff --git a/ui/components/ui/menu/menu.stories.js b/ui/components/ui/menu/menu.stories.js index e067e29d4..73e0291eb 100644 --- a/ui/components/ui/menu/menu.stories.js +++ b/ui/components/ui/menu/menu.stories.js @@ -4,6 +4,7 @@ import { Menu, MenuItem } from '.'; export default { title: 'Menu', + id: __filename, }; export const Basic = () => { diff --git a/ui/components/ui/metafox-logo/metafox-logo.component.js b/ui/components/ui/metafox-logo/metafox-logo.component.js index 2b0b36c87..cdebd67c0 100644 --- a/ui/components/ui/metafox-logo/metafox-logo.component.js +++ b/ui/components/ui/metafox-logo/metafox-logo.component.js @@ -1,19 +1,22 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; +import { getBuildSpecificAsset } from '../../../helpers/utils/build-types'; export default class MetaFoxLogo extends PureComponent { static propTypes = { onClick: PropTypes.func, unsetIconHeight: PropTypes.bool, + useDark: PropTypes.bool, }; static defaultProps = { onClick: undefined, + useDark: false, }; render() { - const { onClick, unsetIconHeight } = this.props; + const { onClick, unsetIconHeight, useDark } = this.props; const iconProps = unsetIconHeight ? {} : { height: 42, width: 42 }; return ( @@ -25,7 +28,11 @@ export default class MetaFoxLogo extends PureComponent { > console.log('changed value: ', e.target.value); diff --git a/ui/components/ui/page-container/index.scss b/ui/components/ui/page-container/index.scss index df4f9067a..27ce13d5f 100644 --- a/ui/components/ui/page-container/index.scss +++ b/ui/components/ui/page-container/index.scss @@ -184,7 +184,7 @@ } } -@media screen and (max-width: 575px) { +@media screen and (max-width: $break-small) { .page-container { height: 100%; width: 100%; @@ -195,7 +195,7 @@ } } -@media screen and (min-width: 576px) { +@media screen and (min-width: $break-large) { .page-container { max-height: 82vh; min-height: 570px; diff --git a/ui/components/ui/page-container/page-container.component.js b/ui/components/ui/page-container/page-container.component.js index 3cefb2273..28b5f75b0 100644 --- a/ui/components/ui/page-container/page-container.component.js +++ b/ui/components/ui/page-container/page-container.component.js @@ -5,6 +5,10 @@ import PageContainerHeader from './page-container-header'; import PageContainerFooter from './page-container-footer'; export default class PageContainer extends PureComponent { + static contextTypes = { + t: PropTypes.func, + }; + static propTypes = { // PageContainerHeader props backButtonString: PropTypes.string, @@ -86,6 +90,19 @@ export default class PageContainer extends PureComponent { return null; } + getTabSubmitText() { + const { tabsComponent } = this.props; + const { activeTabIndex } = this.state; + if (tabsComponent) { + let { children } = tabsComponent.props; + children = children.filter(Boolean); + if (children[activeTabIndex]?.key === 'custom-tab') { + return this.context.t('addCustomToken'); + } + } + return null; + } + render() { const { title, @@ -103,7 +120,7 @@ export default class PageContainer extends PureComponent { headerCloseText, hideCancel, } = this.props; - + const tabSubmitText = this.getTabSubmitText(); return (
    diff --git a/ui/components/ui/popover/popover.stories.js b/ui/components/ui/popover/popover.stories.js index e7029d17f..960f9c994 100644 --- a/ui/components/ui/popover/popover.stories.js +++ b/ui/components/ui/popover/popover.stories.js @@ -16,6 +16,7 @@ const mainWrapperStyle = { export default { title: 'Popover', + id: __filename, }; export const approve = () => ( diff --git a/ui/components/ui/pulse-loader/pulse-loader.stories.js b/ui/components/ui/pulse-loader/pulse-loader.stories.js index a3878d27d..122658d50 100644 --- a/ui/components/ui/pulse-loader/pulse-loader.stories.js +++ b/ui/components/ui/pulse-loader/pulse-loader.stories.js @@ -3,6 +3,7 @@ import PulseLoader from '.'; export default { title: 'PulseLoader', + id: __filename, }; export const common = () => ; diff --git a/ui/components/ui/radio-group/index.scss b/ui/components/ui/radio-group/index.scss index 3c9685920..893ef09f4 100644 --- a/ui/components/ui/radio-group/index.scss +++ b/ui/components/ui/radio-group/index.scss @@ -1,8 +1,12 @@ .radio-group { display: grid; - grid-template-columns: repeat(3, 1fr); - grid-template-rows: 100px; - width: 300px; + grid-template-columns: 33% 33% 33%; + grid-template-rows: 60px; + width: 100%; + + &--has-recommendation { + grid-template-rows: 100px; + } label { cursor: pointer; @@ -16,33 +20,51 @@ height: 20px; } - &__column-line { + &__column-inner { + display: flex; + flex-direction: column; + align-items: center; + } + + &__column-line, + &__column-horizontal-line, + &__column-radio, + &__column-label { + flex: 1 1 auto; + } + + &__column-start-connector { + width: calc(50% + 0.5px); + height: 6px; + border-left: 1px solid $ui-2; + border-bottom: 1px solid $ui-2; + align-self: flex-end; + } + + &__column-end-connector { + width: calc(50% + 0.5px); + height: 6px; + border-right: 1px solid $ui-2; + border-bottom: 1px solid $ui-2; + align-self: flex-start; + } + + &__column-vertical-line { width: 1px; height: 5px; - background-color: $ui-2; - margin: 0 auto; + border-left: 1px solid $ui-2; } &__column-horizontal-line { height: 1px; - background-color: $ui-2; + border-bottom: 1px solid $ui-2; width: 100%; } - &__column:first-child &__column-horizontal-line { - width: 50px; - margin-left: 50px; - } - - &__column:last-child &__column-horizontal-line { - width: 51px; - } - &__column-radio { - margin-inline-end: 1px; - input { cursor: pointer; + margin: 0; } } diff --git a/ui/components/ui/radio-group/radio-group.component.js b/ui/components/ui/radio-group/radio-group.component.js index e1c2f27e8..7bc0dae6b 100644 --- a/ui/components/ui/radio-group/radio-group.component.js +++ b/ui/components/ui/radio-group/radio-group.component.js @@ -1,5 +1,6 @@ import React, { useContext } from 'react'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; import { I18nContext } from '../../../contexts/i18n'; import Typography from '../typography/typography'; import { @@ -8,24 +9,52 @@ import { TYPOGRAPHY, } from '../../../helpers/constants/design-system'; +function Connector({ isFirst, isLast }) { + if (isFirst) { + return
    ; + } else if (isLast) { + return
    ; + } + return ( + <> +
    +
    + + ); +} + +Connector.propTypes = { + isFirst: PropTypes.boolean, + isLast: PropTypes.boolean, +}; + export default function RadioGroup({ options, name, selectedValue, onChange }) { const t = useContext(I18nContext); + const hasRecommendation = Boolean( + options.find((option) => option.recommended), + ); + return ( -
    - {options.map((option) => { +
    + {options.map((option, index) => { const checked = option.value === selectedValue; return (
    -
    diff --git a/ui/css/design-system/attributes.scss b/ui/css/design-system/attributes.scss index 72fdcb51f..d2836769b 100644 --- a/ui/css/design-system/attributes.scss +++ b/ui/css/design-system/attributes.scss @@ -70,3 +70,4 @@ $directions: top, right, bottom, left; $display: block, grid, flex, inline-block, inline-grid, inline-flex, list-item; $text-align: left, right, center, justify, end; $font-weight: bold, normal, 100, 200, 300, 400, 500, 600, 700, 800, 900; +$font-style: normal, italic, oblique; diff --git a/ui/css/itcss/components/network.scss b/ui/css/itcss/components/network.scss index 043626ff8..31137ac43 100644 --- a/ui/css/itcss/components/network.scss +++ b/ui/css/itcss/components/network.scss @@ -57,7 +57,7 @@ margin: 0 14px; } - @media screen and (min-width: 576px) { + @media screen and (min-width: $break-large) { right: calc(((100% - 85vw) / 2) + 2px); } diff --git a/ui/css/itcss/components/newui-sections.scss b/ui/css/itcss/components/newui-sections.scss index 51f485600..398651cc7 100644 --- a/ui/css/itcss/components/newui-sections.scss +++ b/ui/css/itcss/components/newui-sections.scss @@ -50,7 +50,7 @@ $sub-mid-size-breakpoint-range: "screen and (min-width: #{$break-large}) and (ma // main-container media queries -@media screen and (min-width: 576px) { +@media screen and (min-width: $break-large) { .lap-visible { display: flex; } @@ -83,7 +83,7 @@ $sub-mid-size-breakpoint-range: "screen and (min-width: #{$break-large}) and (ma } } -@media screen and (max-width: 575px) { +@media screen and (max-width: $break-small) { .lap-visible { display: none; } @@ -117,11 +117,11 @@ $sub-mid-size-breakpoint-range: "screen and (min-width: #{$break-large}) and (ma flex-direction: row-reverse; justify-content: space-between; - @media screen and (max-width: 575px) { + @media screen and (max-width: $break-small) { height: 100%; } - @media screen and (min-width: 576px) { + @media screen and (min-width: $break-large) { width: 85vw; } diff --git a/ui/ducks/app/app.js b/ui/ducks/app/app.js index 93a21a16f..15d32f32a 100644 --- a/ui/ducks/app/app.js +++ b/ui/ducks/app/app.js @@ -18,12 +18,6 @@ export default function reduceApp(state = {}, action) { name: null, }, }, - sidebar: { - isOpen: false, - transitionName: '', - type: '', - props: {}, - }, alertOpen: false, alertMessage: null, qrCodeData: null, @@ -71,25 +65,6 @@ export default function reduceApp(state = {}, action) { networkDropdownOpen: false, }; - // sidebar methods - case actionConstants.SIDEBAR_OPEN: - return { - ...appState, - sidebar: { - ...action.value, - isOpen: true, - }, - }; - - case actionConstants.SIDEBAR_CLOSE: - return { - ...appState, - sidebar: { - ...appState.sidebar, - isOpen: false, - }, - }; - // alert methods case actionConstants.ALERT_OPEN: return { diff --git a/ui/ducks/app/app.test.js b/ui/ducks/app/app.test.js index 6f536c903..e88ac1b9b 100644 --- a/ui/ducks/app/app.test.js +++ b/ui/ducks/app/app.test.js @@ -38,31 +38,6 @@ describe('App State', () => { expect(newState.networkDropdownOpen).toStrictEqual(false); }); - it('opens sidebar', () => { - const value = { - transitionName: 'sidebar-right', - type: 'wallet-view', - isOpen: true, - }; - const state = reduceApp(metamaskState, { - type: actions.SIDEBAR_OPEN, - value, - }); - - expect(state.sidebar).toStrictEqual(value); - }); - - it('closes sidebar', () => { - const openSidebar = { sidebar: { isOpen: true } }; - const state = { ...metamaskState, ...openSidebar }; - - const newState = reduceApp(state, { - type: actions.SIDEBAR_CLOSE, - }); - - expect(newState.sidebar.isOpen).toStrictEqual(false); - }); - it('opens alert', () => { const state = reduceApp(metamaskState, { type: actions.ALERT_OPEN, diff --git a/ui/ducks/confirm-transaction/confirm-transaction.duck.js b/ui/ducks/confirm-transaction/confirm-transaction.duck.js index d19d1111b..0879e53d6 100644 --- a/ui/ducks/confirm-transaction/confirm-transaction.duck.js +++ b/ui/ducks/confirm-transaction/confirm-transaction.duck.js @@ -17,6 +17,7 @@ import { getTokenData, sumHexes } from '../../helpers/utils/transactions.util'; import { conversionUtil } from '../../../shared/modules/conversion.utils'; import { getAveragePriceEstimateInHexWEI } from '../../selectors/custom-gas'; +import { isEqualCaseInsensitive } from '../../helpers/utils/util'; // Actions const createActionType = (action) => `metamask/confirm-transaction/${action}`; @@ -283,8 +284,8 @@ export function setTransactionToConfirm(transactionId) { const tokenData = getTokenData(data); const tokens = getTokens(state); - const currentToken = tokens?.find( - ({ address }) => tokenAddress === address, + const currentToken = tokens?.find(({ address }) => + isEqualCaseInsensitive(tokenAddress, address), ); dispatch( diff --git a/ui/ducks/metamask/metamask.js b/ui/ducks/metamask/metamask.js index b2bef9558..d78726a6a 100644 --- a/ui/ducks/metamask/metamask.js +++ b/ui/ducks/metamask/metamask.js @@ -22,7 +22,6 @@ export default function reduceMetamask(state = {}, action) { frequentRpcList: [], addressBook: [], contractExchangeRates: {}, - tokens: [], pendingTokens: {}, customNonceValue: '', useBlockie: false, @@ -89,12 +88,6 @@ export default function reduceMetamask(state = {}, action) { return Object.assign(metamaskState, { identities }); } - case actionConstants.UPDATE_TOKENS: - return { - ...metamaskState, - tokens: action.newTokens, - }; - case actionConstants.UPDATE_CUSTOM_NONCE: return { ...metamaskState, diff --git a/ui/ducks/metamask/metamask.test.js b/ui/ducks/metamask/metamask.test.js index 702c7c319..51c88033d 100644 --- a/ui/ducks/metamask/metamask.test.js +++ b/ui/ducks/metamask/metamask.test.js @@ -174,24 +174,6 @@ describe('MetaMask Reducers', () => { }); }); - it('updates tokens', () => { - const newTokens = { - address: '0x617b3f8050a0bd94b6b1da02b4384ee5b4df13f4', - decimals: 18, - symbol: 'META', - }; - - const state = reduceMetamask( - {}, - { - type: actionConstants.UPDATE_TOKENS, - newTokens, - }, - ); - - expect(state.tokens).toStrictEqual(newTokens); - }); - it('toggles account menu', () => { const state = reduceMetamask( {}, diff --git a/ui/ducks/send/send.js b/ui/ducks/send/send.js index 6ee102b69..3b5b8e0f3 100644 --- a/ui/ducks/send/send.js +++ b/ui/ducks/send/send.js @@ -1,8 +1,7 @@ import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import abi from 'human-standard-token-abi'; -import contractMap from '@metamask/contract-metadata'; import BigNumber from 'bignumber.js'; -import { addHexPrefix, toChecksumAddress } from 'ethereumjs-util'; +import { addHexPrefix } from 'ethereumjs-util'; import { debounce } from 'lodash'; import { conversionGreaterThan, @@ -39,6 +38,8 @@ import { getTargetAccount, getIsNonStandardEthChain, checkNetworkAndAccountSupports1559, + getUseTokenDetection, + getTokenList, } from '../../selectors'; import { disconnectGasFeeEstimatePoller, @@ -71,6 +72,7 @@ import { isDefaultMetaMaskChain, isOriginContractAddress, isValidDomainName, + isEqualCaseInsensitive, } from '../../helpers/utils/util'; import { getGasEstimateType, @@ -517,6 +519,8 @@ export const initializeSendState = createAsyncThunk( gasTotal: addHexPrefix(calcGasTotal(gasLimit, gasPrice)), gasEstimatePollToken, eip1559support, + useTokenDetection: getUseTokenDetection(state), + tokenAddressList: Object.keys(getTokenList(state)), }; }, ); @@ -986,7 +990,7 @@ const slice = createSlice({ recipient.warning = null; } else { const isSendingToken = asset.type === ASSET_TYPES.TOKEN; - const { chainId, tokens } = action.payload; + const { chainId, tokens, tokenAddressList } = action.payload; if ( isBurnAddress(recipient.userInput) || (!isValidHexAddress(recipient.userInput, { @@ -1005,11 +1009,12 @@ const slice = createSlice({ } else { recipient.error = null; } - if ( isSendingToken && isValidHexAddress(recipient.userInput) && - (toChecksumAddress(recipient.userInput) in contractMap || + (tokenAddressList.find((address) => + isEqualCaseInsensitive(address, recipient.userInput), + ) || checkExistingAddresses(recipient.userInput, tokens)) ) { recipient.warning = KNOWN_RECIPIENT_ADDRESS_WARNING; @@ -1210,6 +1215,8 @@ const slice = createSlice({ payload: { chainId: action.payload.chainId, tokens: action.payload.tokens, + useTokenDetection: action.payload.useTokenDetection, + tokenAddressList: action.payload.tokenAddressList, }, }); } @@ -1395,7 +1402,14 @@ export function updateRecipientUserInput(userInput) { const state = getState(); const chainId = getCurrentChainId(state); const tokens = getTokens(state); - debouncedValidateRecipientUserInput(dispatch, { chainId, tokens }); + const useTokenDetection = getUseTokenDetection(state); + const tokenAddressList = Object.keys(getTokenList(state)); + debouncedValidateRecipientUserInput(dispatch, { + chainId, + tokens, + useTokenDetection, + tokenAddressList, + }); }; } @@ -1722,6 +1736,10 @@ export function getSendHexData(state) { return state[name].draftTransaction.userInputHexData; } +export function getDraftTransactionID(state) { + return state[name].draftTransaction.id; +} + export function sendAmountIsInError(state) { return Boolean(state[name].amount.error); } diff --git a/ui/ducks/send/send.test.js b/ui/ducks/send/send.test.js index 69dbe8893..9e272b9b3 100644 --- a/ui/ducks/send/send.test.js +++ b/ui/ducks/send/send.test.js @@ -51,6 +51,7 @@ import sendReducer, { getSendAmount, getIsBalanceInsufficient, getSendMaxModeState, + getDraftTransactionID, sendAmountIsInError, getSendHexData, getSendTo, @@ -628,6 +629,8 @@ describe('Send Slice', () => { payload: { chainId: '', tokens: [], + useTokenDetection: true, + tokenAddressList: [], }, }; @@ -649,6 +652,8 @@ describe('Send Slice', () => { payload: { chainId: '0x55', tokens: [], + useTokenDetection: true, + tokenAddressList: [], }, }; @@ -671,6 +676,8 @@ describe('Send Slice', () => { payload: { chainId: '', tokens: [], + useTokenDetection: true, + tokenAddressList: [], }, }; @@ -698,6 +705,8 @@ describe('Send Slice', () => { payload: { chainId: '0x4', tokens: [], + useTokenDetection: true, + tokenAddressList: ['0x514910771af9ca656af840dff83e8264ecf986ca'], }, }; @@ -1111,6 +1120,32 @@ describe('Send Slice', () => { provider: { chainId: '0x4', }, + useTokenDetection: true, + tokenList: { + 0x514910771af9ca656af840dff83e8264ecf986ca: { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + symbol: 'LINK', + decimals: 18, + name: 'Chainlink', + iconUrl: + 'https://s3.amazonaws.com/airswap-token-images/LINK.png', + aggregators: [ + 'airswapLight', + 'bancor', + 'cmc', + 'coinGecko', + 'kleros', + 'oneInch', + 'paraswap', + 'pmm', + 'totle', + 'zapper', + 'zerion', + 'zeroEx', + ], + occurrences: 12, + }, + }, }, send: initialState, gas: { @@ -1484,6 +1519,31 @@ describe('Send Slice', () => { chainId: '', }, tokens: [], + useTokenDetection: true, + tokenList: { + '0x514910771af9ca656af840dff83e8264ecf986ca': { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + symbol: 'LINK', + decimals: 18, + name: 'Chainlink', + iconUrl: 'https://s3.amazonaws.com/airswap-token-images/LINK.png', + aggregators: [ + 'airswapLight', + 'bancor', + 'cmc', + 'coinGecko', + 'kleros', + 'oneInch', + 'paraswap', + 'pmm', + 'totle', + 'zapper', + 'zerion', + 'zeroEx', + ], + occurrences: 12, + }, + }, }, }; @@ -1512,6 +1572,8 @@ describe('Send Slice', () => { expect(store.getActions()[1].payload).toStrictEqual({ chainId: '', tokens: [], + useTokenDetection: true, + tokenAddressList: ['0x514910771af9ca656af840dff83e8264ecf986ca'], }); }); }); @@ -1736,6 +1798,32 @@ describe('Send Slice', () => { chainId: '', }, tokens: [], + useTokenDetection: true, + tokenList: { + 0x514910771af9ca656af840dff83e8264ecf986ca: { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + symbol: 'LINK', + decimals: 18, + name: 'Chainlink', + iconUrl: + 'https://s3.amazonaws.com/airswap-token-images/LINK.png', + aggregators: [ + 'airswapLight', + 'bancor', + 'cmc', + 'coinGecko', + 'kleros', + 'oneInch', + 'paraswap', + 'pmm', + 'totle', + 'zapper', + 'zerion', + 'zeroEx', + ], + occurrences: 12, + }, + }, }, send: { asset: { @@ -2372,6 +2460,21 @@ describe('Send Slice', () => { ).toBe(true); }); + it('has a selector to get the draft transaction ID', () => { + expect(getDraftTransactionID({ send: initialState })).toBeNull(); + expect( + getDraftTransactionID({ + send: { + ...initialState, + draftTransaction: { + ...initialState.draftTransaction, + id: 'ID', + }, + }, + }), + ).toBe('ID'); + }); + it('has a selector to get the user entered hex data', () => { expect(getSendHexData({ send: initialState })).toBeNull(); expect( diff --git a/ui/ducks/swaps/swaps.js b/ui/ducks/swaps/swaps.js index d12d3c4ff..4acfcad50 100644 --- a/ui/ducks/swaps/swaps.js +++ b/ui/ducks/swaps/swaps.js @@ -86,6 +86,7 @@ const initialState = { fetchingQuotes: false, fromToken: null, quotesFetchStartTime: null, + reviewSwapClickedTimestamp: null, topAssets: {}, toToken: null, customGas: { @@ -130,6 +131,9 @@ const slice = createSlice({ setQuotesFetchStartTime: (state, action) => { state.quotesFetchStartTime = action.payload; }, + setReviewSwapClickedTimestamp: (state, action) => { + state.reviewSwapClickedTimestamp = action.payload; + }, setTopAssets: (state, action) => { state.topAssets = action.payload; }, @@ -183,6 +187,9 @@ export const getFetchingQuotes = (state) => state.swaps.fetchingQuotes; export const getQuotesFetchStartTime = (state) => state.swaps.quotesFetchStartTime; +export const getReviewSwapClickedTimestamp = (state) => + state.swaps.reviewSwapClickedTimestamp; + export const getSwapsCustomizationModalPrice = (state) => state.swaps.customGas.price; @@ -236,6 +243,9 @@ export const getUseNewSwapsApi = (state) => export const getSwapsQuoteRefreshTime = (state) => state.metamask.swapsState.swapsQuoteRefreshTime; +export const getSwapsQuotePrefetchingRefreshTime = (state) => + state.metamask.swapsState.swapsQuotePrefetchingRefreshTime; + export const getBackgroundSwapRouteState = (state) => state.metamask.swapsState.routeState; @@ -323,6 +333,7 @@ const { setFetchingQuotes, setFromToken, setQuotesFetchStartTime, + setReviewSwapClickedTimestamp, setTopAssets, setToToken, swapCustomGasModalPriceEdited, @@ -338,6 +349,7 @@ export { setFetchingQuotes, setFromToken as setSwapsFromToken, setQuotesFetchStartTime as setSwapQuotesFetchStartTime, + setReviewSwapClickedTimestamp, setTopAssets, setToToken as setSwapToToken, swapCustomGasModalPriceEdited, @@ -412,6 +424,7 @@ export const fetchQuotesAndSetQuoteState = ( inputValue, maxSlippage, metaMetricsEvent, + pageRedirectionDisabled, ) => { return async (dispatch, getState) => { const state = getState(); @@ -461,8 +474,12 @@ export const fetchQuotesAndSetQuoteState = ( decimals: toTokenDecimals, iconUrl: toTokenIconUrl, } = selectedToToken; - await dispatch(setBackgroundSwapRouteState('loading')); - history.push(LOADING_QUOTES_ROUTE); + // pageRedirectionDisabled is true if quotes prefetching is active (a user is on the Build Quote page). + // In that case we just want to silently prefetch quotes without redirecting to the quotes loading page. + if (!pageRedirectionDisabled) { + await dispatch(setBackgroundSwapRouteState('loading')); + history.push(LOADING_QUOTES_ROUTE); + } dispatch(setFetchingQuotes(true)); const contractExchangeRates = getTokenExchangeRates(state); @@ -779,7 +796,7 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => { sensitiveProperties: swapMetaData, }); - if (!isContractAddressValid(usedTradeTxParams.to, swapMetaData, chainId)) { + if (!isContractAddressValid(usedTradeTxParams.to, chainId)) { captureMessage('Invalid contract address', { extra: { token_from: swapMetaData.token_from, diff --git a/ui/helpers/constants/design-system.js b/ui/helpers/constants/design-system.js index 38e85f779..9716b5567 100644 --- a/ui/helpers/constants/design-system.js +++ b/ui/helpers/constants/design-system.js @@ -10,6 +10,7 @@ export const COLORS = { UI3: 'ui-3', UI4: 'ui-4', BLACK: 'black', + GRAY: 'gray', WHITE: 'white', PRIMARY1: 'primary-1', PRIMARY2: 'primary-2', @@ -156,6 +157,12 @@ export const FONT_WEIGHT = { 900: 900, }; +export const FONT_STYLE = { + ITALIC: 'italic', + NORMAL: 'normal', + OBLIQUE: 'oblique', +}; + export const SEVERITIES = { DANGER: 'danger', WARNING: 'warning', diff --git a/ui/helpers/constants/routes.js b/ui/helpers/constants/routes.js index 781f51063..800a4badb 100644 --- a/ui/helpers/constants/routes.js +++ b/ui/helpers/constants/routes.js @@ -5,6 +5,7 @@ const ASSET_ROUTE = '/asset'; const SETTINGS_ROUTE = '/settings'; const GENERAL_ROUTE = '/settings/general'; const ADVANCED_ROUTE = '/settings/advanced'; +const EXPERIMENTAL_ROUTE = '/settings/experimental'; const SECURITY_ROUTE = '/settings/security'; const ABOUT_US_ROUTE = '/settings/about-us'; const ALERTS_ROUTE = '/settings/alerts'; @@ -17,8 +18,8 @@ const CONTACT_VIEW_ROUTE = '/settings/contact-list/view-contact'; const REVEAL_SEED_ROUTE = '/seed'; const MOBILE_SYNC_ROUTE = '/mobile-sync'; const RESTORE_VAULT_ROUTE = '/restore-vault'; -const ADD_TOKEN_ROUTE = '/add-token'; -const CONFIRM_ADD_TOKEN_ROUTE = '/confirm-add-token'; +const IMPORT_TOKEN_ROUTE = '/import-token'; +const CONFIRM_IMPORT_TOKEN_ROUTE = '/confirm-import-token'; const CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE = '/confirm-add-suggested-token'; const NEW_ACCOUNT_ROUTE = '/new-account'; const IMPORT_ACCOUNT_ROUTE = '/new-account/import'; @@ -73,6 +74,7 @@ const PATH_NAME_MAP = { [SETTINGS_ROUTE]: 'Settings Page', [GENERAL_ROUTE]: 'General Settings Page', [ADVANCED_ROUTE]: 'Advanced Settings Page', + [EXPERIMENTAL_ROUTE]: 'Experimental Settings Page', [SECURITY_ROUTE]: 'Security Settings Page', [ABOUT_US_ROUTE]: 'About Us Page', [ALERTS_ROUTE]: 'Alerts Settings Page', @@ -85,8 +87,8 @@ const PATH_NAME_MAP = { [REVEAL_SEED_ROUTE]: 'Reveal Secret Recovery Phrase Page', [MOBILE_SYNC_ROUTE]: 'Sync With Mobile Page', [RESTORE_VAULT_ROUTE]: 'Restore Vault Page', - [ADD_TOKEN_ROUTE]: 'Add Token Page', - [CONFIRM_ADD_TOKEN_ROUTE]: 'Confirm Add Token Page', + [IMPORT_TOKEN_ROUTE]: 'Import Token Page', + [CONFIRM_IMPORT_TOKEN_ROUTE]: 'Confirm Import Token Page', [CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE]: 'Confirm Add Suggested Token Page', [NEW_ACCOUNT_ROUTE]: 'New Account Page', [IMPORT_ACCOUNT_ROUTE]: 'Import Account Page', @@ -143,8 +145,8 @@ export { REVEAL_SEED_ROUTE, MOBILE_SYNC_ROUTE, RESTORE_VAULT_ROUTE, - ADD_TOKEN_ROUTE, - CONFIRM_ADD_TOKEN_ROUTE, + IMPORT_TOKEN_ROUTE, + CONFIRM_IMPORT_TOKEN_ROUTE, CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE, NEW_ACCOUNT_ROUTE, IMPORT_ACCOUNT_ROUTE, @@ -172,6 +174,7 @@ export { CONFIRMATION_V_NEXT_ROUTE, INITIALIZE_METAMETRICS_OPT_IN_ROUTE, ADVANCED_ROUTE, + EXPERIMENTAL_ROUTE, SECURITY_ROUTE, GENERAL_ROUTE, ABOUT_US_ROUTE, diff --git a/ui/helpers/utils/build-types.js b/ui/helpers/utils/build-types.js new file mode 100644 index 000000000..d6ec658f8 --- /dev/null +++ b/ui/helpers/utils/build-types.js @@ -0,0 +1,30 @@ +import betaJson from '../../../app/build-types/beta/beta-mascot.json'; + +const assetList = { + main: { + metafoxLogoHorizontalDark: '/images/logo/metamask-logo-horizontal.svg', + // Will use default provided by the @metamask/logo library + foxMeshJson: undefined, + }, + beta: { + metafoxLogoHorizontalDark: '/images/logo/metamask-logo-horizontal-dark.svg', + foxMeshJson: betaJson, + }, +}; + +export function isBeta() { + return process.env.METAMASK_BUILD_TYPE === 'beta'; +} + +// Returns a specific version of an asset based on +// the current metamask version (i.e. main, beta, etc.) +export function getBuildSpecificAsset(assetName) { + const buildType = process.env.METAMASK_BUILD_TYPE; + if (!assetList[buildType]?.[assetName]) { + console.warn( + `Cannot find asset for build ${buildType}: ${assetName}, returning main build asset`, + ); + return assetList.main[assetName]; + } + return assetList[buildType][assetName]; +} diff --git a/ui/helpers/utils/fetch-with-cache.test.js b/ui/helpers/utils/fetch-with-cache.test.js index 7244e1d14..76e7ebd01 100644 --- a/ui/helpers/utils/fetch-with-cache.test.js +++ b/ui/helpers/utils/fetch-with-cache.test.js @@ -79,7 +79,10 @@ describe('Fetch with cache', () => { {}, { timeout: 20 }, ), - ).rejects.toThrow({ name: 'AbortError', message: 'Aborted' }); + ).rejects.toThrow({ + name: 'AbortError', + message: 'The user aborted a request.', + }); }); it('throws when the response is unsuccessful', async () => { diff --git a/ui/helpers/utils/icon-factory.js b/ui/helpers/utils/icon-factory.js index 5189dcfce..f5a36b9f9 100644 --- a/ui/helpers/utils/icon-factory.js +++ b/ui/helpers/utils/icon-factory.js @@ -1,8 +1,4 @@ -import contractMap from '@metamask/contract-metadata'; -import { - isValidHexAddress, - toChecksumHexAddress, -} from '../../../shared/modules/hexstring-utils'; +import { isValidHexAddress } from '../../../shared/modules/hexstring-utils'; let iconFactory; @@ -18,11 +14,20 @@ function IconFactory(jazzicon) { this.cache = {}; } -IconFactory.prototype.iconForAddress = function (address, diameter) { - const addr = toChecksumHexAddress(address); - - if (iconExistsFor(addr)) { - return imageElFor(addr); +IconFactory.prototype.iconForAddress = function ( + address, + diameter, + useTokenDetection, + tokenList, +) { + // When useTokenDetection flag is true the tokenList contains tokens with non-checksum address from the dynamic token service api, + // When useTokenDetection flag is false the tokenList contains tokens with checksum addresses from contract-metadata. + // So the flag indicates whether the address of tokens currently on the tokenList is checksum or not. + // And since the token.address from allTokens is checksumaddress + // tokenAddress have to be changed to lowercase when we are using dynamic list + const addr = useTokenDetection ? address.toLowerCase() : address; + if (iconExistsFor(addr, tokenList)) { + return imageElFor(addr, useTokenDetection, tokenList); } return this.generateIdenticonSvg(address, diameter); @@ -49,18 +54,22 @@ IconFactory.prototype.generateNewIdenticon = function (address, diameter) { // util -function iconExistsFor(address) { +function iconExistsFor(address, tokenList) { return ( - contractMap[address] && + tokenList[address] && isValidHexAddress(address, { allowNonPrefixed: false }) && - contractMap[address].logo + tokenList[address].iconUrl ); } -function imageElFor(address) { - const contract = contractMap[address]; - const fileName = contract.logo; - const path = `images/contract/${fileName}`; +function imageElFor(address, useTokenDetection, tokenList) { + const tokenMetadata = tokenList[address]; + const fileName = tokenMetadata?.iconUrl; + // token from dynamic api list is fetched when useTokenDetection is true + // In the static list, the iconUrl will be holding only a filename for the image, + // the corresponding images will be available in the `images/contract/` location when the contract-metadata package was added to the extension + // so that it can be accessed using the filename in iconUrl. + const path = useTokenDetection ? fileName : `images/contract/${fileName}`; const img = document.createElement('img'); img.src = path; img.style.width = '100%'; diff --git a/ui/helpers/utils/is-mobile-view.js b/ui/helpers/utils/is-mobile-view.js index 310d49d6e..55bbba0ba 100644 --- a/ui/helpers/utils/is-mobile-view.js +++ b/ui/helpers/utils/is-mobile-view.js @@ -1,6 +1,6 @@ // Checks if viewport at invoke time fits mobile dimensions // isMobileView :: () => Bool const isMobileView = () => - window.matchMedia('screen and (max-width: 575px)').matches; + window.matchMedia('screen and (max-width: $break-small)').matches; export default isMobileView; diff --git a/ui/helpers/utils/token-util.js b/ui/helpers/utils/token-util.js index 46e3617c8..68431dde5 100644 --- a/ui/helpers/utils/token-util.js +++ b/ui/helpers/utils/token-util.js @@ -1,6 +1,5 @@ import log from 'loglevel'; import BigNumber from 'bignumber.js'; -import contractMap from '@metamask/contract-metadata'; import { conversionUtil, multiplyCurrencies, @@ -8,13 +7,6 @@ import { import * as util from './util'; import { formatCurrency } from './confirm-tx.util'; -const casedContractMap = Object.keys(contractMap).reduce((acc, base) => { - return { - ...acc, - [base.toLowerCase()]: contractMap[base], - }; -}, {}); - const DEFAULT_SYMBOL = ''; async function getSymbolFromContract(tokenAddress) { @@ -48,15 +40,21 @@ async function getDecimalsFromContract(tokenAddress) { } } -function getContractMetadata(tokenAddress) { - return tokenAddress && casedContractMap[tokenAddress.toLowerCase()]; +function getTokenMetadata(tokenAddress, tokenList) { + const casedTokenList = Object.keys(tokenList).reduce((acc, base) => { + return { + ...acc, + [base.toLowerCase()]: tokenList[base], + }; + }, {}); + return tokenAddress && casedTokenList[tokenAddress.toLowerCase()]; } -async function getSymbol(tokenAddress) { +async function getSymbol(tokenAddress, tokenList) { let symbol = await getSymbolFromContract(tokenAddress); if (!symbol) { - const contractMetadataInfo = getContractMetadata(tokenAddress); + const contractMetadataInfo = getTokenMetadata(tokenAddress, tokenList); if (contractMetadataInfo) { symbol = contractMetadataInfo.symbol; @@ -66,11 +64,11 @@ async function getSymbol(tokenAddress) { return symbol; } -async function getDecimals(tokenAddress) { +async function getDecimals(tokenAddress, tokenList) { let decimals = await getDecimalsFromContract(tokenAddress); if (!decimals || decimals === '0') { - const contractMetadataInfo = getContractMetadata(tokenAddress); + const contractMetadataInfo = getTokenMetadata(tokenAddress, tokenList); if (contractMetadataInfo) { decimals = contractMetadataInfo.decimals?.toString(); @@ -80,23 +78,12 @@ async function getDecimals(tokenAddress) { return decimals; } -export async function getSymbolAndDecimals(tokenAddress, existingTokens = []) { - const existingToken = existingTokens.find( - ({ address }) => tokenAddress === address, - ); - - if (existingToken) { - return { - symbol: existingToken.symbol, - decimals: existingToken.decimals, - }; - } - +export async function getSymbolAndDecimals(tokenAddress, tokenList) { let symbol, decimals; try { - symbol = await getSymbol(tokenAddress); - decimals = await getDecimals(tokenAddress); + symbol = await getSymbol(tokenAddress, tokenList); + decimals = await getDecimals(tokenAddress, tokenList); } catch (error) { log.warn( `symbol() and decimal() calls for token at address ${tokenAddress} resulted in error:`, @@ -113,12 +100,12 @@ export async function getSymbolAndDecimals(tokenAddress, existingTokens = []) { export function tokenInfoGetter() { const tokens = {}; - return async (address) => { + return async (address, tokenList) => { if (tokens[address]) { return tokens[address]; } - tokens[address] = await getSymbolAndDecimals(address); + tokens[address] = await getSymbolAndDecimals(address, tokenList); return tokens[address]; }; diff --git a/ui/helpers/utils/transactions.util.js b/ui/helpers/utils/transactions.util.js index 307e3dffc..b91e79ee9 100644 --- a/ui/helpers/utils/transactions.util.js +++ b/ui/helpers/utils/transactions.util.js @@ -196,9 +196,10 @@ export function getStatusKey(transaction) { * This will throw an error if the transaction category is unrecognized and no default is provided. * @param {function} t - The translation function * @param {TRANSACTION_TYPES[keyof TRANSACTION_TYPES]} type - The transaction type constant + * @param {string} nativeCurrency - The native currency of the currently selected network * @returns {string} The transaction category title */ -export function getTransactionTypeTitle(t, type) { +export function getTransactionTypeTitle(t, type, nativeCurrency = 'ETH') { switch (type) { case TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER: { return t('transfer'); @@ -209,8 +210,8 @@ export function getTransactionTypeTitle(t, type) { case TRANSACTION_TYPES.TOKEN_METHOD_APPROVE: { return t('approve'); } - case TRANSACTION_TYPES.SENT_ETHER: { - return t('sentEther'); + case TRANSACTION_TYPES.SIMPLE_SEND: { + return t('sendingNativeAsset', [nativeCurrency]); } case TRANSACTION_TYPES.CONTRACT_INTERACTION: { return t('contractInteraction'); diff --git a/ui/helpers/utils/util.js b/ui/helpers/utils/util.js index 1a02e930a..0cd76837e 100644 --- a/ui/helpers/utils/util.js +++ b/ui/helpers/utils/util.js @@ -56,6 +56,12 @@ export function isDefaultMetaMaskChain(chainId) { return false; } +// Both inputs should be strings. This method is currently used to compare tokenAddress hex strings. +export function isEqualCaseInsensitive(value1, value2) { + if (typeof value1 !== 'string' || typeof value2 !== 'string') return false; + return value1.toLowerCase() === value2.toLowerCase(); +} + export function valuesFor(obj) { if (!obj) { return []; @@ -379,3 +385,19 @@ export function bnLessThanEqualTo(a, b) { } return new BigNumber(a, 10).lte(b, 10); } + +export function getURL(url) { + try { + return new URL(url); + } catch (err) { + return ''; + } +} + +export function getURLHost(url) { + return getURL(url)?.host || ''; +} + +export function getURLHostName(url) { + return getURL(url)?.hostname || ''; +} diff --git a/ui/hooks/useCurrentAsset.js b/ui/hooks/useCurrentAsset.js index 068d076e7..4292a630b 100644 --- a/ui/hooks/useCurrentAsset.js +++ b/ui/hooks/useCurrentAsset.js @@ -3,6 +3,7 @@ import { useRouteMatch } from 'react-router-dom'; import { getTokens } from '../ducks/metamask/metamask'; import { getCurrentChainId } from '../selectors'; import { ASSET_ROUTE } from '../helpers/constants/routes'; +import { isEqualCaseInsensitive } from '../helpers/utils/util'; import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP, ETH_SWAPS_TOKEN_OBJECT, @@ -26,7 +27,10 @@ export function useCurrentAsset() { const tokenAddress = match?.params?.asset; const knownTokens = useSelector(getTokens); const token = - tokenAddress && knownTokens.find(({ address }) => address === tokenAddress); + tokenAddress && + knownTokens.find(({ address }) => + isEqualCaseInsensitive(address, tokenAddress), + ); const chainId = useSelector(getCurrentChainId); return ( diff --git a/ui/hooks/useTokenTracker.js b/ui/hooks/useTokenTracker.js index abcf03233..5ad2fb5af 100644 --- a/ui/hooks/useTokenTracker.js +++ b/ui/hooks/useTokenTracker.js @@ -3,6 +3,7 @@ import TokenTracker from '@metamask/eth-token-tracker'; import { useSelector } from 'react-redux'; import { getCurrentChainId, getSelectedAddress } from '../selectors'; import { SECOND } from '../../shared/constants/time'; +import { isEqualCaseInsensitive } from '../helpers/utils/util'; import { useEqualityCheck } from './useEqualityCheck'; export function useTokenTracker( @@ -26,10 +27,14 @@ export function useTokenTracker( // TODO: improve this pattern for adding this field when we improve support for // EIP721 tokens. const matchingTokensWithIsERC721Flag = matchingTokens.map((token) => { - const additionalTokenData = memoizedTokens.find( - (t) => t.address === token.address, + const additionalTokenData = memoizedTokens.find((t) => + isEqualCaseInsensitive(t.address, token.address), ); - return { ...token, isERC721: additionalTokenData?.isERC721 }; + return { + ...token, + isERC721: additionalTokenData?.isERC721, + image: additionalTokenData?.image, + }; }); setTokensWithBalances(matchingTokensWithIsERC721Flag); setLoading(false); diff --git a/ui/hooks/useTokensToSearch.js b/ui/hooks/useTokensToSearch.js index dd3d30f54..625af05d3 100644 --- a/ui/hooks/useTokensToSearch.js +++ b/ui/hooks/useTokensToSearch.js @@ -9,15 +9,16 @@ import { getCurrentCurrency, getSwapsDefaultToken, getCurrentChainId, + getUseTokenDetection, + getTokenList, } from '../selectors'; import { getConversionRate } from '../ducks/metamask/metamask'; import { getSwapsTokens } from '../ducks/swaps/swaps'; import { isSwapsDefaultTokenSymbol } from '../../shared/modules/swaps.utils'; -import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { useEqualityCheck } from './useEqualityCheck'; -const tokenList = shuffle( +const shuffledContractMap = shuffle( Object.entries(contractMap) .map(([address, tokenData]) => ({ ...tokenData, @@ -32,9 +33,14 @@ export function getRenderableTokenData( conversionRate, currentCurrency, chainId, + tokenList, + useTokenDetection, ) { const { symbol, name, address, iconUrl, string, balance, decimals } = token; - + // token from dynamic api list is fetched when useTokenDetection is true + // And since the token.address from allTokens is checksumaddress + // token Address have to be changed to lowercase when we are using dynamic list + const tokenAddress = useTokenDetection ? address?.toLowerCase() : address; const formattedFiat = getTokenFiatAmount( isSwapsDefaultTokenSymbol(symbol, chainId) @@ -59,12 +65,12 @@ export function getRenderableTokenData( ) || ''; const usedIconUrl = iconUrl || - (contractMap[toChecksumHexAddress(address)] && - `images/contract/${contractMap[toChecksumHexAddress(address)].logo}`); + (tokenList[tokenAddress] && + `images/contract/${tokenList[tokenAddress].iconUrl}`); return { ...token, primaryLabel: symbol, - secondaryLabel: name || contractMap[toChecksumHexAddress(address)]?.name, + secondaryLabel: name || tokenList[tokenAddress]?.name, rightPrimaryLabel: string && `${new BigNumber(string).round(6).toString()} ${symbol}`, rightSecondaryLabel: formattedFiat, @@ -72,18 +78,27 @@ export function getRenderableTokenData( identiconAddress: usedIconUrl ? null : address, balance, decimals, - name: name || contractMap[toChecksumHexAddress(address)]?.name, + name: name || tokenList[tokenAddress]?.name, rawFiat, }; } -export function useTokensToSearch({ usersTokens = [], topTokens = {} }) { +export function useTokensToSearch({ + usersTokens = [], + topTokens = {}, + shuffledTokensList, +}) { const chainId = useSelector(getCurrentChainId); const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual); const conversionRate = useSelector(getConversionRate); const currentCurrency = useSelector(getCurrentCurrency); const defaultSwapsToken = useSelector(getSwapsDefaultToken); - + const tokenList = useSelector(getTokenList); + const useTokenDetection = useSelector(getUseTokenDetection); + // token from dynamic api list is fetched when useTokenDetection is true + const shuffledTokenList = useTokenDetection + ? shuffledTokensList + : shuffledContractMap; const memoizedTopTokens = useEqualityCheck(topTokens); const memoizedUsersToken = useEqualityCheck(usersTokens); @@ -93,6 +108,8 @@ export function useTokensToSearch({ usersTokens = [], topTokens = {} }) { conversionRate, currentCurrency, chainId, + tokenList, + useTokenDetection, ); const memoizedDefaultToken = useEqualityCheck(defaultToken); @@ -102,7 +119,7 @@ export function useTokensToSearch({ usersTokens = [], topTokens = {} }) { ? swapsTokens : [ memoizedDefaultToken, - ...tokenList.filter( + ...shuffledTokenList.filter( (token) => token.symbol !== memoizedDefaultToken.symbol, ), ]; @@ -132,6 +149,8 @@ export function useTokensToSearch({ usersTokens = [], topTokens = {} }) { conversionRate, currentCurrency, chainId, + tokenList, + useTokenDetection, ); if ( isSwapsDefaultTokenSymbol(renderableDataToken.symbol, chainId) || @@ -166,5 +185,7 @@ export function useTokensToSearch({ usersTokens = [], topTokens = {} }) { currentCurrency, memoizedTopTokens, chainId, + tokenList, + useTokenDetection, ]); } diff --git a/ui/hooks/useTransactionDisplayData.js b/ui/hooks/useTransactionDisplayData.js index 94fb83846..d63c6278a 100644 --- a/ui/hooks/useTransactionDisplayData.js +++ b/ui/hooks/useTransactionDisplayData.js @@ -8,10 +8,12 @@ import { camelCaseToCapitalize } from '../helpers/utils/common.util'; import { PRIMARY, SECONDARY } from '../helpers/constants/common'; import { getTokenAddressParam } from '../helpers/utils/token-util'; import { + isEqualCaseInsensitive, formatDateWithYearContext, shortenAddress, stripHttpSchemes, } from '../helpers/utils/util'; + import { PENDING_STATUS_HASH, TOKEN_CATEGORY_HASH, @@ -97,7 +99,9 @@ export function useTransactionDisplayData(transactionGroup) { // hook to return null const token = isTokenCategory && - knownTokens.find(({ address }) => address === recipientAddress); + knownTokens.find(({ address }) => + isEqualCaseInsensitive(address, recipientAddress), + ); const tokenData = useTokenData( initialTransaction?.txParams?.data, isTokenCategory, @@ -218,7 +222,7 @@ export function useTransactionDisplayData(transactionGroup) { title = t('sendSpecifiedTokens', [token?.symbol || t('token')]); recipientAddress = getTokenAddressParam(tokenData); subtitle = t('toAddress', [shortenAddress(recipientAddress)]); - } else if (type === TRANSACTION_TYPES.SENT_ETHER) { + } else if (type === TRANSACTION_TYPES.SIMPLE_SEND) { category = TRANSACTION_GROUP_CATEGORIES.SEND; title = t('send'); subtitle = t('toAddress', [shortenAddress(recipientAddress)]); diff --git a/ui/pages/add-token/index.js b/ui/pages/add-token/index.js deleted file mode 100644 index e107950f1..000000000 --- a/ui/pages/add-token/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import AddToken from './add-token.container'; - -export default AddToken; diff --git a/ui/pages/asset/asset.js b/ui/pages/asset/asset.js index f10743a76..303d6fdeb 100644 --- a/ui/pages/asset/asset.js +++ b/ui/pages/asset/asset.js @@ -3,6 +3,7 @@ import { useSelector } from 'react-redux'; import { Redirect, useParams } from 'react-router-dom'; import { getTokens } from '../../ducks/metamask/metamask'; import { DEFAULT_ROUTE } from '../../helpers/constants/routes'; +import { isEqualCaseInsensitive } from '../../helpers/utils/util'; import NativeAsset from './components/native-asset'; import TokenAsset from './components/token-asset'; @@ -12,7 +13,9 @@ const Asset = () => { const tokens = useSelector(getTokens); const { asset } = useParams(); - const token = tokens.find(({ address }) => address === asset); + const token = tokens.find(({ address }) => + isEqualCaseInsensitive(address, asset), + ); useEffect(() => { const el = document.querySelector('.app'); diff --git a/ui/pages/asset/components/native-asset.js b/ui/pages/asset/components/native-asset.js index 6d17f512c..11fd1cc4d 100644 --- a/ui/pages/asset/components/native-asset.js +++ b/ui/pages/asset/components/native-asset.js @@ -13,6 +13,7 @@ import { } from '../../../selectors/selectors'; import { showModal } from '../../../store/actions'; import { DEFAULT_ROUTE } from '../../../helpers/constants/routes'; +import { getURLHostName } from '../../../helpers/utils/util'; import { useNewMetricEvent } from '../../../hooks/useMetricEvent'; import AssetNavigation from './asset-navigation'; import AssetOptions from './asset-options'; @@ -35,7 +36,7 @@ export default function NativeAsset({ nativeCurrency }) { properties: { link_type: 'Account Tracker', action: 'Asset Options', - block_explorer_domain: accountLink ? new URL(accountLink)?.hostname : '', + block_explorer_domain: getURLHostName(accountLink), }, }); diff --git a/ui/pages/asset/components/token-asset.js b/ui/pages/asset/components/token-asset.js index c5b6410c0..e48d0168f 100644 --- a/ui/pages/asset/components/token-asset.js +++ b/ui/pages/asset/components/token-asset.js @@ -11,6 +11,7 @@ import { getRpcPrefsForCurrentProvider, } from '../../../selectors/selectors'; import { DEFAULT_ROUTE } from '../../../helpers/constants/routes'; +import { getURLHostName } from '../../../helpers/utils/util'; import { showModal } from '../../../store/actions'; import { useNewMetricEvent } from '../../../hooks/useMetricEvent'; @@ -39,9 +40,7 @@ export default function TokenAsset({ token }) { properties: { link_type: 'Token Tracker', action: 'Token Options', - block_explorer_domain: tokenTrackerLink - ? new URL(tokenTrackerLink)?.hostname - : '', + block_explorer_domain: getURLHostName(tokenTrackerLink), }, }); diff --git a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js index 41d506644..ec8d163aa 100644 --- a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js +++ b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js @@ -5,6 +5,7 @@ import Identicon from '../../components/ui/identicon'; import TokenBalance from '../../components/ui/token-balance'; import { getEnvironmentType } from '../../../app/scripts/lib/util'; import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../shared/constants/app'; +import { isEqualCaseInsensitive } from '../../helpers/utils/util'; export default class ConfirmAddSuggestedToken extends Component { static contextTypes = { @@ -14,28 +15,31 @@ export default class ConfirmAddSuggestedToken extends Component { static propTypes = { history: PropTypes.object, - addToken: PropTypes.func, + acceptWatchAsset: PropTypes.func, + rejectWatchAsset: PropTypes.func, mostRecentOverviewPage: PropTypes.string.isRequired, - pendingTokens: PropTypes.object, - removeSuggestedTokens: PropTypes.func, + suggestedAssets: PropTypes.array, tokens: PropTypes.array, }; componentDidMount() { - this._checkPendingTokens(); + this._checksuggestedAssets(); } componentDidUpdate() { - this._checkPendingTokens(); + this._checksuggestedAssets(); } - _checkPendingTokens() { - const { mostRecentOverviewPage, pendingTokens = {}, history } = this.props; + _checksuggestedAssets() { + const { + mostRecentOverviewPage, + suggestedAssets = [], + history, + } = this.props; - if (Object.keys(pendingTokens).length > 0) { + if (suggestedAssets.length > 0) { return; } - if (getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION) { global.platform.closeCurrentWindow(); } else { @@ -49,17 +53,19 @@ export default class ConfirmAddSuggestedToken extends Component { render() { const { - addToken, - pendingTokens, + suggestedAssets, tokens, - removeSuggestedTokens, + rejectWatchAsset, history, mostRecentOverviewPage, + acceptWatchAsset, } = this.props; - const pendingTokenKey = Object.keys(pendingTokens)[0]; - const pendingToken = pendingTokens[pendingTokenKey]; - const hasTokenDuplicates = this.checkTokenDuplicates(pendingTokens, tokens); - const reusesName = this.checkNameReuse(pendingTokens, tokens); + + const hasTokenDuplicates = this.checkTokenDuplicates( + suggestedAssets, + tokens, + ); + const reusesName = this.checkNameReuse(suggestedAssets, tokens); return (
    @@ -68,7 +74,7 @@ export default class ConfirmAddSuggestedToken extends Component { {this.context.t('addSuggestedTokens')}
    - {this.context.t('likeToAddTokens')} + {this.context.t('likeToImportTokens')}
    {hasTokenDuplicates ? (
    {this.context.t('knownTokenWarning')}
    @@ -80,37 +86,35 @@ export default class ConfirmAddSuggestedToken extends Component { ) : null}
    -
    -
    -
    +
    +
    +
    {this.context.t('token')}
    -
    +
    {this.context.t('balance')}
    -
    - {Object.entries(pendingTokens).map(([address, token]) => { - const { name, symbol, image } = token; - +
    + {suggestedAssets.map(({ asset }) => { return (
    -
    +
    -
    - {this.getTokenName(name, symbol)} +
    + {this.getTokenName(asset.name, asset.symbol)}
    -
    - +
    +
    ); @@ -124,10 +128,11 @@ export default class ConfirmAddSuggestedToken extends Component { type="default" large className="page-container__footer-button" - onClick={() => { - removeSuggestedTokens().then(() => - history.push(mostRecentOverviewPage), + onClick={async () => { + await Promise.all( + suggestedAssets.map(async ({ id }) => rejectWatchAsset(id)), ); + history.push(mostRecentOverviewPage); }} > {this.context.t('cancel')} @@ -136,24 +141,25 @@ export default class ConfirmAddSuggestedToken extends Component { type="secondary" large className="page-container__footer-button" - disabled={pendingTokens.length === 0} - onClick={() => { - addToken(pendingToken) - .then(() => removeSuggestedTokens()) - .then(() => { + disabled={suggestedAssets.length === 0} + onClick={async () => { + await Promise.all( + suggestedAssets.map(async ({ asset, id }) => { + await acceptWatchAsset(id); this.context.trackEvent({ event: 'Token Added', category: 'Wallet', sensitiveProperties: { - token_symbol: pendingToken.symbol, - token_contract_address: pendingToken.address, - token_decimal_precision: pendingToken.decimals, - unlisted: pendingToken.unlisted, + token_symbol: asset.symbol, + token_contract_address: asset.address, + token_decimal_precision: asset.decimals, + unlisted: asset.unlisted, source: 'dapp', }, }); - }) - .then(() => history.push(mostRecentOverviewPage)); + }), + ); + history.push(mostRecentOverviewPage); }} > {this.context.t('addToken')} @@ -164,9 +170,11 @@ export default class ConfirmAddSuggestedToken extends Component { ); } - checkTokenDuplicates(pendingTokens, tokens) { - const pending = Object.keys(pendingTokens); - const existing = tokens.map((token) => token.address); + checkTokenDuplicates(suggestedAssets, tokens) { + const pending = suggestedAssets.map(({ asset }) => + asset.address.toUpperCase(), + ); + const existing = tokens.map((token) => token.address.toUpperCase()); const dupes = pending.filter((proposed) => { return existing.includes(proposed); }); @@ -175,20 +183,20 @@ export default class ConfirmAddSuggestedToken extends Component { } /** - * Returns true if any pendingTokens both: + * Returns true if any suggestedAssets both: * - Share a symbol with an existing `tokens` member. * - Does not share an address with that same `tokens` member. * This should be flagged as possibly deceptive or confusing. */ - checkNameReuse(pendingTokens, tokens) { - const duplicates = Object.keys(pendingTokens) - .map((addr) => pendingTokens[addr]) - .filter((token) => { - const dupes = tokens - .filter((old) => old.symbol === token.symbol) - .filter((old) => old.address !== token.address); - return dupes.length > 0; - }); + checkNameReuse(suggestedAssets, tokens) { + const duplicates = suggestedAssets.filter(({ asset }) => { + const dupes = tokens.filter( + (old) => + old.symbol === asset.symbol && + !isEqualCaseInsensitive(old.address, asset.address), + ); + return dupes.length > 0; + }); return duplicates.length > 0; } } diff --git a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js index ae7eb131f..5f40a6cc4 100644 --- a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js +++ b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js @@ -1,28 +1,28 @@ import { connect } from 'react-redux'; import { compose } from 'redux'; import { withRouter } from 'react-router-dom'; -import { addToken, removeSuggestedTokens } from '../../store/actions'; +import { rejectWatchAsset, acceptWatchAsset } from '../../store/actions'; import { getMostRecentOverviewPage } from '../../ducks/history/history'; import ConfirmAddSuggestedToken from './confirm-add-suggested-token.component'; const mapStateToProps = (state) => { const { - metamask: { pendingTokens, suggestedTokens, tokens }, + metamask: { suggestedAssets, tokens }, } = state; - const params = { ...pendingTokens, ...suggestedTokens }; return { mostRecentOverviewPage: getMostRecentOverviewPage(state), - pendingTokens: params, + suggestedAssets, tokens, }; }; const mapDispatchToProps = (dispatch) => { return { - addToken: ({ address, symbol, decimals, image }) => - dispatch(addToken(address, symbol, Number(decimals), image)), - removeSuggestedTokens: () => dispatch(removeSuggestedTokens()), + rejectWatchAsset: (suggestedAssetID) => + dispatch(rejectWatchAsset(suggestedAssetID)), + acceptWatchAsset: (suggestedAssetID) => + dispatch(acceptWatchAsset(suggestedAssetID)), }; }; diff --git a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.stories.js b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.stories.js new file mode 100644 index 000000000..cdd525aa5 --- /dev/null +++ b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.stories.js @@ -0,0 +1,64 @@ +/* eslint-disable react/prop-types */ +import React, { useEffect } from 'react'; +import { text } from '@storybook/addon-knobs'; +import { store, getNewState } from '../../../.storybook/preview'; +import { suggestedTokens } from '../../../.storybook/initial-states/approval-screens/add-suggested-token'; +import { updateMetamaskState } from '../../store/actions'; +import ConfirmAddSuggestedToken from '.'; + +export default { + title: 'Confirmation Screens', + id: __filename, +}; + +const PageSet = ({ children }) => { + const symbol = text('symbol', 'META'); + const image = text('Icon URL', 'metamark.svg'); + + const state = store.getState(); + const suggestedTokensState = state.metamask.suggestedTokens; + + useEffect(() => { + suggestedTokensState[ + '0x6b175474e89094c44da98b954eedeac495271d0f' + ].symbol = symbol; + store.dispatch( + updateMetamaskState( + getNewState(state.metamask, { + suggestedTokens: suggestedTokensState, + }), + ), + ); + }, [symbol, suggestedTokensState, state.metamask]); + useEffect(() => { + suggestedTokensState[ + '0x6b175474e89094c44da98b954eedeac495271d0f' + ].image = image; + store.dispatch( + updateMetamaskState( + getNewState(state.metamask, { + suggestedTokens: suggestedTokensState, + }), + ), + ); + }, [image, suggestedTokensState, state.metamask]); + + return children; +}; + +export const AddSuggestedToken = () => { + const state = store.getState(); + store.dispatch( + updateMetamaskState( + getNewState(state.metamask, { + suggestedTokens, + }), + ), + ); + + return ( + + + + ); +}; diff --git a/ui/pages/confirm-add-token/index.js b/ui/pages/confirm-add-token/index.js deleted file mode 100644 index 8b399cdca..000000000 --- a/ui/pages/confirm-add-token/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import ConfirmAddToken from './confirm-add-token.container'; - -export default ConfirmAddToken; 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 e8230a9f5..d589eb4c6 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 @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import UrlIcon from '../../../components/ui/url-icon'; -import { addressSummary } from '../../../helpers/utils/util'; +import { addressSummary, getURLHostName } from '../../../helpers/utils/util'; import { formatCurrency } from '../../../helpers/utils/confirm-tx.util'; import { ConfirmPageContainerWarning } from '../../../components/app/confirm-page-container/confirm-page-container-content'; import Typography from '../../../components/ui/typography'; @@ -256,7 +256,7 @@ export default class ConfirmApproveContent extends Component {
    diff --git a/ui/pages/confirm-approve/confirm-approve.js b/ui/pages/confirm-approve/confirm-approve.js index 303ecaa7c..64b3cc5c2 100644 --- a/ui/pages/confirm-approve/confirm-approve.js +++ b/ui/pages/confirm-approve/confirm-approve.js @@ -31,6 +31,7 @@ import { useApproveTransaction } from '../../hooks/useApproveTransaction'; import { currentNetworkTxListSelector } from '../../selectors/transactions'; import Loading from '../../components/ui/loading-screen'; import EditGasPopover from '../../components/app/edit-gas-popover/edit-gas-popover.component'; +import { isEqualCaseInsensitive } from '../../helpers/utils/util'; import { getCustomTxParamsData } from './confirm-approve.util'; import ConfirmApproveContent from './confirm-approve-content'; @@ -60,7 +61,9 @@ export default function ConfirmApprove() { ); const currentToken = (tokens && - tokens.find(({ address }) => tokenAddress === address)) || { + tokens.find(({ address }) => + isEqualCaseInsensitive(tokenAddress, address), + )) || { address: tokenAddress, }; diff --git a/ui/pages/confirm-approve/confirm-approve.stories.js b/ui/pages/confirm-approve/confirm-approve.stories.js index 35ee95e31..5c24c93ab 100644 --- a/ui/pages/confirm-approve/confirm-approve.stories.js +++ b/ui/pages/confirm-approve/confirm-approve.stories.js @@ -5,16 +5,14 @@ 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 { store, getNewState } from '../../../.storybook/preview'; -import { - currentNetworkTxListSample, - domainMetadata, -} from '../../../.storybook/initial-states/approval-screens/token-approval'; +import { domainMetadata } from '../../../.storybook/initial-states/approval-screens/token-approval'; import ConfirmApprove from '.'; export default { title: 'Confirmation Screens', + id: __filename, }; // transaction ID, maps to entry in state.metamask.currentNetworkTxList @@ -26,28 +24,34 @@ const PageSet = ({ children }) => { 'Icon URL', 'https://metamask.github.io/test-dapp/metamask-fox.svg', ); - + const state = store.getState(); const currentNetworkTxList = useSelector(currentNetworkTxListSelector); const transaction = currentNetworkTxList.find(({ id }) => id === txId); useEffect(() => { transaction.origin = origin; store.dispatch( - updateMetamaskState({ currentNetworkTxList: [transaction] }), + updateMetamaskState( + getNewState(state.metamask, { + currentNetworkTxList: [transaction], + }), + ), ); - }, [origin, transaction]); + }, [origin, transaction, state.metamask]); useEffect(() => { store.dispatch( - updateMetamaskState({ - domainMetadata: { - [origin]: { - icon: domainIconUrl, + updateMetamaskState( + getNewState(state.metamask, { + domainMetadata: { + [origin]: { + icon: domainIconUrl, + }, }, - }, - }), + }), + ), ); - }, [domainIconUrl, origin]); + }, [domainIconUrl, origin, state.metamask]); const params = useParams(); params.id = txId; @@ -55,10 +59,14 @@ const PageSet = ({ children }) => { }; export const ApproveTokens = () => { + const state = store.getState(); store.dispatch( - updateMetamaskState({ currentNetworkTxList: [currentNetworkTxListSample] }), + updateMetamaskState( + getNewState(state.metamask, { + domainMetadata, + }), + ), ); - store.dispatch(updateMetamaskState({ domainMetadata })); return ( diff --git a/ui/pages/confirm-deploy-contract/confirm-deploy-contract.stories.js b/ui/pages/confirm-deploy-contract/confirm-deploy-contract.stories.js index 3bbbfbdb3..9e021cac1 100644 --- a/ui/pages/confirm-deploy-contract/confirm-deploy-contract.stories.js +++ b/ui/pages/confirm-deploy-contract/confirm-deploy-contract.stories.js @@ -8,6 +8,7 @@ import ConfirmDeployContract from '.'; export default { title: 'Confirmation Screens', + id: __filename, }; // transaction ID diff --git a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.stories.js b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.stories.js index 548097850..9b9426962 100644 --- a/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.stories.js +++ b/ui/pages/confirm-encryption-public-key/confirm-encryption-public-key.stories.js @@ -1,12 +1,13 @@ import React, { useEffect } from 'react'; import { select } from '@storybook/addon-knobs'; -import { store } from '../../../.storybook/preview'; +import { store, getNewState } from '../../../.storybook/preview'; import { updateMetamaskState } from '../../store/actions'; import ConfirmEncryptionPublicKey from '.'; export default { title: 'Confirmation Screens', + id: __filename, }; const PageSet = ({ children }) => { @@ -26,17 +27,18 @@ const PageSet = ({ children }) => { unapprovedEncryptionPublicKeyMsgs['7786962153682822'].msgParams = account.address; store.dispatch( - updateMetamaskState({ - unapprovedEncryptionPublicKeyMsgs, - }), + updateMetamaskState( + getNewState(state.metamask, { + unapprovedEncryptionPublicKeyMsgs, + }), + ), ); - }, [account, unapprovedEncryptionPublicKeyMsgs]); + }, [account, unapprovedEncryptionPublicKeyMsgs, state.metamask]); return children; }; export const ConfirmEncryption = () => { - store.dispatch(updateMetamaskState({ unapprovedTxs: {} })); return ( diff --git a/ui/pages/confirm-add-token/confirm-add-token.component.js b/ui/pages/confirm-import-token/confirm-import-token.component.js similarity index 78% rename from ui/pages/confirm-add-token/confirm-add-token.component.js rename to ui/pages/confirm-import-token/confirm-import-token.component.js index fed9a60e4..5b6a9a95a 100644 --- a/ui/pages/confirm-add-token/confirm-add-token.component.js +++ b/ui/pages/confirm-import-token/confirm-import-token.component.js @@ -1,11 +1,14 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { ASSET_ROUTE, ADD_TOKEN_ROUTE } from '../../helpers/constants/routes'; +import { + ASSET_ROUTE, + IMPORT_TOKEN_ROUTE, +} from '../../helpers/constants/routes'; import Button from '../../components/ui/button'; import Identicon from '../../components/ui/identicon'; import TokenBalance from '../../components/ui/token-balance'; -export default class ConfirmAddToken extends Component { +export default class ConfirmImportToken extends Component { static contextTypes = { t: PropTypes.func, trackEvent: PropTypes.func, @@ -44,42 +47,42 @@ export default class ConfirmAddToken extends Component {
    - {this.context.t('addTokens')} + {this.context.t('importTokensCamelCase')}
    - {this.context.t('likeToAddTokens')} + {this.context.t('likeToImportTokens')}
    -
    -
    -
    +
    +
    +
    {this.context.t('token')}
    -
    +
    {this.context.t('balance')}
    -
    +
    {Object.entries(pendingTokens).map(([address, token]) => { const { name, symbol } = token; return (
    -
    +
    -
    +
    {this.getTokenName(name, symbol)}
    -
    +
    @@ -94,7 +97,7 @@ export default class ConfirmAddToken extends Component { type="default" large className="page-container__footer-button" - onClick={() => history.push(ADD_TOKEN_ROUTE)} + onClick={() => history.push(IMPORT_TOKEN_ROUTE)} > {this.context.t('back')} @@ -128,7 +131,7 @@ export default class ConfirmAddToken extends Component { }); }} > - {this.context.t('addTokens')} + {this.context.t('importTokensCamelCase')}
    diff --git a/ui/pages/confirm-add-token/confirm-add-token.container.js b/ui/pages/confirm-import-token/confirm-import-token.container.js similarity index 88% rename from ui/pages/confirm-add-token/confirm-add-token.container.js rename to ui/pages/confirm-import-token/confirm-import-token.container.js index aef46d261..ef80986cb 100644 --- a/ui/pages/confirm-add-token/confirm-add-token.container.js +++ b/ui/pages/confirm-import-token/confirm-import-token.container.js @@ -2,7 +2,7 @@ import { connect } from 'react-redux'; import { addTokens, clearPendingTokens } from '../../store/actions'; import { getMostRecentOverviewPage } from '../../ducks/history/history'; -import ConfirmAddToken from './confirm-add-token.component'; +import ConfirmImportToken from './confirm-import-token.component'; const mapStateToProps = (state) => { const { @@ -21,4 +21,4 @@ const mapDispatchToProps = (dispatch) => { }; }; -export default connect(mapStateToProps, mapDispatchToProps)(ConfirmAddToken); +export default connect(mapStateToProps, mapDispatchToProps)(ConfirmImportToken); diff --git a/ui/pages/confirm-import-token/confirm-import-token.stories.js b/ui/pages/confirm-import-token/confirm-import-token.stories.js new file mode 100644 index 000000000..87acd2f59 --- /dev/null +++ b/ui/pages/confirm-import-token/confirm-import-token.stories.js @@ -0,0 +1,39 @@ +/* eslint-disable react/prop-types */ +import React, { useEffect } from 'react'; + +import { createBrowserHistory } from 'history'; +import { text } from '@storybook/addon-knobs'; +import { store } from '../../../.storybook/preview'; +import { tokens } from '../../../.storybook/initial-states/approval-screens/add-token'; +import { updateMetamaskState } from '../../store/actions'; +import ConfirmAddToken from '.'; + +export default { + title: 'Confirmation Screens', + id: __filename, +}; + +const history = createBrowserHistory(); + +const PageSet = ({ children }) => { + const symbol = text('symbol', 'TRDT'); + const state = store.getState(); + const pendingTokensState = state.metamask.pendingTokens; + // only change the first token in the list + useEffect(() => { + const pendingTokens = { ...pendingTokensState }; + pendingTokens['0x33f90dee07c6e8b9682dd20f73e6c358b2ed0f03'].symbol = symbol; + store.dispatch(updateMetamaskState({ pendingTokens })); + }, [symbol, pendingTokensState]); + + return children; +}; + +export const AddToken = () => { + store.dispatch(updateMetamaskState({ pendingTokens: tokens })); + return ( + + + + ); +}; diff --git a/ui/pages/confirm-import-token/index.js b/ui/pages/confirm-import-token/index.js new file mode 100644 index 000000000..cddbd5032 --- /dev/null +++ b/ui/pages/confirm-import-token/index.js @@ -0,0 +1,3 @@ +import ConfirmImportToken from './confirm-import-token.container'; + +export default ConfirmImportToken; diff --git a/ui/pages/confirm-add-token/index.scss b/ui/pages/confirm-import-token/index.scss similarity index 97% rename from ui/pages/confirm-add-token/index.scss rename to ui/pages/confirm-import-token/index.scss index a049e9b24..8946405bf 100644 --- a/ui/pages/confirm-add-token/index.scss +++ b/ui/pages/confirm-import-token/index.scss @@ -1,4 +1,4 @@ -.confirm-add-token { +.confirm-import-token { padding: 16px; &__header { diff --git a/ui/pages/confirm-send-ether/confirm-send-ether.stories.js b/ui/pages/confirm-send-ether/confirm-send-ether.stories.js new file mode 100644 index 000000000..693847b64 --- /dev/null +++ b/ui/pages/confirm-send-ether/confirm-send-ether.stories.js @@ -0,0 +1,57 @@ +import React, { useEffect } from 'react'; + +import { select } from '@storybook/addon-knobs'; +import { store } from '../../../.storybook/preview'; +import { updateTransactionParams } from '../../store/actions'; +import ConfirmSendEther from '.'; + +export default { + title: 'Confirmation Screens', + id: __filename, +}; + +// transaction id for redux dispatcher +const id = 3111025347726181; + +const PageSet = ({ children }) => { + const options = []; + const receiverOptions = { + 'Address 1': '0xaD6D458402F60fD3Bd25163575031ACDce07538D', + 'Address 2': '0x55e0bfb2d400e9be8cf9b114e38a40969a02f69a', + }; + const state = store.getState(); + const { identities } = state.metamask; + Object.keys(identities).forEach(function (key) { + options.push({ + label: identities[key].name, + address: key, + }); + }); + const sender = select('Sender', options, options[0]); + const receiver = select( + 'Receiver', + receiverOptions, + '0xaD6D458402F60fD3Bd25163575031ACDce07538D', + ); + + const confirmTransactionState = state.confirmTransaction.txData.txParams; + + useEffect(() => { + confirmTransactionState.from = sender.address; + store.dispatch(updateTransactionParams(id, confirmTransactionState)); + }, [sender, confirmTransactionState]); + + useEffect(() => { + confirmTransactionState.to = receiver; + store.dispatch(updateTransactionParams(id, confirmTransactionState)); + }, [receiver, confirmTransactionState]); + return children; +}; + +export const SendEther = () => { + return ( + + + + ); +}; diff --git a/ui/pages/confirm-send-token/confirm-send-token.stories.js b/ui/pages/confirm-send-token/confirm-send-token.stories.js new file mode 100644 index 000000000..3cd6e45b9 --- /dev/null +++ b/ui/pages/confirm-send-token/confirm-send-token.stories.js @@ -0,0 +1,19 @@ +import React from 'react'; +import ConfirmSendToken from './confirm-send-token.component'; + +export default { + title: 'Confirmation Screens', + id: __filename, +}; + +const PageSet = ({ children }) => { + return children; +}; + +export const SendToken = () => { + return ( + + + + ); +}; diff --git a/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js b/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js index aec55e531..5f7b94696 100644 --- a/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js +++ b/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js @@ -13,6 +13,7 @@ import { getTokenValueParam, } from '../../helpers/utils/token-util'; import { hexWEIToDecETH } from '../../helpers/utils/conversions.util'; +import { isEqualCaseInsensitive } from '../../helpers/utils/util'; import ConfirmTokenTransactionBase from './confirm-token-transaction-base.component'; const mapStateToProps = (state, ownProps) => { @@ -48,7 +49,9 @@ const mapStateToProps = (state, ownProps) => { hexMaximumTransactionFee, } = transactionFeeSelector(state, transaction); const tokens = getTokens(state); - const currentToken = tokens?.find(({ address }) => tokenAddress === address); + const currentToken = tokens?.find(({ address }) => + isEqualCaseInsensitive(tokenAddress, address), + ); const { decimals, symbol: tokenSymbol } = currentToken || {}; const ethTransactionTotalMaxAmount = Number( diff --git a/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.stories.js b/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.stories.js new file mode 100644 index 000000000..f6ef3097c --- /dev/null +++ b/ui/pages/confirm-token-transaction-base/confirm-token-transaction-base.stories.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { store } from '../../../.storybook/preview'; +import ConfirmTokenTransactionBase from './confirm-token-transaction-base.component'; + +export default { + title: 'Confirmation Screens', + id: __filename, +}; + +const state = store.getState(); + +export const ConfirmTokenTransaction = () => { + const { metamask, confirmTransaction } = state; + const { currentCurrency } = metamask; + const { fiatTransactionTotal } = confirmTransaction; + return ( + + ); +}; 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 c9a70540e..65d366970 100644 --- a/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -36,10 +36,10 @@ import InfoTooltip from '../../components/ui/info-tooltip/info-tooltip'; import LoadingHeartBeat from '../../components/ui/loading-heartbeat'; import GasTiming from '../../components/app/gas-timing/gas-timing.component'; import Dialog from '../../components/ui/dialog'; -import Typography from '../../components/ui/typography/typography'; import { COLORS, + FONT_STYLE, FONT_WEIGHT, TYPOGRAPHY, } from '../../helpers/constants/design-system'; @@ -50,6 +50,8 @@ import { removePollingTokenFromAppState, } from '../../store/actions'; +import Typography from '../../components/ui/typography/typography'; + const renderHeartBeatIfNotInTest = () => process.env.IN_TEST === 'true' ? null : ; @@ -79,7 +81,6 @@ export default class ConfirmTransactionBase extends Component { useNonceField: PropTypes.bool, customNonceValue: PropTypes.string, updateCustomNonce: PropTypes.func, - assetImage: PropTypes.string, sendTransaction: PropTypes.func, showTransactionConfirmedModal: PropTypes.func, showRejectTransactionsConfirmationModal: PropTypes.func, @@ -122,9 +123,11 @@ export default class ConfirmTransactionBase extends Component { maxFeePerGas: PropTypes.string, maxPriorityFeePerGas: PropTypes.string, baseFeePerGas: PropTypes.string, + isMainnet: PropTypes.bool, gasFeeIsCustom: PropTypes.bool, showLedgerSteps: PropTypes.bool.isRequired, isFirefox: PropTypes.bool.isRequired, + nativeCurrency: PropTypes.string, }; state = { @@ -303,19 +306,12 @@ export default class ConfirmTransactionBase extends Component { primaryTotalTextOverrideMaxAmount, maxFeePerGas, maxPriorityFeePerGas, + isMainnet, showLedgerSteps, isFirefox, } = this.props; const { t } = this.context; - const getRequestingOrigin = () => { - try { - return new URL(txData.origin)?.hostname; - } catch (err) { - return ''; - } - }; - const renderTotalMaxAmount = () => { if ( primaryTotalTextOverrideMaxAmount === undefined && @@ -325,6 +321,7 @@ export default class ConfirmTransactionBase extends Component { return ( @@ -345,6 +342,7 @@ export default class ConfirmTransactionBase extends Component { return ( @@ -363,6 +361,7 @@ export default class ConfirmTransactionBase extends Component { return ( @@ -453,9 +452,7 @@ export default class ConfirmTransactionBase extends Component { detailTitle={ txData.dappSuggestedGasFees ? ( <> - {t('transactionDetailDappGasHeading', [ - getRequestingOrigin(), - ])} + {t('transactionDetailGasHeading')} -

    {t('transactionDetailGasTooltipIntro')}

    +

    + {t('transactionDetailGasTooltipIntro', [ + isMainnet ? t('networkNameEthereum') : '', + ])} +

    {t('transactionDetailGasTooltipExplanation')}

    ) } - detailTitleColor={ - txData.dappSuggestedGasFees ? COLORS.SECONDARY1 : COLORS.BLACK - } + detailTitleColor={COLORS.BLACK} detailText={

    , ])} subTitle={ - + {txData.dappSuggestedGasFees ? ( + + {t('transactionDetailDappGasMoreInfo')} + + ) : ( + '' )} - maxFeePerGas={hexWEIToDecGWEI( - maxFeePerGas || txData.txParams.maxFeePerGas, - )} - /> + + } />, { - return { - ...acc, - [base.toLowerCase()]: contractMap[base], - }; -}, {}); - let customNonceValue = ''; const customNonceMerge = (txData) => customNonceValue @@ -82,7 +77,6 @@ const mapStateToProps = (state, ownProps) => { conversionRate, identities, addressBook, - assetImages, network, unapprovedTxs, nextNonce, @@ -103,15 +97,24 @@ const mapStateToProps = (state, ownProps) => { data, } = (transaction && transaction.txParams) || txParams; const accounts = getMetaMaskAccounts(state); - const assetImage = assetImages[txParamsToAddress]; const { balance } = accounts[fromAddress]; const { name: fromName } = identities[fromAddress]; const toAddress = propsToAddress || txParamsToAddress; + const tokenList = getTokenList(state); + const useTokenDetection = getUseTokenDetection(state); + const casedTokenList = useTokenDetection + ? tokenList + : Object.keys(tokenList).reduce((acc, base) => { + return { + ...acc, + [base.toLowerCase()]: tokenList[base], + }; + }, {}); const toName = identities[toAddress]?.name || - casedContractMap[toAddress]?.name || + casedTokenList[toAddress]?.name || shortenAddress(toChecksumHexAddress(toAddress)); const checksummedAddress = toChecksumHexAddress(toAddress); @@ -167,6 +170,7 @@ const mapStateToProps = (state, ownProps) => { txParamsAreDappSuggested(fullTxData); const showLedgerSteps = getHardwareWalletType(state) === KEYRING_TYPES.LEDGER; const isFirefox = getPlatform() === PLATFORM_FIREFOX; + const nativeCurrency = getNativeCurrency(state); return { balance, @@ -187,7 +191,6 @@ const mapStateToProps = (state, ownProps) => { conversionRate, transactionStatus, nonce, - assetImage, unapprovedTxs, unapprovedTxCount, currentNetworkUnapprovedTxs, @@ -216,6 +219,7 @@ const mapStateToProps = (state, ownProps) => { gasFeeIsCustom, showLedgerSteps, isFirefox, + nativeCurrency, }; }; diff --git a/ui/pages/confirm-transaction-base/confirm-transaction-base.stories.js b/ui/pages/confirm-transaction-base/confirm-transaction-base.stories.js new file mode 100644 index 000000000..a373eb250 --- /dev/null +++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.stories.js @@ -0,0 +1,20 @@ +import React from 'react'; + +import ConfirmTransactionBase from '.'; + +export default { + title: 'Confirmation Screens', + id: __filename, +}; + +const PageSet = ({ children }) => { + return children; +}; + +export const ConfirmTransactionBaseComponent = () => { + return ( + + + + ); +}; diff --git a/ui/pages/confirm-transaction-switch/confirm-transaction-switch.component.js b/ui/pages/confirm-transaction-switch/confirm-transaction-switch.component.js index 6a708e54d..7e9882d65 100644 --- a/ui/pages/confirm-transaction-switch/confirm-transaction-switch.component.js +++ b/ui/pages/confirm-transaction-switch/confirm-transaction-switch.component.js @@ -31,7 +31,7 @@ export default class ConfirmTransactionSwitch extends Component { return ; } - if (type === TRANSACTION_TYPES.SENT_ETHER) { + if (type === TRANSACTION_TYPES.SIMPLE_SEND) { const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_ETHER_PATH}`; return ; } diff --git a/ui/pages/connected-accounts/connected-accounts.component.js b/ui/pages/connected-accounts/connected-accounts.component.js index 5b9e5f06f..ad40257a9 100644 --- a/ui/pages/connected-accounts/connected-accounts.component.js +++ b/ui/pages/connected-accounts/connected-accounts.component.js @@ -3,6 +3,7 @@ import React, { PureComponent } from 'react'; import Popover from '../../components/ui/popover'; import ConnectedAccountsList from '../../components/app/connected-accounts-list'; import ConnectedAccountsPermissions from '../../components/app/connected-accounts-permissions'; +import { getURLHost } from '../../helpers/utils/util'; export default class ConnectedAccounts extends PureComponent { static contextTypes = { @@ -54,7 +55,7 @@ export default class ConnectedAccounts extends PureComponent { title={ isActiveTabExtension ? t('currentExtension') - : new URL(activeTabOrigin).host + : getURLHost(activeTabOrigin) } subtitle={ connectedAccounts.length diff --git a/ui/pages/connected-accounts/connected-accounts.stories.js b/ui/pages/connected-accounts/connected-accounts.stories.js new file mode 100644 index 000000000..7fcac236e --- /dev/null +++ b/ui/pages/connected-accounts/connected-accounts.stories.js @@ -0,0 +1,31 @@ +import React from 'react'; +import { action } from '@storybook/addon-actions'; +import ConnectedAccounts from './connected-accounts.component'; + +export default { + title: 'Connected Accounts', + id: __filename, +}; + +const account = [ + { + name: 'Account 1', + address: '0x983211ce699ea5ab57cc528086154b6db1ad8e55', + }, +]; +const identities = { + name: 'Account 1', + address: '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4', +}; +export const ConnectedAccountComponent = () => { + return ( + + ); +}; diff --git a/ui/pages/connected-sites/connected-sites.stories.js b/ui/pages/connected-sites/connected-sites.stories.js new file mode 100644 index 000000000..8abfa21dd --- /dev/null +++ b/ui/pages/connected-sites/connected-sites.stories.js @@ -0,0 +1,20 @@ +import React from 'react'; + +import ConnectedSites from '.'; + +export default { + title: 'Connected Sites', + id: __filename, +}; + +const PageSet = ({ children }) => { + return children; +}; + +export const ConnectedSitesComponent = () => { + return ( + + + + ); +}; diff --git a/ui/pages/create-account/connect-hardware/account-list.js b/ui/pages/create-account/connect-hardware/account-list.js index a3bb775df..96a31bd05 100644 --- a/ui/pages/create-account/connect-hardware/account-list.js +++ b/ui/pages/create-account/connect-hardware/account-list.js @@ -7,6 +7,8 @@ import Checkbox from '../../../components/ui/check-box'; import Dropdown from '../../../components/ui/dropdown'; import Popover from '../../../components/ui/popover'; +import { getURLHostName } from '../../../helpers/utils/util'; + class AccountList extends Component { state = { showPopover: false, @@ -143,9 +145,7 @@ class AccountList extends Component { properties: { actions: 'Hardware Connect', link_type: 'Account Tracker', - block_explorer_domain: accountLink - ? new URL(accountLink)?.hostname - : '', + block_explorer_domain: getURLHostName(accountLink), }, }); global.platform.openTab({ diff --git a/ui/pages/create-account/import-account/index.js b/ui/pages/create-account/import-account/index.js index 9c2a38b44..4f94f0965 100644 --- a/ui/pages/create-account/import-account/index.js +++ b/ui/pages/create-account/import-account/index.js @@ -36,41 +36,44 @@ export default class AccountImportSubview extends Component { render() { const menuItems = this.getMenuItemTexts(); const { type } = this.state; + const { t } = this.context; return ( -
    -
    - {this.context.t('importAccountMsg')} - { - global.platform.openTab({ - url: - 'https://metamask.zendesk.com/hc/en-us/articles/360015289932', - }); - }} - > - {this.context.t('here')} - -
    -
    -
    - {this.context.t('selectType')} + <> +
    +
    Import Account
    +
    + {t('importAccountMsg')} + { + global.platform.openTab({ + url: + 'https://metamask.zendesk.com/hc/en-us/articles/360015289932', + }); + }} + > + {t('here')} +
    - ({ value: text }))} - selectedOption={type || menuItems[0]} - onChange={(value) => { - this.setState({ type: value }); - }} - />
    - {this.renderImportView()} -
    +
    +
    +
    + {t('selectType')} +
    + ({ value: text }))} + selectedOption={type || menuItems[0]} + onChange={(value) => { + this.setState({ type: value }); + }} + /> +
    + {this.renderImportView()} +
    + ); } } diff --git a/ui/pages/create-account/import-account/index.scss b/ui/pages/create-account/import-account/index.scss index 1e5efe7f0..91580b70a 100644 --- a/ui/pages/create-account/import-account/index.scss +++ b/ui/pages/create-account/import-account/index.scss @@ -1,11 +1,7 @@ -.new-account-import-disclaimer { - @include H7; - - width: 120%; - background-color: #f4f9fc; - display: inline-block; - align-items: center; - padding: 20px 30px 20px; +.new-account-info-link { + cursor: pointer; + text-decoration: underline; + color: $primary-blue; } .new-account-import-form { diff --git a/ui/pages/create-account/import-account/json.js b/ui/pages/create-account/import-account/json.js index afaeb7751..1aa35c493 100644 --- a/ui/pages/create-account/import-account/json.js +++ b/ui/pages/create-account/import-account/json.js @@ -102,9 +102,10 @@ class JsonImportSubview extends Component { setSelectedAddress, } = this.props; const { fileContents } = this.state; + const { t } = this.context; if (!fileContents) { - const message = this.context.t('needImportFile'); + const message = t('needImportFile'); displayWarning(message); return; } @@ -124,7 +125,7 @@ class JsonImportSubview extends Component { }); displayWarning(null); } else { - displayWarning('Error importing account.'); + displayWarning(t('importAccountError')); this.context.metricsEvent({ eventOpts: { category: 'Accounts', diff --git a/ui/pages/create-account/import-account/private-key.js b/ui/pages/create-account/import-account/private-key.js index c360424be..507fb09f6 100644 --- a/ui/pages/create-account/import-account/private-key.js +++ b/ui/pages/create-account/import-account/private-key.js @@ -38,6 +38,7 @@ class PrivateKeyImportView extends Component { setSelectedAddress, firstAddress, } = this.props; + const { t } = this.context; importNewAccount('Private Key', [privateKey]) .then(({ selectedAddress }) => { @@ -52,7 +53,7 @@ class PrivateKeyImportView extends Component { history.push(mostRecentOverviewPage); displayWarning(null); } else { - displayWarning('Error importing account.'); + displayWarning(t('importAccountError')); this.context.metricsEvent({ eventOpts: { category: 'Accounts', diff --git a/ui/pages/create-account/index.scss b/ui/pages/create-account/index.scss index fa55a3efa..d01604002 100644 --- a/ui/pages/create-account/index.scss +++ b/ui/pages/create-account/index.scss @@ -14,7 +14,7 @@ display: none; } - @media screen and (min-width: 576px) { + @media screen and (min-width: $break-large) { position: absolute; } diff --git a/ui/pages/create-account/new-account.stories.js b/ui/pages/create-account/new-account.stories.js new file mode 100644 index 000000000..1b4e90d46 --- /dev/null +++ b/ui/pages/create-account/new-account.stories.js @@ -0,0 +1,12 @@ +import React from 'react'; +import { action } from '@storybook/addon-actions'; +import NewAccountCreateForm from './new-account.component'; + +export default { + title: 'New Account', + id: __filename, +}; + +export const NewAccountComponent = () => { + return ; +}; diff --git a/ui/pages/first-time-flow/create-password/create-password.component.js b/ui/pages/first-time-flow/create-password/create-password.component.js index 357ce283b..f3aae1d5c 100644 --- a/ui/pages/first-time-flow/create-password/create-password.component.js +++ b/ui/pages/first-time-flow/create-password/create-password.component.js @@ -7,6 +7,7 @@ import { INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE, INITIALIZE_SEED_PHRASE_INTRO_ROUTE, } from '../../../helpers/constants/routes'; +import { isBeta } from '../../../helpers/utils/build-types'; import NewAccount from './new-account'; import ImportWithSeedPhrase from './import-with-seed-phrase'; @@ -31,7 +32,7 @@ export default class CreatePassword extends PureComponent { return (
    - + { + return ; +}; + +export const NewAccountComponent = () => { + return ; +}; diff --git a/ui/pages/first-time-flow/end-of-flow/end-of-flow.component.js b/ui/pages/first-time-flow/end-of-flow/end-of-flow.component.js index f9ab3bb33..af23bc052 100644 --- a/ui/pages/first-time-flow/end-of-flow/end-of-flow.component.js +++ b/ui/pages/first-time-flow/end-of-flow/end-of-flow.component.js @@ -5,6 +5,7 @@ import Snackbar from '../../../components/ui/snackbar'; import MetaFoxLogo from '../../../components/ui/metafox-logo'; import { DEFAULT_ROUTE } from '../../../helpers/constants/routes'; import { returnToOnboardingInitiator } from '../onboarding-initiator-util'; +import { isBeta } from '../../../helpers/utils/build-types'; export default class EndOfFlowScreen extends PureComponent { static contextTypes = { @@ -67,7 +68,7 @@ export default class EndOfFlowScreen extends PureComponent { return (
    - +
    🎉
    {t('congratulations')}
    diff --git a/ui/pages/first-time-flow/end-of-flow/end-of-flow.stories.js b/ui/pages/first-time-flow/end-of-flow/end-of-flow.stories.js new file mode 100644 index 000000000..7d934301d --- /dev/null +++ b/ui/pages/first-time-flow/end-of-flow/end-of-flow.stories.js @@ -0,0 +1,11 @@ +import React from 'react'; +import EndOfFlowScreen from './end-of-flow.component'; + +export default { + title: 'First Time Flow', + id: __filename, +}; + +export const EndOfFlowComponent = () => { + return ; +}; diff --git a/ui/pages/first-time-flow/metametrics-opt-in/index.scss b/ui/pages/first-time-flow/metametrics-opt-in/index.scss index 90b437831..69695129b 100644 --- a/ui/pages/first-time-flow/metametrics-opt-in/index.scss +++ b/ui/pages/first-time-flow/metametrics-opt-in/index.scss @@ -13,7 +13,7 @@ margin-right: 28%; color: black; - @media screen and (max-width: 575px) { + @media screen and (max-width: $break-small) { justify-content: center; margin-left: 2%; margin-right: 0%; @@ -103,7 +103,7 @@ &__footer { margin-top: 26px; - @media screen and (max-width: 575px) { + @media screen and (max-width: $break-small) { margin-top: 10px; justify-content: center; margin-left: 2%; diff --git a/ui/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js b/ui/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js index 4e13ca634..2ba9a4648 100644 --- a/ui/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js +++ b/ui/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js @@ -2,6 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import MetaFoxLogo from '../../../components/ui/metafox-logo'; import PageContainerFooter from '../../../components/ui/page-container/page-container-footer'; +import { isBeta } from '../../../helpers/utils/build-types'; export default class MetaMetricsOptIn extends Component { static propTypes = { @@ -30,7 +31,7 @@ export default class MetaMetricsOptIn extends Component { return (
    - +
    diff --git a/ui/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.stories.js b/ui/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.stories.js new file mode 100644 index 000000000..afd83ded1 --- /dev/null +++ b/ui/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.stories.js @@ -0,0 +1,16 @@ +import React from 'react'; +import { action } from '@storybook/addon-actions'; +import MetaMetricsOptIn from './metametrics-opt-in.component'; + +export default { + title: 'First Time Flow', + id: __filename, +}; + +export const MetaMetricsOptInComponent = () => { + return ( + + ); +}; diff --git a/ui/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss b/ui/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss index 92c6fa9f0..a1c7bbc8d 100644 --- a/ui/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss +++ b/ui/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss @@ -4,7 +4,7 @@ } &__sorted-seed-words { - max-width: 575px; + max-width: $break-small; } &__seed-word { @@ -57,7 +57,7 @@ color: transparent; } - @media screen and (max-width: 575px) { + @media screen and (max-width: $break-small) { @include H6; padding: 6px 18px; @@ -70,7 +70,7 @@ display: flex; flex-flow: row wrap; min-height: 161px; - max-width: 575px; + max-width: $break-small; border: 1px solid #cdcdcd; border-radius: 6px; background-color: $white; diff --git a/ui/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss b/ui/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss index 0af006dab..e6d217de4 100644 --- a/ui/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss +++ b/ui/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss @@ -1,5 +1,5 @@ .reveal-seed-phrase { - @media screen and (max-width: 576px) { + @media screen and (max-width: $break-small) { display: flex; flex-direction: column; width: 96%; diff --git a/ui/pages/first-time-flow/seed-phrase/seed-phrase-intro/seed-phrase-intro.component.js b/ui/pages/first-time-flow/seed-phrase/seed-phrase-intro/seed-phrase-intro.component.js index 304339967..0930eb995 100644 --- a/ui/pages/first-time-flow/seed-phrase/seed-phrase-intro/seed-phrase-intro.component.js +++ b/ui/pages/first-time-flow/seed-phrase/seed-phrase-intro/seed-phrase-intro.component.js @@ -24,6 +24,19 @@ export default function SeedPhraseIntro() { history.push(INITIALIZE_SEED_PHRASE_ROUTE); }; + const subtitles = { + en: 'English', + es: 'Spanish', + hi: 'Hindi', + id: 'Indonesian', + ja: 'Japanese', + ko: 'Korean', + pt: 'Portuguese', + ru: 'Russian', + tl: 'Tagalog', + vi: 'Vietnamese', + }; + return (
    @@ -49,13 +62,18 @@ export default function SeedPhraseIntro() { type="video/webm" src="./images/videos/recovery-onboarding/video.webm" /> - + {Object.keys(subtitles).map((key) => { + return ( + + ); + })} diff --git a/ui/pages/first-time-flow/seed-phrase/seed-phrase.component.js b/ui/pages/first-time-flow/seed-phrase/seed-phrase.component.js index 612e86c77..db31f3c39 100644 --- a/ui/pages/first-time-flow/seed-phrase/seed-phrase.component.js +++ b/ui/pages/first-time-flow/seed-phrase/seed-phrase.component.js @@ -11,6 +11,7 @@ import { DEFAULT_ROUTE, } from '../../../helpers/constants/routes'; import MetaFoxLogo from '../../../components/ui/metafox-logo'; +import { isBeta } from '../../../helpers/utils/build-types'; import ConfirmSeedPhrase from './confirm-seed-phrase'; import RevealSeedPhrase from './reveal-seed-phrase'; import SeedPhraseIntro from './seed-phrase-intro'; @@ -50,7 +51,7 @@ export default class SeedPhrase extends PureComponent { return (
    - + - +
    diff --git a/ui/pages/first-time-flow/select-action/select-action.stories.js b/ui/pages/first-time-flow/select-action/select-action.stories.js new file mode 100644 index 000000000..205d64499 --- /dev/null +++ b/ui/pages/first-time-flow/select-action/select-action.stories.js @@ -0,0 +1,11 @@ +import React from 'react'; +import SelectAction from './select-action.component'; + +export default { + title: 'First Time Flow', + id: __filename, +}; + +export const SelectActionComponent = () => { + return ; +}; diff --git a/ui/pages/first-time-flow/welcome/beta-welcome-footer.component.js b/ui/pages/first-time-flow/welcome/beta-welcome-footer.component.js new file mode 100644 index 000000000..e7ebd921d --- /dev/null +++ b/ui/pages/first-time-flow/welcome/beta-welcome-footer.component.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { useI18nContext } from '../../../hooks/useI18nContext'; + +const BetaWelcomeFooter = () => { + const t = useI18nContext(); + + return ( + <> +
    {t('betaWelcome')}
    +
    + + ); +}; + +export default BetaWelcomeFooter; diff --git a/ui/pages/first-time-flow/welcome/index.scss b/ui/pages/first-time-flow/welcome/index.scss index 7ccf6ad64..947ff29a9 100644 --- a/ui/pages/first-time-flow/welcome/index.scss +++ b/ui/pages/first-time-flow/welcome/index.scss @@ -27,11 +27,18 @@ &__description { text-align: center; - div { + p { @include Paragraph; + + text-align: start; + margin-bottom: 22px; } - @media screen and (max-width: 575px) { + a { + color: $primary-1; + } + + @media screen and (max-width: $break-small) { font-size: 0.9rem; } } diff --git a/ui/pages/first-time-flow/welcome/welcome-footer.component.js b/ui/pages/first-time-flow/welcome/welcome-footer.component.js new file mode 100644 index 000000000..93f42fa97 --- /dev/null +++ b/ui/pages/first-time-flow/welcome/welcome-footer.component.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { useI18nContext } from '../../../hooks/useI18nContext'; + +const WelcomeFooter = () => { + const t = useI18nContext(); + + return ( + <> +
    {t('welcome')}
    +
    +

    {t('metamaskDescription')}

    +

    {t('happyToSeeYou')}

    +
    + + ); +}; + +export default WelcomeFooter; diff --git a/ui/pages/first-time-flow/welcome/welcome.component.js b/ui/pages/first-time-flow/welcome/welcome.component.js index 43d81b4fa..20a929372 100644 --- a/ui/pages/first-time-flow/welcome/welcome.component.js +++ b/ui/pages/first-time-flow/welcome/welcome.component.js @@ -7,6 +7,9 @@ import { INITIALIZE_CREATE_PASSWORD_ROUTE, INITIALIZE_SELECT_ACTION_ROUTE, } from '../../../helpers/constants/routes'; +import { isBeta } from '../../../helpers/utils/build-types'; +import WelcomeFooter from './welcome-footer.component'; +import BetaWelcomeFooter from './beta-welcome-footer.component'; export default class Welcome extends PureComponent { static propTypes = { @@ -50,11 +53,7 @@ export default class Welcome extends PureComponent { width="125" height="125" /> -
    {t('welcome')}
    -
    -
    {t('metamaskDescription')}
    -
    {t('happyToSeeYou')}
    -
    + {isBeta() ? : }
    diff --git a/ui/pages/home/home.container.js b/ui/pages/home/home.container.js index 4d179abf9..19768c60d 100644 --- a/ui/pages/home/home.container.js +++ b/ui/pages/home/home.container.js @@ -46,7 +46,7 @@ import Home from './home.component'; const mapStateToProps = (state) => { const { metamask, appState } = state; const { - suggestedTokens, + suggestedAssets, seedPhraseBackedUp, tokens, threeBoxSynced, @@ -83,7 +83,7 @@ const mapStateToProps = (state) => { return { forgottenPassword, - suggestedTokens, + suggestedAssets, swapsEnabled, unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state), shouldShowSeedPhraseReminder: diff --git a/ui/pages/add-token/add-token.component.js b/ui/pages/import-token/import-token.component.js similarity index 80% rename from ui/pages/add-token/add-token.component.js rename to ui/pages/import-token/import-token.component.js index 57852de20..3f50ba607 100644 --- a/ui/pages/add-token/add-token.component.js +++ b/ui/pages/import-token/import-token.component.js @@ -1,9 +1,15 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { getTokenTrackerLink } from '@metamask/etherscan-link'; -import { checkExistingAddresses } from '../../helpers/utils/util'; +import { + checkExistingAddresses, + getURLHostName, +} from '../../helpers/utils/util'; import { tokenInfoGetter } from '../../helpers/utils/token-util'; -import { CONFIRM_ADD_TOKEN_ROUTE } from '../../helpers/constants/routes'; +import { + CONFIRM_IMPORT_TOKEN_ROUTE, + EXPERIMENTAL_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'; @@ -21,7 +27,7 @@ const emptyAddr = '0x0000000000000000000000000000000000000000'; const MIN_DECIMAL_VALUE = 0; const MAX_DECIMAL_VALUE = 36; -class AddToken extends Component { +class ImportToken extends Component { static contextTypes = { t: PropTypes.func, }; @@ -37,6 +43,11 @@ class AddToken extends Component { mostRecentOverviewPage: PropTypes.string.isRequired, chainId: PropTypes.string, rpcPrefs: PropTypes.object, + tokenList: PropTypes.object, + }; + + static defaultProps = { + tokenList: {}, }; state = { @@ -137,7 +148,10 @@ class AddToken extends Component { return; } - const { setPendingTokens, history } = this.props; + const { setPendingTokens, history, tokenList } = this.props; + const tokenAddressList = Object.keys(tokenList).map((address) => + address.toLowerCase(), + ); const { customAddress: address, customSymbol: symbol, @@ -151,12 +165,16 @@ class AddToken extends Component { decimals, }; - setPendingTokens({ customToken, selectedTokens }); - history.push(CONFIRM_ADD_TOKEN_ROUTE); + setPendingTokens({ customToken, selectedTokens, tokenAddressList }); + history.push(CONFIRM_IMPORT_TOKEN_ROUTE); } async attemptToAutoFillTokenParams(address) { - const { symbol = '', decimals } = await this.tokenInfoGetter(address); + const { tokenList } = this.props; + const { symbol = '', decimals } = await this.tokenInfoGetter( + address, + tokenList, + ); const symbolAutoFilled = Boolean(symbol); const decimalAutoFilled = Boolean(decimals); @@ -262,11 +280,29 @@ class AddToken extends Component { { blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null }, ); const blockExplorerLabel = rpcPrefs?.blockExplorerUrl - ? new URL(blockExplorerTokenLink).hostname + ? getURLHostName(blockExplorerTokenLink) : this.context.t('etherscan'); return ( -
    +
    + + {this.context.t('learnScamRisk')} + , + ])} + type="warning" + withRightButton + useIcon + iconFillColor="#f8c000" + /> - +
    + {this.context.t('tokenSymbol')} {symbolAutoFilled && !forceEditSymbol && (
    this.setState({ forceEditSymbol: true })} > {this.context.t('edit')} @@ -333,8 +369,8 @@ class AddToken extends Component { {this.context.t('verifyThisTokenDecimalOn', [
    @@ -355,17 +391,35 @@ class AddToken extends Component { } renderSearchToken() { + const { tokenList, history } = this.props; const { tokenSelectorError, selectedTokens, searchResults } = this.state; - return ( -
    +
    + history.push(EXPERIMENTAL_ROUTE)} + > + {this.context.t('enableFromSettings')} + , + ])} + type={false} + withRightButton + useIcon + iconFillColor="#037DD6" + className="import-token__token-detection-announcement" + /> this.setState({ searchResults: results }) } error={tokenSelectorError} + tokenList={tokenList} /> -
    +
    this.handleNext()} + hideCancel disabled={Boolean(this.hasError()) || !this.hasSelected()} - onCancel={() => { + onClose={() => { clearPendingTokens(); history.push(mostRecentOverviewPage); }} @@ -414,4 +469,4 @@ class AddToken extends Component { } } -export default AddToken; +export default ImportToken; diff --git a/ui/pages/add-token/add-token.container.js b/ui/pages/import-token/import-token.container.js similarity index 66% rename from ui/pages/add-token/add-token.container.js rename to ui/pages/import-token/import-token.container.js index 08f6505e3..8a0099f98 100644 --- a/ui/pages/add-token/add-token.container.js +++ b/ui/pages/import-token/import-token.container.js @@ -3,10 +3,10 @@ import { connect } from 'react-redux'; import { setPendingTokens, clearPendingTokens } from '../../store/actions'; import { getMostRecentOverviewPage } from '../../ducks/history/history'; import { - getIsMainnet, getRpcPrefsForCurrentProvider, + getIsMainnet, } from '../../selectors/selectors'; -import AddToken from './add-token.component'; +import ImportToken from './import-token.component'; const mapStateToProps = (state) => { const { @@ -15,16 +15,25 @@ const mapStateToProps = (state) => { tokens, pendingTokens, provider: { chainId }, + useTokenDetection, + tokenList, }, } = state; + const showSearchTabCustomNetwork = + useTokenDetection && Boolean(Object.keys(tokenList).length); + const showSearchTab = + getIsMainnet(state) || + showSearchTabCustomNetwork || + process.env.IN_TEST === 'true'; return { identities, mostRecentOverviewPage: getMostRecentOverviewPage(state), tokens, pendingTokens, - showSearchTab: getIsMainnet(state) || process.env.IN_TEST === 'true', + showSearchTab, chainId, rpcPrefs: getRpcPrefsForCurrentProvider(state), + tokenList, }; }; @@ -35,4 +44,4 @@ const mapDispatchToProps = (dispatch) => { }; }; -export default connect(mapStateToProps, mapDispatchToProps)(AddToken); +export default connect(mapStateToProps, mapDispatchToProps)(ImportToken); diff --git a/ui/pages/import-token/import-token.stories.js b/ui/pages/import-token/import-token.stories.js new file mode 100644 index 000000000..d40c5560c --- /dev/null +++ b/ui/pages/import-token/import-token.stories.js @@ -0,0 +1,13 @@ +import React from 'react'; +import { boolean } from '@storybook/addon-knobs'; + +import ImportToken from './import-token.component'; + +export default { + title: 'Import Token', + id: __filename, +}; + +export const ImportTokenComponent = () => { + return ; +}; diff --git a/ui/pages/add-token/add-token.test.js b/ui/pages/import-token/import-token.test.js similarity index 80% rename from ui/pages/add-token/add-token.test.js rename to ui/pages/import-token/import-token.test.js index 4b36e7f39..c6be1f1bb 100644 --- a/ui/pages/add-token/add-token.test.js +++ b/ui/pages/import-token/import-token.test.js @@ -3,9 +3,9 @@ import { Provider } from 'react-redux'; import sinon from 'sinon'; import configureMockStore from 'redux-mock-store'; import { mountWithRouter } from '../../../test/lib/render-helpers'; -import AddToken from './add-token.container'; +import ImportToken from './import-token.container'; -describe('Add Token', () => { +describe('Import Token', () => { let wrapper; const state = { @@ -26,13 +26,14 @@ describe('Add Token', () => { identities: {}, mostRecentOverviewPage: '/', showSearchTab: true, + tokenList: {}, }; - describe('Add Token', () => { + describe('Import Token', () => { beforeAll(() => { wrapper = mountWithRouter( - + , store, ); @@ -46,7 +47,7 @@ describe('Add Token', () => { it('next button is disabled when no fields are populated', () => { const nextButton = wrapper.find( - '.button.btn-secondary.page-container__footer-button', + '.button.btn-primary.page-container__footer-button', ); expect(nextButton.props().disabled).toStrictEqual(true); @@ -59,7 +60,7 @@ describe('Add Token', () => { customAddress.simulate('change', event); expect( - wrapper.find('AddToken').instance().state.customAddress, + wrapper.find('ImportToken').instance().state.customAddress, ).toStrictEqual(tokenAddress); }); @@ -70,7 +71,7 @@ describe('Add Token', () => { customAddress.last().simulate('change', event); expect( - wrapper.find('AddToken').instance().state.customSymbol, + wrapper.find('ImportToken').instance().state.customSymbol, ).toStrictEqual(tokenSymbol); }); @@ -81,27 +82,25 @@ describe('Add Token', () => { customAddress.last().simulate('change', event); expect( - wrapper.find('AddToken').instance().state.customDecimals, + wrapper.find('ImportToken').instance().state.customDecimals, ).toStrictEqual(Number(tokenPrecision)); }); it('next', () => { const nextButton = wrapper.find( - '.button.btn-secondary.page-container__footer-button', + '.button.btn-primary.page-container__footer-button', ); nextButton.simulate('click'); expect(props.setPendingTokens.calledOnce).toStrictEqual(true); expect(props.history.push.calledOnce).toStrictEqual(true); expect(props.history.push.getCall(0).args[0]).toStrictEqual( - '/confirm-add-token', + '/confirm-import-token', ); }); it('cancels', () => { - const cancelButton = wrapper.find( - 'button.btn-default.page-container__footer-button', - ); + const cancelButton = wrapper.find('.page-container__header-close'); cancelButton.simulate('click'); expect(props.clearPendingTokens.calledOnce).toStrictEqual(true); diff --git a/ui/pages/import-token/index.js b/ui/pages/import-token/index.js new file mode 100644 index 000000000..8fa4ee8c8 --- /dev/null +++ b/ui/pages/import-token/index.js @@ -0,0 +1,3 @@ +import ImportToken from './import-token.container'; + +export default ImportToken; diff --git a/ui/pages/add-token/index.scss b/ui/pages/import-token/index.scss similarity index 88% rename from ui/pages/add-token/index.scss rename to ui/pages/import-token/index.scss index 581832ca4..378801a33 100644 --- a/ui/pages/add-token/index.scss +++ b/ui/pages/import-token/index.scss @@ -1,6 +1,6 @@ @import 'token-list/index'; -.add-token { +.import-token { $self: &; &__custom-token-form { @@ -21,7 +21,7 @@ } &__search-token { - padding: 16px; + padding: 0 16px 16px 16px; } &__token-list { @@ -54,4 +54,8 @@ color: $primary-blue; padding-left: 0; } + + &__token-detection-announcement { + margin-bottom: 16px; + } } diff --git a/ui/pages/add-token/token-list/index.js b/ui/pages/import-token/token-list/index.js similarity index 100% rename from ui/pages/add-token/token-list/index.js rename to ui/pages/import-token/token-list/index.js diff --git a/ui/pages/add-token/token-list/index.scss b/ui/pages/import-token/token-list/index.scss similarity index 100% rename from ui/pages/add-token/token-list/index.scss rename to ui/pages/import-token/token-list/index.scss diff --git a/ui/pages/add-token/token-list/token-list-placeholder/index.js b/ui/pages/import-token/token-list/token-list-placeholder/index.js similarity index 100% rename from ui/pages/add-token/token-list/token-list-placeholder/index.js rename to ui/pages/import-token/token-list/token-list-placeholder/index.js diff --git a/ui/pages/add-token/token-list/token-list-placeholder/index.scss b/ui/pages/import-token/token-list/token-list-placeholder/index.scss similarity index 87% rename from ui/pages/add-token/token-list/token-list-placeholder/index.scss rename to ui/pages/import-token/token-list/token-list-placeholder/index.scss index bbc1e8304..f2eecb446 100644 --- a/ui/pages/add-token/token-list/token-list-placeholder/index.scss +++ b/ui/pages/import-token/token-list/token-list-placeholder/index.scss @@ -16,7 +16,7 @@ margin-top: 8px; opacity: 0.5; - @media screen and (max-width: 575px) { + @media screen and (max-width: $break-small) { width: 60%; } } diff --git a/ui/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js b/ui/pages/import-token/token-list/token-list-placeholder/token-list-placeholder.component.js similarity index 100% rename from ui/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js rename to ui/pages/import-token/token-list/token-list-placeholder/token-list-placeholder.component.js diff --git a/ui/pages/import-token/token-list/token-list-placeholder/token-list-placeholder.stories.js b/ui/pages/import-token/token-list/token-list-placeholder/token-list-placeholder.stories.js new file mode 100644 index 000000000..e0003e31b --- /dev/null +++ b/ui/pages/import-token/token-list/token-list-placeholder/token-list-placeholder.stories.js @@ -0,0 +1,11 @@ +import React from 'react'; +import TokenListPlaceholder from './token-list-placeholder.component'; + +export default { + title: 'TokenListPlaceholder', + id: __filename, +}; + +export const TokenListPlaceholderComponent = () => { + return ; +}; diff --git a/ui/pages/add-token/token-list/token-list.component.js b/ui/pages/import-token/token-list/token-list.component.js similarity index 81% rename from ui/pages/add-token/token-list/token-list.component.js rename to ui/pages/import-token/token-list/token-list.component.js index 819be4173..4d6827a99 100644 --- a/ui/pages/add-token/token-list/token-list.component.js +++ b/ui/pages/import-token/token-list/token-list.component.js @@ -14,6 +14,7 @@ export default class TokenList extends Component { results: PropTypes.array, selectedTokens: PropTypes.object, onToggleToken: PropTypes.func, + useTokenDetection: PropTypes.bool, }; render() { @@ -22,6 +23,7 @@ export default class TokenList extends Component { selectedTokens = {}, onToggleToken, tokens = [], + useTokenDetection, } = this.props; return results.length === 0 ? ( @@ -35,13 +37,17 @@ export default class TokenList extends Component { {Array(6) .fill(undefined) .map((_, i) => { - const { logo, symbol, name, address } = results[i] || {}; + const { iconUrl, symbol, name, address } = results[i] || {}; + // token from dynamic api list is fetched when useTokenDetection is true + const iconPath = useTokenDetection + ? iconUrl + : `images/contract/${iconUrl}`; const tokenAlreadyAdded = checkExistingAddresses(address, tokens); const onClick = () => !tokenAlreadyAdded && onToggleToken(results[i]); return ( - Boolean(logo || symbol || name) && ( + Boolean(iconUrl || symbol || name) && (
    diff --git a/ui/pages/add-token/token-list/token-list.container.js b/ui/pages/import-token/token-list/token-list.container.js similarity index 74% rename from ui/pages/add-token/token-list/token-list.container.js rename to ui/pages/import-token/token-list/token-list.container.js index 4896067f7..565bd4262 100644 --- a/ui/pages/add-token/token-list/token-list.container.js +++ b/ui/pages/import-token/token-list/token-list.container.js @@ -2,9 +2,10 @@ import { connect } from 'react-redux'; import TokenList from './token-list.component'; const mapStateToProps = ({ metamask }) => { - const { tokens } = metamask; + const { tokens, useTokenDetection } = metamask; return { tokens, + useTokenDetection, }; }; diff --git a/ui/pages/add-token/token-search/index.js b/ui/pages/import-token/token-search/index.js similarity index 100% rename from ui/pages/add-token/token-search/index.js rename to ui/pages/import-token/token-search/index.js diff --git a/ui/pages/add-token/token-search/token-search.component.js b/ui/pages/import-token/token-search/token-search.component.js similarity index 63% rename from ui/pages/add-token/token-search/token-search.component.js rename to ui/pages/import-token/token-search/token-search.component.js index 76bfdab09..6a6a2c0dc 100644 --- a/ui/pages/add-token/token-search/token-search.component.js +++ b/ui/pages/import-token/token-search/token-search.component.js @@ -1,26 +1,9 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import contractMap from '@metamask/contract-metadata'; import Fuse from 'fuse.js'; import InputAdornment from '@material-ui/core/InputAdornment'; import TextField from '../../../components/ui/text-field'; - -const contractList = Object.entries(contractMap) - .map(([address, tokenData]) => ({ ...tokenData, address })) - .filter((tokenData) => Boolean(tokenData.erc20)); - -const fuse = new Fuse(contractList, { - shouldSort: true, - threshold: 0.45, - location: 0, - distance: 100, - maxPatternLength: 32, - minMatchCharLength: 1, - keys: [ - { name: 'name', weight: 0.5 }, - { name: 'symbol', weight: 0.5 }, - ], -}); +import { isEqualCaseInsensitive } from '../../../helpers/utils/util'; export default class TokenSearch extends Component { static contextTypes = { @@ -34,17 +17,40 @@ export default class TokenSearch extends Component { static propTypes = { onSearch: PropTypes.func, error: PropTypes.string, + tokenList: PropTypes.object, }; state = { searchQuery: '', }; + constructor(props) { + super(props); + const { tokenList } = this.props; + this.tokenList = Object.values(tokenList); + this.tokenSearchFuse = new Fuse(this.tokenList, { + shouldSort: true, + threshold: 0.45, + location: 0, + distance: 100, + maxPatternLength: 32, + minMatchCharLength: 1, + keys: [ + { name: 'name', weight: 0.5 }, + { name: 'symbol', weight: 0.5 }, + ], + }); + } + handleSearch(searchQuery) { this.setState({ searchQuery }); - const fuseSearchResult = fuse.search(searchQuery); - const addressSearchResult = contractList.filter((token) => { - return token.address.toLowerCase() === searchQuery.toLowerCase(); + const fuseSearchResult = this.tokenSearchFuse.search(searchQuery); + const addressSearchResult = this.tokenList.filter((token) => { + return ( + token.address && + searchQuery && + isEqualCaseInsensitive(token.address, searchQuery) + ); }); const results = [...addressSearchResult, ...fuseSearchResult]; this.props.onSearch({ searchQuery, results }); diff --git a/ui/pages/import-token/token-search/token-search.stories.js b/ui/pages/import-token/token-search/token-search.stories.js new file mode 100644 index 000000000..9d0e7cbf2 --- /dev/null +++ b/ui/pages/import-token/token-search/token-search.stories.js @@ -0,0 +1,11 @@ +import React from 'react'; +import TokenSearch from './token-search.component'; + +export default { + title: 'TokenSearch', + id: __filename, +}; + +export const TokenSearchComponent = () => { + return ; +}; diff --git a/ui/pages/keychains/index.scss b/ui/pages/keychains/index.scss index 263a1073c..a38d6a5ab 100644 --- a/ui/pages/keychains/index.scss +++ b/ui/pages/keychains/index.scss @@ -33,7 +33,7 @@ } } -@media only screen and (max-width: 575px) { +@media only screen and (max-width: $break-small) { .import-account { margin: 24px; display: flex; @@ -68,7 +68,7 @@ } } -@media only screen and (max-width: 575px) { +@media only screen and (max-width: $break-small) { .import-account__input { width: 100%; } diff --git a/ui/pages/mobile-sync/mobile-sync.component.js b/ui/pages/mobile-sync/mobile-sync.component.js index 0eab239dd..bb61df1ae 100644 --- a/ui/pages/mobile-sync/mobile-sync.component.js +++ b/ui/pages/mobile-sync/mobile-sync.component.js @@ -28,6 +28,7 @@ export default class MobileSyncPage extends Component { requestRevealSeedWords: PropTypes.func.isRequired, exportAccounts: PropTypes.func.isRequired, keyrings: PropTypes.array, + hideWarning: PropTypes.func.isRequired, }; state = { @@ -200,9 +201,9 @@ export default class MobileSyncPage extends Component { sendByPost: false, // true to send via post storeInHistory: false, }, - (status, response) => { + (status, _response) => { if (status.error) { - reject(response); + reject(status.errorData); } else { resolve(); } @@ -223,13 +224,16 @@ export default class MobileSyncPage extends Component { network, preferences, transactions, + tokens, } = await this.props.fetchInfoToSync(); + const { t } = this.context; const allDataStr = JSON.stringify({ accounts, network, preferences, transactions, + tokens, udata: { pwd: this.state.password, seed: this.state.seedWords, @@ -244,7 +248,7 @@ export default class MobileSyncPage extends Component { await this.sendMessage(chunks[i], i + 1, totalChunks); } } catch (e) { - this.props.displayWarning('Sync failed :('); + this.props.displayWarning(`${t('syncFailed')} :(`); this.setState({ syncing: false }); this.syncing = false; this.notifyError(e.toString()); @@ -265,9 +269,9 @@ export default class MobileSyncPage extends Component { sendByPost: false, // true to send via post storeInHistory: false, }, - (status, response) => { + (status, _response) => { if (status.error) { - reject(response); + reject(status.errorData); } else { resolve(); } @@ -277,6 +281,9 @@ export default class MobileSyncPage extends Component { } componentWillUnmount() { + if (this.state.error) { + this.props.hideWarning(); + } this.clearTimeouts(); this.disconnectWebsockets(); } diff --git a/ui/pages/mobile-sync/mobile-sync.container.js b/ui/pages/mobile-sync/mobile-sync.container.js index 1172a97dc..9f70adae9 100644 --- a/ui/pages/mobile-sync/mobile-sync.container.js +++ b/ui/pages/mobile-sync/mobile-sync.container.js @@ -4,6 +4,7 @@ import { requestRevealSeedWords, fetchInfoToSync, exportAccounts, + hideWarning, } from '../../store/actions'; import { getMostRecentOverviewPage } from '../../ducks/history/history'; import { getMetaMaskKeyrings } from '../../selectors'; @@ -17,6 +18,7 @@ const mapDispatchToProps = (dispatch) => { displayWarning: (message) => dispatch(displayWarning(message || null)), exportAccounts: (password, addresses) => dispatch(exportAccounts(password, addresses)), + hideWarning: () => dispatch(hideWarning()), }; }; diff --git a/ui/pages/mobile-sync/mobile-sync.stories.js b/ui/pages/mobile-sync/mobile-sync.stories.js new file mode 100644 index 000000000..762a3bc26 --- /dev/null +++ b/ui/pages/mobile-sync/mobile-sync.stories.js @@ -0,0 +1,14 @@ +import React from 'react'; +import { action } from '@storybook/addon-actions'; +import MobileSyncPage from './mobile-sync.component'; + +export default { + title: 'Mobile Sync', + id: __filename, +}; + +export const MobileSyncComponent = () => { + return ( + + ); +}; diff --git a/ui/pages/pages.scss b/ui/pages/pages.scss index 09c1c58bc..5cbc7932e 100644 --- a/ui/pages/pages.scss +++ b/ui/pages/pages.scss @@ -1,7 +1,7 @@ /** Please import your files in alphabetical order **/ -@import 'add-token/index'; +@import 'import-token/index'; @import 'asset/asset'; -@import 'confirm-add-token/index'; +@import 'confirm-import-token/index'; @import 'confirm-approve/index'; @import 'confirm-decrypt-message/confirm-decrypt-message'; @import 'confirm-encryption-public-key/confirm-encryption-public-key'; diff --git a/ui/pages/permissions-connect/choose-account/index.scss b/ui/pages/permissions-connect/choose-account/index.scss index bb4b05a42..d02f5136b 100644 --- a/ui/pages/permissions-connect/choose-account/index.scss +++ b/ui/pages/permissions-connect/choose-account/index.scss @@ -18,7 +18,7 @@ color: $Grey-300; } - @media screen and (min-width: 576px) { + @media screen and (min-width: $break-large) { width: 426px; } @@ -155,7 +155,7 @@ flex-direction: column; justify-content: flex-end; - @media screen and (min-width: 576px) { + @media screen and (min-width: $break-large) { flex-direction: column-reverse; justify-content: space-between; padding-bottom: 20px; @@ -171,7 +171,7 @@ margin-top: 8px; border-top: 1px solid #d6d9dc; - @media screen and (min-width: 576px) { + @media screen and (min-width: $break-large) { border-top: none; } diff --git a/ui/pages/permissions-connect/index.scss b/ui/pages/permissions-connect/index.scss index dd0bc7548..197764b8e 100644 --- a/ui/pages/permissions-connect/index.scss +++ b/ui/pages/permissions-connect/index.scss @@ -9,7 +9,7 @@ display: flex; flex-direction: column; - @media screen and (min-width: 576px) { + @media screen and (min-width: $break-large) { .page-container { max-height: none; min-height: auto; diff --git a/ui/pages/permissions-connect/permissions-connect.stories.js b/ui/pages/permissions-connect/permissions-connect.stories.js new file mode 100644 index 000000000..6eff21540 --- /dev/null +++ b/ui/pages/permissions-connect/permissions-connect.stories.js @@ -0,0 +1,83 @@ +import React from 'react'; +import { action } from '@storybook/addon-actions'; +import { PermissionPageContainerContent } from '../../components/app/permission-page-container'; +import PermissionsConnectFooter from '../../components/app/permissions-connect-footer'; +import { PageContainerFooter } from '../../components/ui/page-container'; +import ChooseAccount from './choose-account'; + +export default { + title: 'Permissions Connect', + id: __filename, +}; + +export const ChooseAccountComponent = () => { + return ( + + ); +}; + +export const PermissionPageContainerComponent = () => { + return ( +
    + +
    + + +
    +
    + ); +}; diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index d70256e17..4b631c302 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -8,7 +8,6 @@ import FirstTimeFlow from '../first-time-flow'; import SendTransactionScreen from '../send'; import Swaps from '../swaps'; import ConfirmTransaction from '../confirm-transaction'; -import Sidebar from '../../components/app/sidebars'; import Home from '../home'; import Settings from '../settings'; import Authenticated from '../../helpers/higher-order-components/authenticated'; @@ -18,8 +17,8 @@ import PermissionsConnect from '../permissions-connect'; import RestoreVaultPage from '../keychains/restore-vault'; import RevealSeedConfirmation from '../keychains/reveal-seed'; import MobileSyncPage from '../mobile-sync'; -import AddTokenPage from '../add-token'; -import ConfirmAddTokenPage from '../confirm-add-token'; +import ImportTokenPage from '../import-token'; +import ConfirmImportTokenPage from '../confirm-import-token'; import ConfirmAddSuggestedTokenPage from '../confirm-add-suggested-token'; import CreateAccountPage from '../create-account'; import Loading from '../../components/ui/loading-screen'; @@ -34,10 +33,9 @@ import Alerts from '../../components/app/alerts'; import Asset from '../asset'; import { - ADD_TOKEN_ROUTE, + IMPORT_TOKEN_ROUTE, ASSET_ROUTE, CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE, - CONFIRM_ADD_TOKEN_ROUTE, CONFIRM_TRANSACTION_ROUTE, CONNECT_ROUTE, DEFAULT_ROUTE, @@ -54,6 +52,7 @@ import { UNLOCK_ROUTE, BUILD_QUOTE_ROUTE, CONFIRMATION_V_NEXT_ROUTE, + CONFIRM_IMPORT_TOKEN_ROUTE, } from '../../helpers/constants/routes'; import { @@ -61,7 +60,7 @@ import { ENVIRONMENT_TYPE_POPUP, } from '../../../shared/constants/app'; import { getEnvironmentType } from '../../../app/scripts/lib/util'; -import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction'; +import { isBeta } from '../../helpers/utils/build-types'; import ConfirmationPage from '../confirmation'; export default class Routes extends Component { @@ -75,15 +74,12 @@ export default class Routes extends Component { isNetworkLoading: PropTypes.bool, provider: PropTypes.object, frequentRpcListDetail: PropTypes.array, - sidebar: PropTypes.object, alertOpen: PropTypes.bool, - hideSidebar: PropTypes.func, isUnlocked: PropTypes.bool, setLastActiveTime: PropTypes.func, history: PropTypes.object, location: PropTypes.object, lockMetaMask: PropTypes.func, - submittedPendingTransactions: PropTypes.array, isMouseUser: PropTypes.bool, setMouseUserState: PropTypes.func, providerId: PropTypes.string, @@ -150,10 +146,14 @@ export default class Routes extends Component { exact /> - + id === sidebarTransaction.id, - ); - const { os, browser } = browserEnvironment; return (
    setMouseUserState(true)} @@ -331,14 +315,6 @@ export default class Routes extends Component { } /> )} - dispatch(lockMetamask(false)), - hideSidebar: () => dispatch(hideSidebar()), setCurrentCurrencyToUSD: () => dispatch(setCurrentCurrency('usd')), setMouseUserState: (isMouseUser) => dispatch(setMouseUserState(isMouseUser)), diff --git a/ui/pages/send/send-content/add-recipient/add-recipient.stories.js b/ui/pages/send/send-content/add-recipient/add-recipient.stories.js new file mode 100644 index 000000000..8f1555989 --- /dev/null +++ b/ui/pages/send/send-content/add-recipient/add-recipient.stories.js @@ -0,0 +1,37 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { text } from '@storybook/addon-knobs'; + +import configureStore from '../../../../store/store'; + +import testData from '../../../../../.storybook/test-data'; +import AddRecipient from './add-recipient.component'; + +const store = configureStore(testData); + +export default { + title: 'AddRecipient', + id: __filename, + decorators: [(story) => {story()}], +}; + +export const AddRecipientComponent = () => { + const { metamask } = store.getState(); + const { addressBook, recipient } = metamask; + return ( +
    + undefined} + nonContacts={[addressBook]} + ownedAccounts={[addressBook]} + addressBook={[addressBook]} + updateGas={() => undefined} + // ToError and ToWarning wording must match on translation + ensError={text('To Error', 'loading')} + ensWarning={text('To Warning', 'loading')} + /> +
    + ); +}; diff --git a/ui/pages/send/send-content/add-recipient/ens-input.component.js b/ui/pages/send/send-content/add-recipient/ens-input.component.js index f2e84dc49..5747d4044 100644 --- a/ui/pages/send/send-content/add-recipient/ens-input.component.js +++ b/ui/pages/send/send-content/add-recipient/ens-input.component.js @@ -36,15 +36,18 @@ export default class EnsInput extends Component { } onPaste = (event) => { - event.clipboardData.items[0].getAsString((text) => { - const input = text.trim(); - if ( - !isBurnAddress(input) && - isValidHexAddress(input, { mixedCaseUseChecksum: true }) - ) { - this.props.onPaste(input); - } - }); + if (event.clipboardData.items?.length) { + const clipboardItem = event.clipboardData.items[0]; + clipboardItem?.getAsString((text) => { + const input = text.trim(); + if ( + !isBurnAddress(input) && + isValidHexAddress(input, { mixedCaseUseChecksum: true }) + ) { + this.props.onPaste(input); + } + }); + } }; onChange = ({ target: { value } }) => { @@ -125,6 +128,7 @@ export default class EnsInput extends Component { placeholder={t('recipientAddressPlaceholder')} onChange={this.onChange} onPaste={this.onPaste} + spellCheck="false" value={selectedAddress || userInput} autoFocus data-testid="ens-input" diff --git a/ui/pages/send/send-content/send-asset-row/send-asset-row.component.js b/ui/pages/send/send-content/send-asset-row/send-asset-row.component.js index e6c1d9bb9..c71fa23cb 100644 --- a/ui/pages/send/send-content/send-asset-row/send-asset-row.component.js +++ b/ui/pages/send/send-content/send-asset-row/send-asset-row.component.js @@ -6,6 +6,7 @@ import TokenBalance from '../../../../components/ui/token-balance'; import UserPreferencedCurrencyDisplay from '../../../../components/app/user-preferenced-currency-display'; import { ERC20, PRIMARY } from '../../../../helpers/constants/common'; import { ASSET_TYPES } from '../../../../ducks/send'; +import { isEqualCaseInsensitive } from '../../../../helpers/utils/util'; export default class SendAssetRow extends Component { static propTypes = { @@ -14,10 +15,10 @@ export default class SendAssetRow extends Component { address: PropTypes.string, decimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), symbol: PropTypes.string, + image: PropTypes.string, }), ).isRequired, accounts: PropTypes.object.isRequired, - assetImages: PropTypes.object, selectedAddress: PropTypes.string.isRequired, sendAssetAddress: PropTypes.string, updateSendAsset: PropTypes.func.isRequired, @@ -85,8 +86,8 @@ export default class SendAssetRow extends Component { renderSendToken() { const { sendAssetAddress } = this.props; - const token = this.props.tokens.find( - ({ address }) => address === sendAssetAddress, + const token = this.props.tokens.find(({ address }) => + isEqualCaseInsensitive(address, sendAssetAddress), ); return (
    this.selectToken(ASSET_TYPES.TOKEN, token)} >
    - +
    {symbol}
    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 674d3bec1..b522cc33e 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 @@ -3,7 +3,6 @@ import { getNativeCurrency } from '../../../../ducks/metamask/metamask'; import { getMetaMaskAccounts, getNativeCurrencyImage, - getAssetImages, } from '../../../../selectors'; import { updateSendAsset, getSendAssetAddress } from '../../../../ducks/send'; import SendAssetRow from './send-asset-row.component'; @@ -16,7 +15,6 @@ function mapStateToProps(state) { accounts: getMetaMaskAccounts(state), nativeCurrency: getNativeCurrency(state), nativeCurrencyImage: getNativeCurrencyImage(state), - assetImages: getAssetImages(state), }; } diff --git a/ui/pages/send/send-content/send-content.stories.js b/ui/pages/send/send-content/send-content.stories.js new file mode 100644 index 000000000..abaa02b1b --- /dev/null +++ b/ui/pages/send/send-content/send-content.stories.js @@ -0,0 +1,24 @@ +import React from 'react'; +import { boolean, text } from '@storybook/addon-knobs'; + +import SendContent from './send-content.component'; + +export default { + title: 'SendContent', + id: __filename, +}; + +export const SendContentComponent = () => { + return ( + + ); +}; diff --git a/ui/pages/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.stories.js b/ui/pages/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.stories.js new file mode 100644 index 000000000..507d1b948 --- /dev/null +++ b/ui/pages/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.stories.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { action } from '@storybook/addon-actions'; +import { number, boolean } from '@storybook/addon-knobs'; +import GasFeeDisplay from './gas-fee-display.component'; + +export default { + title: 'GasFeeDisplay', + id: __filename, +}; + +export const GasFeeDisplayComponent = () => { + const gasTotal = number('Gas Total', 10000000000); + return ( + + ); +}; diff --git a/ui/pages/send/send-content/send-gas-row/send-gas-row.stories.js b/ui/pages/send/send-content/send-gas-row/send-gas-row.stories.js new file mode 100644 index 000000000..9c726c9d1 --- /dev/null +++ b/ui/pages/send/send-content/send-gas-row/send-gas-row.stories.js @@ -0,0 +1,77 @@ +/* eslint-disable react/prop-types */ + +import React, { useEffect, useState } from 'react'; +import { Provider } from 'react-redux'; +import { boolean } from '@storybook/addon-knobs'; +import testData from '../../../../../.storybook/test-data'; + +import configureStore from '../../../../store/store'; +import { calcGasTotal } from '../../send.utils'; +import { updateMetamaskState } from '../../../../store/actions'; +import { GAS_INPUT_MODES } from '../../../../ducks/send'; +import SendGasRow from './send-gas-row.component'; + +const store = configureStore(testData); + +export default { + title: 'SendGasRow', + id: __filename, + decorators: [(story) => {story()}], +}; + +export const SendGasRowComponent = () => { + const state = store.getState(); + const { metamask } = state; + const { send } = metamask; + const [sendState, setSendState] = useState(send); + + const insufficientBalance = boolean('Is Insufficient Balance', false); + + useEffect(() => { + const newState = Object.assign(metamask, { + send: sendState, + }); + store.dispatch(updateMetamaskState(newState)); + }, [sendState, metamask]); + + const updateGasPrice = ({ gasPrice, gasLimit }) => { + let newGasTotal = send.gasTotal; + if (send.gasLimit) { + newGasTotal = calcGasTotal(gasLimit, gasPrice); + } + const newState = { + ...state.metamask.send, + gasPrice, + gasTotal: newGasTotal, + }; + + setSendState(newState); + }; + + const updateGasLimit = (limit) => { + let newGasTotal = send.gasTotal; + if (send.gasPrice) { + newGasTotal = calcGasTotal(limit, send.gasPrice); + } + const newState = { + ...state.metamask.send, + gasLimit: limit, + gasTotal: newGasTotal, + }; + + setSendState(newState); + }; + + return ( +
    + +
    + ); +}; diff --git a/ui/pages/send/send-footer/send-footer.component.js b/ui/pages/send/send-footer/send-footer.component.js index 840146f5f..cde413e0a 100644 --- a/ui/pages/send/send-footer/send-footer.component.js +++ b/ui/pages/send/send-footer/send-footer.component.js @@ -2,7 +2,11 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { isEqual } from 'lodash'; import PageContainerFooter from '../../../components/ui/page-container/page-container-footer'; -import { CONFIRM_TRANSACTION_ROUTE } from '../../../helpers/constants/routes'; +import { + CONFIRM_TRANSACTION_ROUTE, + DEFAULT_ROUTE, +} from '../../../helpers/constants/routes'; +import { SEND_STAGES } from '../../../ducks/send'; export default class SendFooter extends Component { static propTypes = { @@ -13,9 +17,12 @@ export default class SendFooter extends Component { sign: PropTypes.func, to: PropTypes.string, toAccounts: PropTypes.array, + sendStage: PropTypes.string, sendErrors: PropTypes.object, gasEstimateType: PropTypes.string, mostRecentOverviewPage: PropTypes.string.isRequired, + cancelTx: PropTypes.func, + draftTransactionID: PropTypes.string, }; static contextTypes = { @@ -24,9 +31,21 @@ export default class SendFooter extends Component { }; onCancel() { - const { resetSendState, history, mostRecentOverviewPage } = this.props; + const { + cancelTx, + draftTransactionID, + history, + mostRecentOverviewPage, + resetSendState, + sendStage, + } = this.props; + + if (draftTransactionID) cancelTx({ id: draftTransactionID }); resetSendState(); - history.push(mostRecentOverviewPage); + + const nextRoute = + sendStage === SEND_STAGES.EDIT ? DEFAULT_ROUTE : mostRecentOverviewPage; + history.push(nextRoute); } async onSubmit(event) { @@ -85,11 +104,14 @@ export default class SendFooter extends Component { } render() { + const { t } = this.context; + const { sendStage } = this.props; return ( this.onCancel()} onSubmit={(e) => this.onSubmit(e)} disabled={this.props.disabled} + cancelText={sendStage === SEND_STAGES.EDIT ? t('reject') : t('cancel')} /> ); } diff --git a/ui/pages/send/send-footer/send-footer.component.test.js b/ui/pages/send/send-footer/send-footer.component.test.js index fcd4472d6..23bc565ac 100644 --- a/ui/pages/send/send-footer/send-footer.component.test.js +++ b/ui/pages/send/send-footer/send-footer.component.test.js @@ -1,8 +1,12 @@ import React from 'react'; import { shallow } from 'enzyme'; import sinon from 'sinon'; -import { CONFIRM_TRANSACTION_ROUTE } from '../../../helpers/constants/routes'; +import { + CONFIRM_TRANSACTION_ROUTE, + DEFAULT_ROUTE, +} from '../../../helpers/constants/routes'; import PageContainerFooter from '../../../components/ui/page-container/page-container-footer'; +import { renderWithProvider } from '../../../../test/jest'; import SendFooter from './send-footer.component'; describe('SendFooter Component', () => { @@ -10,6 +14,7 @@ describe('SendFooter Component', () => { const propsMethodSpies = { addToAddressBookIfNew: sinon.spy(), + cancelTx: sinon.spy(), resetSendState: sinon.spy(), sign: sinon.spy(), update: sinon.spy(), @@ -20,31 +25,40 @@ describe('SendFooter Component', () => { }; const MOCK_EVENT = { preventDefault: () => undefined }; + const renderShallow = (props) => { + return shallow( + , + { context: { t: (str) => str, metricsEvent: () => ({}) } }, + ); + }; + beforeAll(() => { sinon.spy(SendFooter.prototype, 'onCancel'); sinon.spy(SendFooter.prototype, 'onSubmit'); }); beforeEach(() => { - wrapper = shallow( - , - { context: { t: (str) => str, metricsEvent: () => ({}) } }, - ); + wrapper = renderShallow(); }); afterEach(() => { propsMethodSpies.resetSendState.resetHistory(); + propsMethodSpies.cancelTx.resetHistory(); propsMethodSpies.addToAddressBookIfNew.resetHistory(); propsMethodSpies.resetSendState.resetHistory(); propsMethodSpies.sign.resetHistory(); @@ -65,6 +79,15 @@ describe('SendFooter Component', () => { expect(propsMethodSpies.resetSendState.callCount).toStrictEqual(1); }); + it('should call cancelTx', () => { + expect(propsMethodSpies.cancelTx.callCount).toStrictEqual(0); + wrapper.instance().onCancel(); + expect(propsMethodSpies.cancelTx.callCount).toStrictEqual(1); + expect(propsMethodSpies.cancelTx.getCall(0).args[0]?.id).toStrictEqual( + 'ID', + ); + }); + it('should call history.push', () => { expect(historySpies.push.callCount).toStrictEqual(0); wrapper.instance().onCancel(); @@ -73,6 +96,14 @@ describe('SendFooter Component', () => { 'mostRecentOverviewPage', ); }); + + it('should call history.push with DEFAULT_ROUTE in edit stage', () => { + wrapper = renderShallow({ sendStage: 'EDIT' }); + expect(historySpies.push.callCount).toStrictEqual(0); + wrapper.instance().onCancel(); + expect(historySpies.push.callCount).toStrictEqual(1); + expect(historySpies.push.getCall(0).args[0]).toStrictEqual(DEFAULT_ROUTE); + }); }); describe('onSubmit', () => { @@ -107,7 +138,9 @@ describe('SendFooter Component', () => { addToAddressBookIfNew={propsMethodSpies.addToAddressBookIfNew} amount="mockAmount" resetSendState={propsMethodSpies.resetSendState} + cancelTx={propsMethodSpies.cancelTx} disabled + draftTransactionID="ID" editingTransactionId="mockEditingTransactionId" errors={{}} from={{ address: 'mockAddress', balance: 'mockBalance' }} @@ -147,4 +180,28 @@ describe('SendFooter Component', () => { expect(SendFooter.prototype.onCancel.callCount).toStrictEqual(1); }); }); + + describe('Cancel Button', () => { + const renderFooter = (props) => + renderWithProvider( + , + ); + + it('has a cancel button in footer', () => { + const { getByText } = renderFooter(); + expect(getByText('Cancel')).toBeTruthy(); + }); + + it('has label changed to Reject in editing stage', () => { + const { getByText } = renderFooter({ sendStage: 'EDIT' }); + expect(getByText('Reject')).toBeTruthy(); + }); + }); }); diff --git a/ui/pages/send/send-footer/send-footer.container.js b/ui/pages/send/send-footer/send-footer.container.js index bcdb796e1..eba93e5d7 100644 --- a/ui/pages/send/send-footer/send-footer.container.js +++ b/ui/pages/send/send-footer/send-footer.container.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { addToAddressBook } from '../../../store/actions'; +import { addToAddressBook, cancelTx } from '../../../store/actions'; import { getRenderableEstimateDataForSmallButtonsFromGWEI, getDefaultActiveButtonIndex, @@ -7,10 +7,12 @@ import { import { resetSendState, getGasPrice, + getSendStage, getSendTo, getSendErrors, isSendFormInvalid, signTransaction, + getDraftTransactionID, } from '../../../ducks/send'; import { getMostRecentOverviewPage } from '../../../ducks/history/history'; import { addHexPrefix } from '../../../../app/scripts/lib/util'; @@ -43,7 +45,9 @@ function mapStateToProps(state) { disabled: isSendFormInvalid(state), to: getSendTo(state), toAccounts: getSendToAccounts(state), + sendStage: getSendStage(state), sendErrors: getSendErrors(state), + draftTransactionID: getDraftTransactionID(state), gasEstimateType, mostRecentOverviewPage: getMostRecentOverviewPage(state), }; @@ -52,6 +56,7 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { return { resetSendState: () => dispatch(resetSendState()), + cancelTx: (t) => dispatch(cancelTx(t)), sign: () => dispatch(signTransaction()), addToAddressBookIfNew: (newAddress, toAccounts, nickname = '') => { const hexPrefixedAddress = addHexPrefix(newAddress); 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 61c081719..d5eb9f8ab 100644 --- a/ui/pages/send/send-footer/send-footer.container.test.js +++ b/ui/pages/send/send-footer/send-footer.container.test.js @@ -1,6 +1,6 @@ import sinon from 'sinon'; -import { addToAddressBook } from '../../../store/actions'; +import { addToAddressBook, cancelTx } from '../../../store/actions'; import { resetSendState, signTransaction } from '../../../ducks/send'; let mapDispatchToProps; @@ -14,6 +14,7 @@ jest.mock('react-redux', () => ({ jest.mock('../../../store/actions.js', () => ({ addToAddressBook: jest.fn(), + cancelTx: jest.fn(), })); jest.mock('../../../ducks/metamask/metamask', () => ({ @@ -24,6 +25,8 @@ jest.mock('../../../ducks/send', () => ({ getGasPrice: (s) => `mockGasPrice:${s}`, getSendTo: (s) => `mockTo:${s}`, getSendErrors: (s) => `mockSendErrors:${s}`, + getSendStage: (s) => `mockStage:${s}`, + getDraftTransaction: (s) => ({ id: `draftTransaction:${s}` }), resetSendState: jest.fn(), signTransaction: jest.fn(), })); @@ -53,6 +56,16 @@ describe('send-footer container', () => { }); }); + describe('cancelTx()', () => { + it('should dispatch an action', () => { + const draftTansaction = { id: 'ID' }; + mapDispatchToPropsObject.cancelTx(draftTansaction); + expect(dispatchSpy.calledOnce).toStrictEqual(true); + expect(cancelTx).toHaveBeenCalledTimes(1); + expect(cancelTx).toHaveBeenCalledWith(draftTansaction); + }); + }); + describe('sign()', () => { it('should dispatch a signTransaction action', () => { mapDispatchToPropsObject.sign(); diff --git a/ui/pages/send/send-footer/send-footer.stories.js b/ui/pages/send/send-footer/send-footer.stories.js new file mode 100644 index 000000000..26d2ecfbb --- /dev/null +++ b/ui/pages/send/send-footer/send-footer.stories.js @@ -0,0 +1,28 @@ +import React from 'react'; +import { action } from '@storybook/addon-actions'; +import { boolean } from '@storybook/addon-knobs'; + +import SendFooter from './send-footer.component'; + +export default { + title: 'SendFooter', + id: __filename, +}; + +export const SendFooterComponent = () => { + const disabled = boolean('Disabled', false); + return ( + action('Cancel Button Pressed')()} + sign={() => action('Next Button Pressed')()} + // The other props below are only to make the component show no error + from={{ address: '' }} + history={{ push: () => undefined }} + addToAddressBookIfNew={() => undefined} + disabled={disabled} + mostRecentOverviewPage="" + resetSendState={() => undefined} + sendErrors={{}} + /> + ); +}; diff --git a/ui/pages/send/send-header/send-header.component.js b/ui/pages/send/send-header/send-header.component.js index e77c2c012..5e8b7f106 100644 --- a/ui/pages/send/send-header/send-header.component.js +++ b/ui/pages/send/send-header/send-header.component.js @@ -28,7 +28,7 @@ export default function SendHeader() { let title = asset.type === ASSET_TYPES.NATIVE ? t('send') : t('sendTokens'); if (stage === SEND_STAGES.ADD_RECIPIENT || stage === SEND_STAGES.INACTIVE) { - title = t('addRecipient'); + title = t('sendTo'); } else if (stage === SEND_STAGES.EDIT) { title = t('edit'); } @@ -38,7 +38,9 @@ export default function SendHeader() { className="send__header" onClose={onClose} title={title} - headerCloseText={t('cancel')} + headerCloseText={ + stage === SEND_STAGES.EDIT ? t('cancelEdit') : t('cancel') + } /> ); } diff --git a/ui/pages/send/send-header/send-header.component.test.js b/ui/pages/send/send-header/send-header.component.test.js index 02598756d..c718018cb 100644 --- a/ui/pages/send/send-header/send-header.component.test.js +++ b/ui/pages/send/send-header/send-header.component.test.js @@ -21,7 +21,7 @@ jest.mock('react-router-dom', () => { describe('SendHeader Component', () => { describe('Title', () => { - it('should render "Add Recipient" for INACTIVE or ADD_RECIPIENT stages', () => { + it('should render "Send to" for INACTIVE or ADD_RECIPIENT stages', () => { const { getByText, rerender } = renderWithProvider( , configureMockStore(middleware)({ @@ -30,7 +30,7 @@ describe('SendHeader Component', () => { history: { mostRecentOverviewPage: 'activity' }, }), ); - expect(getByText('Add Recipient')).toBeTruthy(); + expect(getByText('Send to')).toBeTruthy(); rerender( , configureMockStore(middleware)({ @@ -39,7 +39,7 @@ describe('SendHeader Component', () => { history: { mostRecentOverviewPage: 'activity' }, }), ); - expect(getByText('Add Recipient')).toBeTruthy(); + expect(getByText('Send to')).toBeTruthy(); }); it('should render "Send" for DRAFT stage when asset type is NATIVE', () => { @@ -103,6 +103,18 @@ describe('SendHeader Component', () => { expect(getByText('Cancel')).toBeTruthy(); }); + it('has button label changed to Cancel Edit in editing stage', () => { + const { getByText } = renderWithProvider( + , + configureMockStore(middleware)({ + send: { ...initialState, stage: SEND_STAGES.EDIT }, + gas: { basicEstimateStatus: 'LOADING' }, + history: { mostRecentOverviewPage: 'activity' }, + }), + ); + expect(getByText('Cancel Edit')).toBeTruthy(); + }); + it('resets send state when clicked', () => { const store = configureMockStore(middleware)({ send: initialState, diff --git a/ui/pages/send/send.scss b/ui/pages/send/send.scss index e416b41b2..c47b21fe5 100644 --- a/ui/pages/send/send.scss +++ b/ui/pages/send/send.scss @@ -22,6 +22,7 @@ right: 1rem; width: min-content; font-size: 0.75rem; + white-space: nowrap; } } @@ -136,6 +137,10 @@ &__title { @include H6; + max-width: 20em; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; color: $black; } diff --git a/ui/pages/send/send.test.js b/ui/pages/send/send.test.js index 5110a37e3..af638d1ea 100644 --- a/ui/pages/send/send.test.js +++ b/ui/pages/send/send.test.js @@ -124,11 +124,11 @@ describe('Send Page', () => { }); }); - describe('Add Recipient Flow', () => { - it('should render the header with Add Recipient displayed', () => { + describe('Send Flow', () => { + it('should render the header with Send to displayed', () => { const store = configureMockStore(middleware)(baseStore); const { getByText } = renderWithProvider(, store); - expect(getByText('Add Recipient')).toBeTruthy(); + expect(getByText('Send to')).toBeTruthy(); }); it('should render the EnsInput field', () => { @@ -146,7 +146,7 @@ describe('Send Page', () => { }); }); - describe('Send and Edit Flow', () => { + describe('Send and Edit Flow (draft)', () => { it('should render the header with Send displayed', () => { const store = configureMockStore(middleware)({ ...baseStore, diff --git a/ui/pages/settings/advanced-tab/advanced-tab.stories.js b/ui/pages/settings/advanced-tab/advanced-tab.stories.js new file mode 100644 index 000000000..3ba345ead --- /dev/null +++ b/ui/pages/settings/advanced-tab/advanced-tab.stories.js @@ -0,0 +1,45 @@ +import React from 'react'; +import { text, boolean } from '@storybook/addon-knobs'; +import AdvancedTab from './advanced-tab.component'; + +export default { + title: 'AdvancedTab', + id: __filename, +}; + +export const AdvancedTabComponent = () => { + return ( +
    + undefined} + setShowFiatConversionOnTestnetsPreference={() => undefined} + setThreeBoxSyncingPermission={() => undefined} + setIpfsGateway={() => undefined} + setLedgerLivePreference={() => undefined} + setDismissSeedBackUpReminder={() => undefined} + setUseNonceField={() => undefined} + setHexDataFeatureFlag={() => undefined} + displayWarning={() => undefined} + history={{ push: () => undefined }} + showResetAccountConfirmationModal={() => undefined} + setAdvancedInlineGasFeatureFlag={() => undefined} + warning={text('Warning', 'Warning Sample')} + ipfsGateway="ipfs-gateway" + useNonceField={boolean('Customize Transaction Nonce', false)} + sendHexData={boolean('Show Hex Data', false)} + advancedInlineGas={boolean('Advanced Inline Gas', false)} + showFiatInTestnets={boolean('Show Conversion on Testnets', false)} + threeBoxSyncingAllowed={boolean( + 'Sync data with 3Box (experimental)', + false, + )} + threeBoxDisabled={boolean('3Box Disabled', false)} + useLedgerLive={boolean('Use Ledger Live', false)} + dismissSeedBackUpReminder={boolean( + 'Dismiss recovery phrase backup reminder', + false, + )} + /> +
    + ); +}; 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 a0651b817..2f4b135f0 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 @@ -1,5 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import classnames from 'classnames'; import ContactList from '../../../components/app/contact-list'; import { CONTACT_ADD_ROUTE, @@ -68,12 +69,16 @@ export default class ContactListTab extends Component { } renderAddButton() { - const { history } = this.props; + const { history, viewingContact, editingContact } = this.props; return (