Merge remote-tracking branch 'origin/develop' into master-sync

feature/default_network_editable
ryanml 3 years ago
commit 741f623338
  1. 4
      .circleci/scripts/chrome-install.sh
  2. 2
      .github/workflows/crowdin_action.yml
  3. 10
      .storybook/metametrics.js
  4. 121
      .storybook/test-data.js
  5. 3
      README.md
  6. 10
      app/_locales/de/messages.json
  7. 10
      app/_locales/el/messages.json
  8. 85
      app/_locales/en/messages.json
  9. 10
      app/_locales/es_419/messages.json
  10. 10
      app/_locales/fr/messages.json
  11. 10
      app/_locales/hi/messages.json
  12. 10
      app/_locales/id/messages.json
  13. 10
      app/_locales/ja/messages.json
  14. 10
      app/_locales/ko/messages.json
  15. 10
      app/_locales/pt_BR/messages.json
  16. 10
      app/_locales/ru/messages.json
  17. 10
      app/_locales/tl/messages.json
  18. 10
      app/_locales/tr/messages.json
  19. 10
      app/_locales/vi/messages.json
  20. 10
      app/_locales/zh_CN/messages.json
  21. BIN
      app/images/darkmode-banner.png
  22. 3
      app/images/lock-icon.svg
  23. 68
      app/images/token-detection.svg
  24. 3
      app/images/unlock-icon.svg
  25. 75
      app/scripts/controllers/detect-tokens.js
  26. 181
      app/scripts/controllers/metametrics.js
  27. 190
      app/scripts/controllers/metametrics.test.js
  28. 2
      app/scripts/controllers/preferences.js
  29. 56
      app/scripts/controllers/transactions/index.js
  30. 22
      app/scripts/controllers/transactions/index.test.js
  31. 86
      app/scripts/lib/createRPCMethodTrackingMiddleware.js
  32. 12
      app/scripts/lib/segment.js
  33. 108
      app/scripts/metamask-controller.js
  34. 1
      crowdin.yml
  35. 51
      development/README.md
  36. 4
      development/metamaskbot-build-announce.js
  37. 5
      docs/README.md
  38. 14
      package.json
  39. 44
      shared/constants/metametrics.js
  40. 10
      shared/constants/network.js
  41. 25
      shared/constants/swaps.js
  42. 26
      shared/modules/network.utils.js
  43. 50
      shared/notifications/index.js
  44. 73
      test/data/fetch-mocks.json
  45. 3
      test/e2e/fixtures/address-entry/state.json
  46. 3
      test/e2e/fixtures/connected-state/state.json
  47. 3
      test/e2e/fixtures/custom-rpc/state.json
  48. 7
      test/e2e/fixtures/custom-token/state.json
  49. 3
      test/e2e/fixtures/eip-1559-v2-dapp/state.json
  50. 3
      test/e2e/fixtures/eip-1559-v2/state.json
  51. 3
      test/e2e/fixtures/import-ui/state.json
  52. 21
      test/e2e/fixtures/import-utc-json/test-json-import-account-file.json
  53. 358
      test/e2e/fixtures/imported-account/state.json
  54. 3
      test/e2e/fixtures/localization/state.json
  55. 3
      test/e2e/fixtures/metrics-enabled/state.json
  56. 358
      test/e2e/fixtures/navigate-transactions/state.json
  57. 7
      test/e2e/fixtures/onboarding/state.json
  58. 3
      test/e2e/fixtures/send-edit-v2/state.json
  59. 3
      test/e2e/fixtures/send-edit/state.json
  60. 3
      test/e2e/fixtures/threebox-enabled/state.json
  61. 111
      test/e2e/mock-e2e.js
  62. 114
      test/e2e/tests/chain-interactions.spec.js
  63. 73
      test/e2e/tests/from-import-ui.spec.js
  64. 3
      test/e2e/tests/phishing-detection.spec.js
  65. 273
      test/e2e/tests/settings-search.spec.js
  66. 94
      test/e2e/tests/swap-eth.spec.js
  67. 49
      test/e2e/webdriver/index.js
  68. 5
      ui/components/app/app-components.scss
  69. 24
      ui/components/app/app-header/app-header.stories.js
  70. 2
      ui/components/app/asset-list-item/asset-list-item.js
  71. 2
      ui/components/app/asset-list/asset-list.js
  72. 36
      ui/components/app/asset-list/detetcted-tokens-link/detected-tokens-link.js
  73. 5
      ui/components/app/asset-list/detetcted-tokens-link/index.scss
  74. 22
      ui/components/app/collectibles-detection-notice/collectibles-detection-notice.js
  75. 22
      ui/components/app/collectibles-detection-notice/index.scss
  76. 8
      ui/components/app/collectibles-tab/collectibles-tab.js
  77. 4
      ui/components/app/create-new-vault/create-new-vault.js
  78. 50
      ui/components/app/detected-token/detected-token-aggregators/detected-token-aggregators.js
  79. 43
      ui/components/app/detected-token/detected-token-aggregators/detected-token-aggregators.stories.js
  80. 43
      ui/components/app/detected-token/detected-token-aggregators/detected-token-aggregators.test.js
  81. 13
      ui/components/app/detected-token/detected-token-aggregators/index.scss
  82. 41
      ui/components/app/detected-token/detected-token-details/detected-token-details.js
  83. 26
      ui/components/app/detected-token/detected-token-details/detected-token-details.stories.js
  84. 35
      ui/components/app/detected-token/detected-token-details/detected-token-details.test.js
  85. 9
      ui/components/app/detected-token/detected-token-details/index.scss
  86. 57
      ui/components/app/detected-token/detected-token-values/detected-token-values.js
  87. 43
      ui/components/app/detected-token/detected-token-values/detected-token-values.stories.js
  88. 37
      ui/components/app/detected-token/detected-token-values/detected-token-values.test.js
  89. 5
      ui/components/app/detected-token/detected-token-values/index.scss
  90. 2
      ui/components/app/edit-gas-display/edit-gas-display.component.js
  91. 5
      ui/components/app/edit-gas-fee-popover/edit-gas-tooltip/index.scss
  92. 192
      ui/components/app/hold-to-reveal-button/hold-to-reveal-button.js
  93. 22
      ui/components/app/hold-to-reveal-button/hold-to-reveal-button.stories.js
  94. 72
      ui/components/app/hold-to-reveal-button/hold-to-reveal-button.test.js
  95. 1
      ui/components/app/hold-to-reveal-button/index.js
  96. 164
      ui/components/app/hold-to-reveal-button/index.scss
  97. 2
      ui/components/app/import-token-link/import-token-link.component.js
  98. 2
      ui/components/app/menu-bar/account-options-menu.js
  99. 2
      ui/components/app/menu-bar/menu-bar.js
  100. 16
      ui/components/app/modals/confirm-remove-account/confirm-remove-account.stories.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -5,12 +5,12 @@ set -u
set -o pipefail
# To get the latest version, see <https://www.ubuntuupdates.org/ppa/google_chrome?dist=stable>
CHROME_VERSION='99.0.4844.51-1'
CHROME_VERSION='100.0.4896.60-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}"
# To retrieve this checksum, run the `wget` and `shasum` commands below
CHROME_BINARY_SHA512SUM='f0fa5c6c23d9e8373aafa14622ed4362cbf3a101691ce309864e4aa0030dc3bf7b8e7c8ce294c06106b26eb6b8dc0f2b80376bf2a49d703fc9f6597961b9432e'
CHROME_BINARY_SHA512SUM='d7a98777650e8218fef4acc8466d4ddf5e234b97fbc16c33f38f69f9ebfe7b6bb6827a90aad15ea729d7c1bacfa78488c5d5194b531f00f454302dd9c0957e4e'
wget -O "${CHROME_BINARY}" -t 5 "${CHROME_BINARY_URL}"

@ -21,7 +21,7 @@ jobs:
uses: actions/checkout@v2
- name: crowdin action
uses: crowdin/github-action@d0622816ed4f4744db27d04374b2cef6867f7bed
uses: crowdin/github-action@9237b4cb361788dfce63feb2e2f15c09e2fe7415
with:
upload_translations: true
download_translations: true

@ -1,22 +1,14 @@
import React from 'react';
import {
MetaMetricsProvider,
LegacyMetaMetricsProvider,
LegacyMetaMetricsProvider
} from '../ui/contexts/metametrics';
import {
MetaMetricsProvider as NewMetaMetricsProvider,
LegacyMetaMetricsProvider as NewLegacyMetaMetricsProvider,
} from '../ui/contexts/metametrics.new';
const MetaMetricsProviderStorybook = (props) =>
(
<MetaMetricsProvider>
<LegacyMetaMetricsProvider>
<NewMetaMetricsProvider>
<NewLegacyMetaMetricsProvider>
{props.children}
</NewLegacyMetaMetricsProvider>
</NewMetaMetricsProvider>
</LegacyMetaMetricsProvider>
</MetaMetricsProvider>
);

@ -13,8 +13,128 @@ const state = {
protocol: 'https:',
url: 'https://metamask.github.io/test-dapp/',
},
networkList: [
{
blockExplorerUrl: "https://etherscan.io",
chainId: "0x1",
iconColor: 'var(--mainnet)',
isATestNetwork: false,
labelKey: "mainnet",
providerType: "mainnet",
rpcUrl: "https://mainnet.infura.io/v3/",
ticker: "ETH",
viewOnly: true,
},
{
blockExplorerUrl: "https://ropsten.etherscan.io",
chainId: "0x3",
iconColor: 'var(--ropsten)',
isATestNetwork: true,
labelKey: "ropsten",
providerType: "ropsten",
rpcUrl: "https://ropsten.infura.io/v3/",
ticker: "ETH",
viewOnly: true,
},
{
blockExplorerUrl: "https://rinkeby.etherscan.io",
chainId: "0x4",
iconColor: 'var(--rinkeby)',
isATestNetwork: true,
labelKey: "rinkeby",
providerType: "rinkeby",
rpcUrl: "https://rinkeby.infura.io/v3/",
ticker: "ETH",
viewOnly: true,
},
{
blockExplorerUrl: "https://goerli.etherscan.io",
chainId: "0x5",
iconColor: 'var(--goerli)',
isATestNetwork: true,
labelKey: "goerli",
providerType: "goerli",
rpcUrl: "https://goerli.infura.io/v3/",
ticker: "ETH",
viewOnly: true,
},
{
blockExplorerUrl: "https://kovan.etherscan.io",
chainId: "0x2a",
iconColor: 'var(--kovan)',
isATestNetwork: true,
labelKey: "kovan",
providerType: "kovan",
rpcUrl: "https://kovan.infura.io/v3/",
ticker: "ETH",
viewOnly: true,
},
{
blockExplorerUrl: "",
chainId: "0x539",
iconColor: 'var(--localhost)',
isATestNetwork: true,
label: "Localhost 8545",
providerType: "rpc",
rpcUrl: "http://localhost:8545",
ticker: "ETH",
},
{
blockExplorerUrl: "https://bscscan.com",
chainId: "0x38",
iconColor: 'var(--localhost)',
isATestNetwork: false,
label: "Binance Smart Chain",
providerType: "rpc",
rpcUrl: "https://bsc-dataseed.binance.org/",
ticker: "BNB",
},
{
blockExplorerUrl: "https://cchain.explorer.avax.network/",
chainId: "0xa86a",
iconColor: 'var(--localhost)',
isATestNetwork: false,
label: "Avalanche",
providerType: "rpc",
rpcUrl: "https://api.avax.network/ext/bc/C/rpc",
ticker: "AVAX",
},
{
blockExplorerUrl: "https://polygonscan.com",
chainId: "0x89",
iconColor: 'var(--localhost)',
isATestNetwork: false,
label: "Polygon",
providerType: "rpc",
rpcUrl: "https://polygon-rpc.com",
ticker: "MATIC",
},
],
metamask: {
tokenList: {
'0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f': {
address: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f',
symbol: 'SNX',
decimals: 18,
name: 'Synthetix Network Token',
iconUrl: 'https://assets.coingecko.com/coins/images/3406/large/SNX.png',
aggregators: [
'Aave',
'Bancor',
'CMC',
'Crypto.com',
'CoinGecko',
'1inch',
'Paraswap',
'PMM',
'Synthetix',
'Zapper',
'Zerion',
'0x',
],
occurrences: 12,
unlisted: false
},
'0x6b175474e89094c44da98b954eedeac495271d0f': {
address: '0x6b175474e89094c44da98b954eedeac495271d0f',
symbol: 'META',
@ -989,6 +1109,7 @@ const state = {
suggestedAssets: [],
useNonceField: false,
usePhishDetect: true,
useTokenDetection: true,
lostIdentities: {},
forgottenPassword: false,
ipfsGateway: 'dweb.link',

@ -20,7 +20,7 @@ To learn how to contribute to the MetaMask project itself, visit our [Internal D
- Install dependencies: `yarn setup` (not the usual install command)
- Copy the `.metamaskrc.dist` file to `.metamaskrc`
- Replace the `INFURA_PROJECT_ID` value with your own personal [Infura Project ID](https://infura.io/docs).
- If debugging MetaMetrics, you'll need to add a value for `SEGMENT_WRITE_KEY` [Segment write key](https://segment.com/docs/connections/find-writekey/).
- If debugging MetaMetrics, you'll need to add a value for `SEGMENT_WRITE_KEY` [Segment write key](https://segment.com/docs/connections/find-writekey/), see [Developing on MetaMask](./development/README.md).
- Build the project to the `./dist/` folder with `yarn dist`.
Uncompressed builds can be found in `/dist`, compressed builds can be found in `/builds` once they're built.
@ -97,6 +97,7 @@ Whenever you change dependencies (adding, removing, or updating, either in `pack
- [How to add a new translation to MetaMask](./docs/translating-guide.md)
- [Publishing Guide](./docs/publishing.md)
- [How to use the TREZOR emulator](./docs/trezor-emulator.md)
- [Developing on MetaMask](./development/README.md)
- [How to generate a visualization of this repository's development](./development/gource-viz.sh)
[1]: http://www.nomnoml.com/#view/%5B%3Cactor%3Euser%5D%0A%0A%5Bmetamask-ui%7C%0A%20%20%20%5Btools%7C%0A%20%20%20%20%20react%0A%20%20%20%20%20redux%0A%20%20%20%20%20thunk%0A%20%20%20%20%20ethUtils%0A%20%20%20%20%20jazzicon%0A%20%20%20%5D%0A%20%20%20%5Bcomponents%7C%0A%20%20%20%20%20app%0A%20%20%20%20%20account-detail%0A%20%20%20%20%20accounts%0A%20%20%20%20%20locked-screen%0A%20%20%20%20%20restore-vault%0A%20%20%20%20%20identicon%0A%20%20%20%20%20config%0A%20%20%20%20%20info%0A%20%20%20%5D%0A%20%20%20%5Breducers%7C%0A%20%20%20%20%20app%0A%20%20%20%20%20metamask%0A%20%20%20%20%20identities%0A%20%20%20%5D%0A%20%20%20%5Bactions%7C%0A%20%20%20%20%20%5BbackgroundConnection%5D%0A%20%20%20%5D%0A%20%20%20%5Bcomponents%5D%3A-%3E%5Bactions%5D%0A%20%20%20%5Bactions%5D%3A-%3E%5Breducers%5D%0A%20%20%20%5Breducers%5D%3A-%3E%5Bcomponents%5D%0A%5D%0A%0A%5Bweb%20dapp%7C%0A%20%20%5Bui%20code%5D%0A%20%20%5Bweb3%5D%0A%20%20%5Bmetamask-inpage%5D%0A%20%20%0A%20%20%5B%3Cactor%3Eui%20developer%5D%0A%20%20%5Bui%20developer%5D-%3E%5Bui%20code%5D%0A%20%20%5Bui%20code%5D%3C-%3E%5Bweb3%5D%0A%20%20%5Bweb3%5D%3C-%3E%5Bmetamask-inpage%5D%0A%5D%0A%0A%5Bmetamask-background%7C%0A%20%20%5Bprovider-engine%5D%0A%20%20%5Bhooked%20wallet%20subprovider%5D%0A%20%20%5Bid%20store%5D%0A%20%20%0A%20%20%5Bprovider-engine%5D%3C-%3E%5Bhooked%20wallet%20subprovider%5D%0A%20%20%5Bhooked%20wallet%20subprovider%5D%3C-%3E%5Bid%20store%5D%0A%20%20%5Bconfig%20manager%7C%0A%20%20%20%20%5Brpc%20configuration%5D%0A%20%20%20%20%5Bencrypted%20keys%5D%0A%20%20%20%20%5Bwallet%20nicknames%5D%0A%20%20%5D%0A%20%20%0A%20%20%5Bprovider-engine%5D%3C-%5Bconfig%20manager%5D%0A%20%20%5Bid%20store%5D%3C-%3E%5Bconfig%20manager%5D%0A%5D%0A%0A%5Buser%5D%3C-%3E%5Bmetamask-ui%5D%0A%0A%5Buser%5D%3C%3A--%3A%3E%5Bweb%20dapp%5D%0A%0A%5Bmetamask-contentscript%7C%0A%20%20%5Bplugin%20restart%20detector%5D%0A%20%20%5Brpc%20passthrough%5D%0A%5D%0A%0A%5Brpc%20%7C%0A%20%20%5Bethereum%20blockchain%20%7C%0A%20%20%20%20%5Bcontracts%5D%0A%20%20%20%20%5Baccounts%5D%0A%20%20%5D%0A%5D%0A%0A%5Bweb%20dapp%5D%3C%3A--%3A%3E%5Bmetamask-contentscript%5D%0A%5Bmetamask-contentscript%5D%3C-%3E%5Bmetamask-background%5D%0A%5Bmetamask-background%5D%3C-%3E%5Bmetamask-ui%5D%0A%5Bmetamask-background%5D%3C-%3E%5Brpc%5D%0A

@ -1276,13 +1276,6 @@
"importAccountSeedPhrase": {
"message": "Ein Konto mit einem Seed-Schlüssel importieren"
},
"importExistingWalletDescription": {
"message": "Geben Sie die Geheime Wiederherstellungsphrase (alias Seed Phrase) ein, die Sie beim Erstellen Ihrer Wallet erhalten haben. $1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
},
"importExistingWalletTitle": {
"message": "Eine bestehende Wallet mit einer Geheime Wiederherstellungsphrase importieren"
},
"importMyWallet": {
"message": "Meine Wallet importieren"
},
@ -1738,9 +1731,6 @@
"newNFTsDetected": {
"message": "Neu! NFT-Erkennung"
},
"newNFTsDetectedInfo": {
"message": "Erlaube der MetaMaske, NFTs automatisch von Opensea zu erkennen und in deiner MetaMask Wallet anzuzeigen."
},
"newNetworkAdded": {
"message": "“$1” wurde erfolgreich hinzugefügt!"
},

@ -1270,13 +1270,6 @@
"importAccountSeedPhrase": {
"message": "Εισαγωγή λογαριασμού με Μυστική Φράση Ανάκτησης"
},
"importExistingWalletDescription": {
"message": "Εισάγετε τη Μυστική Φράση Ανάκτησης (δλδ Seed Phrase) που σας δόθηκε όταν δημιουργήσατε το πορτοφόλι σας. $1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
},
"importExistingWalletTitle": {
"message": "Εισαγωγή υπάρχοντος πορτοφολιού με Μυστική Φράση Ανάκτησης"
},
"importMyWallet": {
"message": "Εισαγωγή Πορτοφολιού μου"
},
@ -1738,9 +1731,6 @@
"newNFTsDetected": {
"message": "Νέο! Εντοπισμός NFT"
},
"newNFTsDetectedInfo": {
"message": "Επιτρέψτε στο MetaMask να ανιχνεύει αυτόματα NFTs από το Opensea και να εμφανίζεται στο πορτοφόλι σας MetaMask."
},
"newNetworkAdded": {
"message": "Το “$1” προστέθηκε με επιτυχία!"
},

@ -62,6 +62,13 @@
"message": "$1 may access and spend this asset",
"description": "$1 is the url of the site requesting ability to spend"
},
"accessYourWalletWithSRP": {
"message": "Access your wallet with your Secret Recovery Phrase"
},
"accessYourWalletWithSRPDescription": {
"message": "MetaMask cannot recover your password. We will use your Secret Recovery Phrase to validate your ownership, restore your wallet and set up a new password. First, enter the Secret Recovery Phrase that you were given when you created your wallet. $1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
},
"accessingYourCamera": {
"message": "Accessing your camera..."
},
@ -714,6 +721,9 @@
"custom": {
"message": "Advanced"
},
"customContentSearch": {
"message": "Search for a previously added network"
},
"customGas": {
"message": "Customize Gas"
},
@ -733,6 +743,12 @@
"customToken": {
"message": "Custom Token"
},
"customTokenWarningInNonTokenDetectionNetwork": {
"message": "Token detection is not available on this network yet. Please import token manually and make sure you trust it. Learn about $1"
},
"customTokenWarningInTokenDetectionNetwork": {
"message": "Before manually importing a token, make sure you trust it. Learn about $1."
},
"customerSupport": {
"message": "customer support"
},
@ -1258,6 +1274,9 @@
"message": "From: $1",
"description": "$1 is the address to include in the From label. It is typically shortened first using shortenAddress"
},
"fromTokenLists": {
"message": "From token lists: $1"
},
"functionApprove": {
"message": "Function: Approve"
},
@ -1468,13 +1487,6 @@
"importAccountSeedPhrase": {
"message": "Import a wallet with Secret Recovery Phrase"
},
"importExistingWalletDescription": {
"message": "Enter your Secret Recovery Phrase (aka Seed Phrase) that you were given when you created your wallet. $1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
},
"importExistingWalletTitle": {
"message": "Import existing wallet with Secret Recovery Phrase"
},
"importMyWallet": {
"message": "Import My Wallet"
},
@ -2000,12 +2012,12 @@
"newContract": {
"message": "New Contract"
},
"newNFTDetectedMessage": {
"message": "Allow MetaMask to automatically detect NFTs from Opensea and display in your wallet."
},
"newNFTsDetected": {
"message": "New! NFT detection"
},
"newNFTsDetectedInfo": {
"message": "Allow MetaMask to automatically detect NFTs from Opensea and display in your MetaMask wallet."
},
"newNetworkAdded": {
"message": "“$1” was successfully added!"
},
@ -2097,6 +2109,37 @@
"notEnoughGas": {
"message": "Not Enough Gas"
},
"notifications10ActionText": {
"message": "Visit in settings",
"description": "The 'call to action' on the button, or link, of the 'Visit in settings' notification. Upon clicking, users will be taken to settings page."
},
"notifications10DescriptionOne": {
"message": "Improved token detection is currently available on Ethereum Mainnet, Polygon, BSC, and Avalanche networks. More to come!"
},
"notifications10DescriptionThree": {
"message": "Token detection feature is ON by default. But you can disable it from Settings."
},
"notifications10DescriptionTwo": {
"message": "We source tokens from third party tokens lists. Tokens listed on more than two token lists will be automatically detected."
},
"notifications10Title": {
"message": "Improved token detection is here"
},
"notifications11Description": {
"message": "Tokens can be created by anyone and can have duplicate names. If you see a token appear that you don’t trust or haven’t interacted with - it’s safer to not trust it."
},
"notifications11Title": {
"message": "Scam and security risks"
},
"notifications12ActionText": {
"message": "Enable dark mode"
},
"notifications12Description": {
"message": "Dark Mode will be enabled for new users depending on their system preferences. For existing users, enable Dark Mode manually under Settings -> Experimental."
},
"notifications12Title": {
"message": "Wen dark mode? Now dark mode! 🕶🦊"
},
"notifications1Description": {
"message": "MetaMask Mobile users can now swap tokens inside their mobile wallet. Scan the QR code to get the mobile app and start swapping.",
"description": "Description of a notification in the 'See What's New' popup. Describes the swapping on mobile feature."
@ -2186,6 +2229,10 @@
"notifications9Title": {
"message": "👓 We are making transactions easier to read."
},
"numberOfNewTokensDetected": {
"message": "$1 new tokens found in this account",
"description": "$1 is the number of new tokens detected"
},
"ofTextNofM": {
"message": "of"
},
@ -2283,6 +2330,9 @@
"origin": {
"message": "Origin"
},
"padlock": {
"message": "Padlock"
},
"parameters": {
"message": "Parameters"
},
@ -2767,7 +2817,7 @@
"message": "Settings"
},
"settingsSearchMatchingNotFound": {
"message": "No matching results found"
"message": "No matching results found."
},
"shorthandVersion": {
"message": "v$1",
@ -2858,6 +2908,10 @@
"message": "$1 snap has access to:",
"description": "$1 represents the name of the snap"
},
"snapAdded": {
"message": "Added on $1 from $2",
"description": "$1 represents the date the snap was installed, $2 represents which origin installed the snap."
},
"snapError": {
"message": "Snap Error: '$1'. Error Code: '$2'",
"description": "This is shown when a snap encounters an error. $1 is the error message from the snap, and $2 is the error code."
@ -3505,6 +3559,9 @@
"testFaucet": {
"message": "Test Faucet"
},
"testNetworks": {
"message": "Test networks"
},
"theme": {
"message": "Theme"
},
@ -3555,6 +3612,9 @@
"tokenDetection": {
"message": "Token detection"
},
"tokenDetectionAlertMessage": {
"message": "Token detection is currently available on $1. $2"
},
"tokenDetectionAnnouncement": {
"message": "New! Improved token detection is available on Ethereum Mainnet as an experimental feature. $1"
},
@ -3713,6 +3773,9 @@
"typePassword": {
"message": "Type your MetaMask password"
},
"typeYourSRP": {
"message": "Type your Secret Recovery Phrase"
},
"u2f": {
"message": "U2F",
"description": "A name on an API for the browser to interact with devices that support the U2F protocol. On some browsers we use it to connect MetaMask to Ledger devices."

@ -1319,13 +1319,6 @@
"importAccountSeedPhrase": {
"message": "Importar una cartera con la frase secreta de recuperación"
},
"importExistingWalletDescription": {
"message": "Ingrese su frase secreta de recuperación (también conocida como Frase Semilla) que recibió al crear su cartera. $1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
},
"importExistingWalletTitle": {
"message": "Importar la cartera existente con la frase secreta de recuperación"
},
"importMyWallet": {
"message": "Importar Mi cartera"
},
@ -1787,9 +1780,6 @@
"newNFTsDetected": {
"message": "¡Nuevo! Detección NFT"
},
"newNFTsDetectedInfo": {
"message": "Permitir que MetaMask detecte automáticamente NFT de Opensea y los muestre en su cartera MetaMask."
},
"newNetworkAdded": {
"message": "¡\"$1\" se añadió con éxito!"
},

@ -1270,13 +1270,6 @@
"importAccountSeedPhrase": {
"message": "Importez un compte avec une phrase mnémotechnique"
},
"importExistingWalletDescription": {
"message": "Saisissez la phrase secrète de récupération (aussi appelée «phrase mnémonique» ou «seed») qui vous a été attribuée lors de la création de votre portefeuille. $1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
},
"importExistingWalletTitle": {
"message": "Importer un portefeuille existant avec la phrase secrète de récupération"
},
"importMyWallet": {
"message": "Importer mon portefeuille"
},
@ -1738,9 +1731,6 @@
"newNFTsDetected": {
"message": "Nouveau! Détection de NFT"
},
"newNFTsDetectedInfo": {
"message": "Cela permet à MetaMask de détecter automatiquement les NFT d’Opensea et de les afficher dans votre portefeuille MetaMask."
},
"newNetworkAdded": {
"message": $1» a été ajouté avec succès!"
},

@ -1270,13 +1270,6 @@
"importAccountSeedPhrase": {
"message": "गत रिकवरज कथ एक ख आयत कर"
},
"importExistingWalletDescription": {
"message": "अपनट रिकवरज (उरफ सड फज) दरज कर आपक अपनट बन पर दि गय। $1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
},
"importExistingWalletTitle": {
"message": "सट रिकवरज कथ मट आयत कर"
},
"importMyWallet": {
"message": "मट आयत कर"
},
@ -1738,9 +1731,6 @@
"newNFTsDetected": {
"message": "नय! NFT डिशन"
},
"newNFTsDetectedInfo": {
"message": "MetaMask क Opensea स NFT कवचित रप स पत लग और अपन MetaMask वट मरदरित करन अनमति।"
},
"newNetworkAdded": {
"message": "\"$1\" सफलतवक ज गय!"
},

@ -1270,13 +1270,6 @@
"importAccountSeedPhrase": {
"message": "Impor dompet dengan Frasa Pemulihan Rahasia"
},
"importExistingWalletDescription": {
"message": "Masukkan Frasa Pemulihan Rahasia Anda (alias Frasa Benih) yang diberikan saat Anda membuat dompet. $1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
},
"importExistingWalletTitle": {
"message": "Impor dompet yang ada dengan Frasa Pemulihan Rahasia"
},
"importMyWallet": {
"message": "Impor Dompet Saya"
},
@ -1738,9 +1731,6 @@
"newNFTsDetected": {
"message": "Baru! Deteksi NFT"
},
"newNFTsDetectedInfo": {
"message": "Izinkan MetaMask untuk mendeteksi NFT dari Opensea secara otomatis dan menampilkannya di dompet MetaMask Anda."
},
"newNetworkAdded": {
"message": "“$1” berhasil ditambahkan!"
},

@ -1270,13 +1270,6 @@
"importAccountSeedPhrase": {
"message": "シークレットリカバリーフレーズを使用してウォレットをインポート"
},
"importExistingWalletDescription": {
"message": "ウォレットの作成時に提供されたシークレットリカバリーフレーズ (シードフレーズ) を入力してください。$1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
},
"importExistingWalletTitle": {
"message": "シークレットリカバリーフレーズで既存のウォレットをインポート"
},
"importMyWallet": {
"message": "ウォレットをインポート"
},
@ -1738,9 +1731,6 @@
"newNFTsDetected": {
"message": "新機能! NFT検出"
},
"newNFTsDetectedInfo": {
"message": "MetaMaskがOpenseからNFTを自動的に検出し、MetaMaskウォレットに表示できるようにします。"
},
"newNetworkAdded": {
"message": "「$1」が追加されました!"
},

@ -1270,13 +1270,6 @@
"importAccountSeedPhrase": {
"message": "비밀 복구 구문으로 계정 가져오기"
},
"importExistingWalletDescription": {
"message": "지갑을 만들 때 받은 비밀 복구 구문(시드 구문)을 입력하세요. $1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
},
"importExistingWalletTitle": {
"message": "비밀 복구 구문을 사용하여 기존 지갑 가져오기"
},
"importMyWallet": {
"message": "내 지갑 가져오기"
},
@ -1738,9 +1731,6 @@
"newNFTsDetected": {
"message": "신규! NFT 감지"
},
"newNFTsDetectedInfo": {
"message": "MetaMask가 Opensea에서 자동으로 NFT를 감지하고 MetaMask 지갑에 표시하도록 허용합니다."
},
"newNetworkAdded": {
"message": "“$1”가 성공적으로 추가되었습니다!"
},

@ -1303,13 +1303,6 @@
"importAccountSeedPhrase": {
"message": "Importe uma carteira com a Frase de Recuperação Secreta"
},
"importExistingWalletDescription": {
"message": "Digite sua Frase de Recuperação Secreta (ou seja, a frase seed) que lhe foi dada quando você criou a sua carteira. $1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
},
"importExistingWalletTitle": {
"message": "Importar carteira existente com Frase de Recuperação Secreta"
},
"importMyWallet": {
"message": "Importar minha carteira"
},
@ -1771,9 +1764,6 @@
"newNFTsDetected": {
"message": "Novidade! Detecção de NFT"
},
"newNFTsDetectedInfo": {
"message": "Autorize que a MetaMask detecte NFTs automaticamente do Opensea e os exiba na sua carteira MetaMask."
},
"newNetworkAdded": {
"message": "“$1” foi adicionado com sucesso!"
},

@ -1270,13 +1270,6 @@
"importAccountSeedPhrase": {
"message": "Импорт кошелька с помощью секретной фразы для восстановления"
},
"importExistingWalletDescription": {
"message": "Введите секретную фразу для восстановления (также известную как «сид-фраза»), которую вы получили при создании кошелька. $1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
},
"importExistingWalletTitle": {
"message": "Импортируйте существующий кошелек с помощью секретной фразы для восстановления"
},
"importMyWallet": {
"message": "Импорт моего кошелька"
},
@ -1738,9 +1731,6 @@
"newNFTsDetected": {
"message": "Новинка! Обнаружение NFT"
},
"newNFTsDetectedInfo": {
"message": "Разрешите MetaMask автоматически обнаруживать NFT из Opensea и отображать их в вашем кошельке MetaMask."
},
"newNetworkAdded": {
"message": "«$1» успешно добавлен!"
},

@ -1270,13 +1270,6 @@
"importAccountSeedPhrase": {
"message": "Mag-import ng account gamit ang Secret Recovery Phrase"
},
"importExistingWalletDescription": {
"message": "Ilagay ang iyong Secret Recovery Phrase (kilala rin bilang Seed Phrase) na ibinigay sa iyo noong gumawa ka ng iyong wallet. $1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
},
"importExistingWalletTitle": {
"message": "Mag-import ng umiiral na wallet gamit ang Secret Recovery Phrase"
},
"importMyWallet": {
"message": "I-import ang Wallet Ko"
},
@ -1738,9 +1731,6 @@
"newNFTsDetected": {
"message": "Bago! Pag-detect ng NFT"
},
"newNFTsDetectedInfo": {
"message": "Payagan ang MetaMask na awtomatikong i-detect ang mga NFT mula sa Opensea at ipakita sa iyong MetaMask wallet."
},
"newNetworkAdded": {
"message": "Ang “$1” matagumpay na naidagdag!"
},

@ -1270,13 +1270,6 @@
"importAccountSeedPhrase": {
"message": "Gizli Kurtarma İfadesi ile bir cüzdanı içe aktarın"
},
"importExistingWalletDescription": {
"message": "Cüzdanınızı oluşturduğunuzda size verilen Gizli Kurtarma İfadenizi (başka bir deyişle Tohum İfadesi) girin. $1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
},
"importExistingWalletTitle": {
"message": "Gizli Kurtarma İfadesi ile mevcut cüzdanı içe aktarın"
},
"importMyWallet": {
"message": "Cüzdanımı İçe Aktar"
},
@ -1738,9 +1731,6 @@
"newNFTsDetected": {
"message": "Yeni! NFT algılama"
},
"newNFTsDetectedInfo": {
"message": "MetaMask'in otomatik olarak Opensea'den NFT'leri algılamasına ve MetaMask cüzdanınızda görüntülemesine izin verin."
},
"newNetworkAdded": {
"message": "\"$1\" başarılı bir şekilde eklendi!"
},

@ -1270,13 +1270,6 @@
"importAccountSeedPhrase": {
"message": "Nhập một ví bằng Cụm mật khẩu khôi phục bí mật"
},
"importExistingWalletDescription": {
"message": "Nhập Cụm Mật Khẩu Khôi Phục Bí Mật (còn được gọi là Cụm Mật Khẩu Gốc) mà bạn được cấp khi tạo ví. $1",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
},
"importExistingWalletTitle": {
"message": "Nhập ví hiện tại bằng Cụm Mật Khẩu Khôi Phục Bí Mật"
},
"importMyWallet": {
"message": "Nhập Ví Của Tôi"
},
@ -1738,9 +1731,6 @@
"newNFTsDetected": {
"message": "Mới! Phát hiện NFT"
},
"newNFTsDetectedInfo": {
"message": "Cho phép MetaMask tự động phát hiện NFT từ Opensea và hiển thị trong ví MetaMask của bạn."
},
"newNetworkAdded": {
"message": "“$1” đã được thêm thành công!"
},

@ -1270,13 +1270,6 @@
"importAccountSeedPhrase": {
"message": "使用账户助记词导入账户"
},
"importExistingWalletDescription": {
"message": "输入您创建$1钱包时提供的保密恢复短语(或Seed Phrase)。",
"description": "$1 is the words 'Learn More' from key 'learnMore', separated here so that it can be added as a link"
},
"importExistingWalletTitle": {
"message": "使用账户助记词导入现有钱包"
},
"importMyWallet": {
"message": "导入我的钱包"
},
@ -1738,9 +1731,6 @@
"newNFTsDetected": {
"message": "新功能!NFT 检测"
},
"newNFTsDetectedInfo": {
"message": "允许 MetaMask自动检测Opensea 的 NFT,并在您的 MetaMask钱包中显示。"
},
"newNetworkAdded": {
"message": "成功添加了“$1”!"
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

@ -0,0 +1,3 @@
<svg width="8" height="9" viewBox="0 0 8 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.03125 3.9375H6.60938V2.67188C6.60938 1.19883 5.41055 0 3.9375 0C2.46445 0 1.26562 1.19883 1.26562 2.67188V3.9375H0.84375C0.37793 3.9375 0 4.31543 0 4.78125V8.15625C0 8.62207 0.37793 9 0.84375 9H7.03125C7.49707 9 7.875 8.62207 7.875 8.15625V4.78125C7.875 4.31543 7.49707 3.9375 7.03125 3.9375ZM5.20312 3.9375H2.67188V2.67188C2.67188 1.97402 3.23965 1.40625 3.9375 1.40625C4.63535 1.40625 5.20312 1.97402 5.20312 2.67188V3.9375Z" fill="#EAF6FF"/>
</svg>

After

Width:  |  Height:  |  Size: 556 B

@ -0,0 +1,68 @@
<svg width="296" height="128" viewBox="0 0 296 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_85_2183)">
<rect width="296" height="128" rx="10" fill="#EAF6FF"/>
<path d="M190.249 -144.178C194.964 -141.475 199.004 -137.595 201.949 -133.169L393.025 163.267C399.883 173.845 400.209 186.936 394.262 197.995C388.315 209.053 376.979 215.85 364.343 216.036L-186.122 223.472C-200.738 223.696 -213.619 214.748 -218.789 201.227C-223.96 187.705 -219.904 172.434 -208.735 163.196L-15.2248 3.25028L151.243 -140.339C158.745 -146.596 168.646 -149.523 178.29 -147.905C182.544 -147.812 186.712 -146.205 190.249 -144.178Z" fill="url(#paint0_linear_85_2183)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M180.891 32.9781C198.139 42.9931 199.367 58.8541 184.547 69.7501C183.364 70.6151 182.084 71.4491 180.641 72.2841C179.248 73.0971 177.788 73.8371 176.275 74.5291C157.432 83.0761 130.029 82.3301 112.795 72.2841C94.096 61.4541 94.228 43.8071 112.99 32.9781C131.786 22.1261 162.217 22.1261 180.891 32.9781Z" fill="#A1A7A8"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M243.483 103.472C245.372 104.569 245.492 107.253 243.708 108.515L239.623 111.404L175.924 70.957L176.674 64.647L243.483 103.472Z" fill="#7BA8BA"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M101.89 44.0009L101.923 55.4389C101.904 49.5769 105.76 43.7059 113.469 39.2529C128.824 30.3869 153.779 30.3869 169.039 39.2529C176.611 43.6539 180.398 49.4249 180.412 55.2179L180.381 43.7709C180.365 37.9869 176.577 32.2069 169.008 27.8159C153.748 18.9429 128.794 18.9429 113.436 27.8159C105.725 32.2609 101.872 38.1299 101.89 44.0009Z" fill="#E54A21"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M169.008 32.4959C184.264 41.3589 184.152 55.7939 168.798 64.6649C153.438 73.5299 128.539 73.5299 113.281 64.6649C98.023 55.7939 98.109 41.3419 113.436 32.4959C128.794 23.6299 153.748 23.6299 169.008 32.4959Z" fill="#D7FF82" fill-opacity="0.3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M93.146 44.033L93.176 55.475C93.195 62.547 97.839 69.617 107.131 74.995C124.357 85.036 151.768 85.784 170.605 77.241C172.117 76.55 173.58 75.802 174.971 75C176.416 74.161 177.697 73.327 178.877 72.462C185.742 67.41 189.164 61.303 189.146 55.198L189.113 43.759C189.133 49.855 185.709 55.973 178.846 61.015C177.664 61.88 176.384 62.714 174.94 63.549C173.547 64.361 172.088 65.102 170.574 65.794C151.731 74.341 124.328 73.595 107.094 63.549C97.808 58.17 93.166 51.109 93.146 44.033Z" fill="#FF6C3F"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M168.798 59.987C184.152 51.114 184.264 36.676 169.008 27.816C153.748 18.943 128.794 18.943 113.436 27.816C98.109 36.661 98.023 51.114 113.281 59.987C128.539 68.846 153.438 68.846 168.798 59.987ZM175.19 24.243C192.438 34.258 193.666 50.119 178.846 61.015C177.664 61.88 176.384 62.714 174.94 63.549C173.547 64.361 172.088 65.102 170.574 65.794C151.731 74.341 124.328 73.595 107.094 63.549C88.396 52.719 88.527 35.071 107.289 24.243C126.086 13.391 156.516 13.391 175.19 24.243Z" fill="#FF8E66"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M176.061 64.1429C177.482 63.3229 178.762 63.2489 179.689 63.7859L212.67 82.9559C211.746 82.4139 210.461 82.4929 209.045 83.3129C206.269 84.9169 204.004 88.8319 204.01 92.0299C204.016 93.6369 204.583 94.7649 205.498 95.2929L172.514 76.1249C171.602 75.5989 171.037 74.4719 171.031 72.8599C171.021 69.6719 173.285 65.7519 176.061 64.1429Z" fill="#FF8E66"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M205.498 95.2931C204.583 94.7651 204.016 93.6371 204.01 92.0301C204.01 91.8581 204.082 91.6671 204.095 91.4951L171.113 72.3301C171.101 72.5041 171.031 72.6861 171.031 72.8601C171.037 74.4721 171.602 75.5991 172.514 76.1251L205.498 95.2931Z" fill="#E64A21"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M209.045 83.3129C206.269 84.9169 204.004 88.8319 204.01 92.0299C204.021 95.2559 206.299 96.5529 209.077 94.9479C211.887 93.3209 214.15 89.4079 214.142 86.1769C214.133 82.9849 211.853 81.6859 209.045 83.3129Z" fill="#FD6220"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M197.91 75.6931C199.623 74.7031 201.17 74.6141 202.282 75.2601L242.063 98.3791C240.946 97.7341 239.396 97.8181 237.691 98.8091C234.338 100.743 231.609 105.467 231.617 109.323C231.621 111.264 232.309 112.617 233.408 113.257L193.631 90.1471C192.53 89.5061 191.846 88.1471 191.841 86.2081C191.828 82.3511 194.56 77.6301 197.91 75.6931Z" fill="#FF8E66"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M233.408 113.257C232.309 112.617 231.621 111.264 231.617 109.323C231.613 108.692 231.793 108.022 231.926 107.359L192.15 84.2461C192.015 84.9061 191.838 85.5771 191.841 86.2081C191.846 88.1471 192.53 89.5061 193.631 90.1471L233.408 113.257Z" fill="#FF6C3F"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M237.691 98.8091C234.338 100.743 231.609 105.467 231.617 109.323C231.629 113.214 234.376 114.779 237.726 112.843C241.118 110.893 243.846 106.165 243.835 102.266C243.824 98.4141 241.078 96.8511 237.691 98.8091Z" fill="#FF6C3F"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M162.721 40C162.995 45.476 159.55 51.006 152.333 55.177C144.061 59.951 132.726 61.814 122 60.877C132.903 64.898 147.576 64.175 157.135 58.656C165.854 53.619 167.713 46.089 162.721 40Z" fill="#B9D299"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M137.69 58.259C137.011 58.093 136.273 58.087 135.57 58.191C136.279 58.232 136.98 58.251 137.69 58.259Z" fill="#FFC9B6"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M106.908 63.428C97.752 58.065 93.166 51.057 93.146 44.033L93.176 55.475C93.195 62.483 97.773 69.494 106.908 74.855V63.428Z" fill="#E64A21"/>
<path d="M38.9983 99.2535C46.7879 93.9618 58.893 92.316 66.0476 95.5549C73.2022 98.7938 72.6467 105.746 64.8523 111.033C57.0579 116.319 44.9529 117.965 37.7983 114.726C30.6437 111.487 31.1947 104.54 38.9983 99.2535Z" fill="#A1A7A8"/>
<path d="M72.1144 95.0365C79.9164 89.7372 77.1431 79.2354 84.309 82.4789C91.4748 85.7223 99.5181 98.7237 95.3959 105.782C91.4026 112.62 80.4681 114.868 73.3022 111.624C66.1363 108.381 64.2985 100.33 72.1144 95.0365Z" fill="#A1A7A8"/>
<path d="M92.1239 100.347C90.0563 103.364 87.9841 106.384 85.9071 109.408C83.2887 113.23 77.3302 114.497 69.7152 112.166C58.098 108.62 47.016 98.0505 44.9663 88.5653C44.2521 85.2977 44.7119 82.587 46.0885 80.5744L52.3113 71.5149C50.9358 73.5212 50.4748 76.2383 51.1831 79.505C53.2328 88.9902 64.3159 99.553 75.932 103.105C83.5482 105.431 89.5067 104.163 92.1239 100.347Z" fill="url(#paint1_linear_85_2183)"/>
<g style="mix-blend-mode:overlay" opacity="0.6">
<path d="M75.2091 104.046C82.3787 106.244 88.0797 105.259 90.9305 101.96C87.9154 104.728 82.5188 105.42 75.8358 103.398C64.177 99.8252 53.0533 89.203 50.9963 79.6644C50.2854 76.3793 50.7481 73.6469 52.1286 71.6294L50.7647 73.623C49.9761 75.477 49.8019 77.7057 50.3637 80.312C52.4266 89.8514 63.5504 100.474 75.2091 104.046Z" fill="white"/>
</g>
<path d="M68.3086 69.0824C79.9374 72.6401 91.0325 83.219 93.0903 92.7195C95.148 102.22 87.3921 107.043 75.7455 103.483C64.099 99.9229 53.0217 89.3464 50.9699 79.8467C48.918 70.347 56.6799 65.5247 68.3086 69.0824Z" fill="url(#paint2_linear_85_2183)"/>
<g style="mix-blend-mode:overlay" opacity="0.4">
<path d="M50.9872 79.6931C53.0462 89.2483 64.1807 99.8892 75.8509 103.468C87.3885 107.003 95.132 102.303 93.3132 92.9693L52.129 71.6321C50.74 73.6587 50.2768 76.3958 50.9872 79.6931Z" fill="white"/>
</g>
<path d="M69.2375 73.3708C77.9694 76.0341 86.3009 83.9813 87.8713 91.117C89.4418 98.2528 83.5836 101.87 74.8529 99.2002C66.1221 96.5306 57.7906 88.5834 56.2202 81.4476C54.6498 74.3118 60.502 70.6941 69.2375 73.3708Z" fill="#FFAC47"/>
<path d="M67.8442 75.0487C76.5938 77.7274 84.9432 85.7016 86.4884 92.8512C86.9694 95.0669 86.7385 96.941 85.9375 98.4008C87.7734 96.8556 88.5438 94.3742 87.881 91.1954C86.3371 84.0394 77.9876 76.0652 69.207 73.389C63.1606 71.536 58.5032 72.6965 56.7112 75.9498C58.9907 74.0195 62.9574 73.5572 67.8442 75.0487Z" fill="#F7932F"/>
<path d="M59.3483 98.06C59.4477 100.064 59.5399 102.074 59.6382 104.084C59.6773 105.336 59.3585 106.599 58.7231 107.71C55.8831 112.93 47.28 116.399 39.5045 115.459C33.5801 114.744 29.8259 111.67 29.6682 107.935C29.5759 105.926 29.4765 103.922 29.3842 101.912C29.5599 105.65 33.2962 108.72 39.2205 109.436C46.9841 110.374 55.5992 106.907 58.4392 101.687C59.0759 100.576 59.3928 99.3119 59.3483 98.06Z" fill="url(#paint3_linear_85_2183)"/>
<g style="mix-blend-mode:overlay" opacity="0.6">
<path d="M39.0083 110.01C46.8263 110.964 55.502 107.442 58.3622 102.138C58.7886 101.368 59.0782 100.528 59.2154 99.6644C59.0555 100.386 58.7925 101.085 58.4358 101.736C55.5756 107.04 46.9119 110.564 39.0819 109.609C33.1161 108.882 29.3357 105.759 29.1771 101.964L29.2459 103.399C30.0012 106.69 33.6006 109.324 39.0083 110.01Z" fill="white"/>
</g>
<path d="M49.2592 90.7806C57.0141 91.7169 61.0192 96.7007 58.1753 101.917C55.3314 107.132 46.745 110.589 38.9782 109.651C31.2114 108.713 27.2242 103.731 30.061 98.5211C32.8978 93.3109 41.4984 89.8434 49.2592 90.7806Z" fill="url(#paint4_linear_85_2183)"/>
<g style="mix-blend-mode:overlay" opacity="0.4">
<path d="M35.3902 93.6283L42.2734 109.705C46.1328 109.624 49.9807 108.512 53.4581 106.474L46.696 90.7546C42.827 90.7197 38.9406 91.7076 35.3902 93.6283Z" fill="white"/>
</g>
<path d="M47.9761 93.1329C53.8104 93.8359 56.8118 97.5539 54.6748 101.488C52.5377 105.422 46.0887 108.007 40.2604 107.304C34.432 106.602 31.4306 102.884 33.5629 98.9428C35.6952 95.0016 42.1478 92.4306 47.9761 93.1329Z" fill="#FFAC47"/>
<path d="M34.362 100.253C36.4998 96.3244 42.964 93.7184 48.8039 94.423C51.5538 94.753 53.6718 95.7579 54.9198 97.1385C53.9806 95.1122 51.4858 93.5672 47.9413 93.1391C42.1014 92.4344 35.6372 95.0404 33.4995 98.9688C32.9652 99.8908 32.733 100.956 32.8414 101.987C32.9498 103.018 33.3923 103.953 34.0954 104.637C33.78 103.971 33.6388 103.218 33.6856 102.45C33.7323 101.682 33.9653 100.925 34.362 100.253Z" fill="#F7932F"/>
</g>
<defs>
<linearGradient id="paint0_linear_85_2183" x1="551.526" y1="148.245" x2="-241.732" y2="100.545" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEA84"/>
<stop offset="1" stop-color="#FFC9F1"/>
</linearGradient>
<linearGradient id="paint1_linear_85_2183" x1="43.6854" y1="89.7662" x2="92.4112" y2="98.7972" gradientUnits="userSpaceOnUse">
<stop stop-color="#F28500"/>
<stop offset="1" stop-color="#FBA905"/>
</linearGradient>
<linearGradient id="paint2_linear_85_2183" x1="64.5856" y1="72.876" x2="87.7599" y2="107.655" gradientUnits="userSpaceOnUse">
<stop stop-color="#FBCF0A"/>
<stop offset="1" stop-color="#FBA505"/>
</linearGradient>
<linearGradient id="paint3_linear_85_2183" x1="2489.04" y1="3804.33" x2="3992.61" y2="4082.47" gradientUnits="userSpaceOnUse">
<stop stop-color="#F28500"/>
<stop offset="1" stop-color="#FBA905"/>
</linearGradient>
<linearGradient id="paint4_linear_85_2183" x1="3430.66" y1="3856.51" x2="2146.71" y2="2912.53" gradientUnits="userSpaceOnUse">
<stop stop-color="#FBCF0A"/>
<stop offset="1" stop-color="#FBA505"/>
</linearGradient>
<clipPath id="clip0_85_2183">
<rect width="296" height="128" rx="10" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

@ -0,0 +1,3 @@
<svg width="7" height="9" viewBox="0 0 7 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.25 4.50001H2.375V2.68771C2.375 1.99162 2.87031 1.4133 3.48906 1.40626C4.11406 1.39923 4.625 1.97052 4.625 2.67189V2.95314C4.625 3.18693 4.79219 3.37501 5 3.37501H5.5C5.70781 3.37501 5.875 3.18693 5.875 2.95314V2.67189C5.875 1.19533 4.80469 -0.00525612 3.49219 1.7306e-05C2.17969 0.00529073 1.125 1.22169 1.125 2.69825V4.50001H0.75C0.335938 4.50001 0 4.87794 0 5.34376V8.15625C0 8.62207 0.335938 9 0.75 9H6.25C6.66406 9 7 8.62207 7 8.15625V5.34376C7 4.87794 6.66406 4.50001 6.25 4.50001Z" fill="#EAF6FF"/>
</svg>

After

Width:  |  Height:  |  Size: 616 B

@ -4,6 +4,7 @@ import SINGLE_CALL_BALANCES_ABI from 'single-call-balance-checker-abi';
import { SINGLE_CALL_BALANCES_ADDRESS } from '../constants/contracts';
import { MINUTE } from '../../../shared/constants/time';
import { MAINNET_CHAIN_ID } from '../../../shared/constants/network';
import { isTokenDetectionEnabledForNetwork } from '../../../shared/modules/network.utils';
import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils';
// By default, poll every 3 minutes
@ -24,6 +25,7 @@ export default class DetectTokensController {
* @param config.keyringMemStore
* @param config.tokenList
* @param config.tokensController
* @param config.assetsContractController
*/
constructor({
interval = DEFAULT_INTERVAL,
@ -32,7 +34,9 @@ export default class DetectTokensController {
keyringMemStore,
tokenList,
tokensController,
assetsContractController = null,
} = {}) {
this.assetsContractController = assetsContractController;
this.tokensController = tokensController;
this.preferences = preferences;
this.interval = interval;
@ -44,6 +48,9 @@ export default class DetectTokensController {
return token.address;
});
this.hiddenTokens = this.tokensController?.state.ignoredTokens;
this.detectedTokens = process.env.TOKEN_DETECTION_V2
? this.tokensController?.state.detectedTokens
: [];
preferences?.store.subscribe(({ selectedAddress, useTokenDetection }) => {
if (
@ -55,14 +62,24 @@ export default class DetectTokensController {
this.restartTokenDetection();
}
});
tokensController?.subscribe(({ tokens = [], ignoredTokens = [] }) => {
tokensController?.subscribe(
({ tokens = [], ignoredTokens = [], detectedTokens = [] }) => {
this.tokenAddresses = tokens.map((token) => {
return token.address;
});
this.hiddenTokens = ignoredTokens;
});
this.detectedTokens = process.env.TOKEN_DETECTION_V2
? detectedTokens
: [];
},
);
}
/**
* TODO: Remove during TOKEN_DETECTION_V2 feature flag clean up
*
* @param tokens
*/
async _getTokenBalances(tokens) {
const ethContract = this.web3.eth
.contract(SINGLE_CALL_BALANCES_ABI)
@ -84,14 +101,23 @@ export default class DetectTokensController {
if (!this.isActive) {
return;
}
if (
process.env.TOKEN_DETECTION_V2 &&
(!this.useTokenDetection ||
!isTokenDetectionEnabledForNetwork(
this._network.store.getState().provider.chainId,
))
) {
return;
}
const { tokenList } = this._tokenList.state;
// since the token detection is currently enabled only on Mainnet
// we can use the chainId check to ensure token detection is not triggered for any other network
// but once the balance check contract for other networks are deploayed and ready to use, we need to update this check.
if (
this._network.store.getState().provider.chainId !== MAINNET_CHAIN_ID ||
Object.keys(tokenList).length === 0
!process.env.TOKEN_DETECTION_V2 &&
(this._network.store.getState().provider.chainId !== MAINNET_CHAIN_ID ||
Object.keys(tokenList).length === 0)
) {
return;
}
@ -105,6 +131,9 @@ export default class DetectTokensController {
) &&
!this.hiddenTokens.find((address) =>
isEqualCaseInsensitive(address, tokenAddress),
) &&
!this.detectedTokens.find(({ address }) =>
isEqualCaseInsensitive(address, tokenAddress),
)
) {
tokensToDetect.push(tokenAddress);
@ -117,7 +146,12 @@ export default class DetectTokensController {
for (const tokensSlice of sliceOfTokensToDetect) {
let result;
try {
result = await this._getTokenBalances(tokensSlice);
result = process.env.TOKEN_DETECTION_V2
? await this.assetsContractController.getBalancesInSingleCall(
this.selectedAddress,
tokensSlice,
)
: await this._getTokenBalances(tokensSlice);
} catch (error) {
warn(
`MetaMask - DetectTokensController single call balance fetch failed`,
@ -126,11 +160,35 @@ export default class DetectTokensController {
return;
}
const tokensWithBalance = tokensSlice.filter((_, index) => {
let tokensWithBalance = [];
if (process.env.TOKEN_DETECTION_V2) {
if (result) {
const nonZeroTokenAddresses = Object.keys(result);
for (const nonZeroTokenAddress of nonZeroTokenAddresses) {
const {
address,
symbol,
decimals,
iconUrl,
aggregators,
} = tokenList[nonZeroTokenAddress];
tokensWithBalance.push({
address,
symbol,
decimals,
image: iconUrl,
aggregators,
});
}
if (tokensWithBalance.length > 0) {
await this.tokensController.addDetectedTokens(tokensWithBalance);
}
}
} else {
tokensWithBalance = tokensSlice.filter((_, index) => {
const balance = result[index];
return balance && !balance.isZero();
});
await Promise.all(
tokensWithBalance.map((tokenAddress) => {
return this.tokensController.addToken(
@ -142,6 +200,7 @@ export default class DetectTokensController {
);
}
}
}
/**
* Restart token detection polling period and call detectNewTokens

@ -1,4 +1,4 @@
import { isEqual, merge, omit, omitBy, pickBy } from 'lodash';
import { isEqual, merge, omit, omitBy, pickBy, size, sum } from 'lodash';
import { ObservableStore } from '@metamask/obs-store';
import { bufferToHex, keccak } from 'ethereumjs-util';
import { generateUUID } from 'pubnub';
@ -31,6 +31,7 @@ const exceptionsToFilter = {
* @typedef {import('../../../shared/constants/metametrics').MetaMetricsPagePayload} MetaMetricsPagePayload
* @typedef {import('../../../shared/constants/metametrics').MetaMetricsPageOptions} MetaMetricsPageOptions
* @typedef {import('../../../shared/constants/metametrics').MetaMetricsEventFragment} MetaMetricsEventFragment
* @typedef {import('../../../shared/constants/metametrics').MetaMetricsTraits} MetaMetricsTraits
*/
/**
@ -281,6 +282,30 @@ export default class MetaMetricsController {
this.store.updateState({ fragments });
}
/**
* Calls this._identify with validated metaMetricsId and user traits if user is participating
* in the MetaMetrics analytics program
*
* @param {Object} userTraits
*/
identify(userTraits) {
const { metaMetricsId, participateInMetaMetrics } = this.state;
if (!participateInMetaMetrics || !metaMetricsId || !userTraits) {
return;
}
if (typeof userTraits !== 'object') {
console.warn(
`MetaMetricsController#identify: userTraits parameter must be an object. Received type: ${typeof userTraits}`,
);
return;
}
const allValidTraits = this._buildValidTraits(userTraits);
this._identify(allValidTraits);
}
/**
* Setter for the `participateInMetaMetrics` property
*
@ -434,7 +459,7 @@ export default class MetaMetricsController {
handleMetaMaskStateUpdate(newState) {
const userTraits = this._buildUserTraitsObject(newState);
if (userTraits) {
// this.identify(userTraits);
this.identify(userTraits);
}
}
@ -507,14 +532,33 @@ export default class MetaMetricsController {
};
}
/**
* This method generates the MetaMetrics user traits object, omitting any
* traits that have not changed since the last invocation of this method.
*
* @param {object} metamaskState - Full metamask state object.
* @returns {MetaMetricsTraits | null} traits that have changed since last update
*/
_buildUserTraitsObject(metamaskState) {
/**
* @type {MetaMetricsTraits}
*/
const currentTraits = {
[TRAITS.ADDRESS_BOOK_ENTRIES]: sum(
Object.values(metamaskState.addressBook).map(size),
),
[TRAITS.LEDGER_CONNECTION_TYPE]: metamaskState.ledgerTransportType,
[TRAITS.NUMBER_OF_ACCOUNTS]: Object.values(metamaskState.identities)
.length,
[TRAITS.NETWORKS_ADDED]: metamaskState.frequentRpcListDetail.map(
(rpc) => rpc.chainId,
),
[TRAITS.NFT_AUTODETECTION_ENABLED]: metamaskState.useCollectibleDetection,
[TRAITS.NUMBER_OF_ACCOUNTS]: Object.values(metamaskState.identities)
.length,
[TRAITS.NUMBER_OF_NFT_COLLECTIONS]: this._getNumberOfNFtCollection(
metamaskState,
),
[TRAITS.NUMBER_OF_TOKENS]: this._getNumberOfTokens(metamaskState),
[TRAITS.OPENSEA_API_ENABLED]: metamaskState.openSeaEnabled,
[TRAITS.THREE_BOX_ENABLED]: metamaskState.threeBoxSyncingAllowed,
[TRAITS.THEME]: metamaskState.theme || 'default',
};
@ -536,6 +580,135 @@ export default class MetaMetricsController {
return null;
}
/**
* Returns a new object of all valid user traits. For dates, we transform them into ISO-8601 timestamp strings.
*
* @see {@link https://segment.com/docs/connections/spec/common/#timestamps}
* @param {Object} userTraits
* @returns {Object}
*/
_buildValidTraits(userTraits) {
return Object.entries(userTraits).reduce((validTraits, [key, value]) => {
if (this._isValidTraitDate(value)) {
validTraits[key] = value.toISOString();
} else if (this._isValidTrait(value)) {
validTraits[key] = value;
} else {
console.warn(
`MetaMetricsController: "${key}" value is not a valid trait type`,
);
}
return validTraits;
}, {});
}
/**
*
* @param {object} metamaskState
* @returns number of unique collectible addresses
*/
_getNumberOfNFtCollection(metamaskState) {
const { allCollectibles } = metamaskState;
if (!allCollectibles) {
return 0;
}
const allAddresses = Object.values(allCollectibles)
.flatMap((chainCollectibles) => Object.values(chainCollectibles))
.flat()
.map((collectible) => collectible.address);
const unique = [...new Set(allAddresses)];
return unique.length;
}
/**
* @param {object} metamaskState
* @returns number of unique token addresses
*/
_getNumberOfTokens(metamaskState) {
return Object.values(metamaskState.allTokens).reduce(
(result, accountsByChain) => {
return result + sum(Object.values(accountsByChain).map(size));
},
0,
);
}
/**
* Calls segment.identify with given user traits
*
* @see {@link https://segment.com/docs/connections/sources/catalog/libraries/server/node/#identify}
* @private
* @param {Object} userTraits
*/
_identify(userTraits) {
const { metaMetricsId } = this.state;
if (!userTraits || Object.keys(userTraits).length === 0) {
console.warn('MetaMetricsController#_identify: No userTraits found');
return;
}
try {
this.segment.identify({
userId: metaMetricsId,
traits: userTraits,
});
} catch (err) {
this._captureException(err);
}
}
/**
* Validates the trait value. Segment accepts any data type. We are adding validation here to
* support data types for our Segment destination(s) e.g. MixPanel
*
* @param {*} value
* @returns {boolean}
*/
_isValidTrait(value) {
const type = typeof value;
return (
type === 'string' ||
type === 'boolean' ||
type === 'number' ||
this._isValidTraitArray(value) ||
this._isValidTraitDate(value)
);
}
/**
* Segment accepts any data type value. We have special logic to validate arrays.
*
* @param {*} value
* @returns {boolean}
*/
_isValidTraitArray = (value) => {
return (
Array.isArray(value) &&
(value.every((element) => {
return typeof element === 'string';
}) ||
value.every((element) => {
return typeof element === 'boolean';
}) ||
value.every((element) => {
return typeof element === 'number';
}))
);
};
/**
* Returns true if the value is an accepted date type
*
* @param {*} value
* @returns {boolean}
*/
_isValidTraitDate = (value) => {
return Object.prototype.toString.call(value) === '[object Date]';
};
/**
* Perform validation on the payload and update the id type to use before
* sending to Segment. Also examines the options to route and handle the

@ -23,6 +23,20 @@ const FAKE_CHAIN_ID = '0x1338';
const LOCALE = 'en_US';
const TEST_META_METRICS_ID = '0xabc';
const MOCK_TRAITS = {
test_boolean: true,
test_string: 'abc',
test_number: 123,
test_bool_array: [true, true, false],
test_string_array: ['test', 'test', 'test'],
test_boolean_array: [1, 2, 3],
};
const MOCK_INVALID_TRAITS = {
test_null: null,
test_array_multi_types: [true, 'a', 1],
};
const DEFAULT_TEST_CONTEXT = {
app: { name: 'MetaMask Extension', version: VERSION },
page: METAMETRICS_BACKGROUND_PAGE_OBJECT,
@ -216,6 +230,78 @@ describe('MetaMetricsController', function () {
});
});
describe('identify', function () {
it('should call segment.identify for valid traits if user is participating in metametrics', async function () {
const metaMetricsController = getMetaMetricsController({
participateInMetaMetrics: true,
metaMetricsId: TEST_META_METRICS_ID,
});
const mock = sinon.mock(segment);
mock
.expects('identify')
.once()
.withArgs({ userId: TEST_META_METRICS_ID, traits: MOCK_TRAITS });
metaMetricsController.identify({
...MOCK_TRAITS,
...MOCK_INVALID_TRAITS,
});
mock.verify();
});
it('should transform date type traits into ISO-8601 timestamp strings', async function () {
const metaMetricsController = getMetaMetricsController({
participateInMetaMetrics: true,
metaMetricsId: TEST_META_METRICS_ID,
});
const mock = sinon.mock(segment);
const mockDate = new Date();
const mockDateISOString = mockDate.toISOString();
mock
.expects('identify')
.once()
.withArgs({
userId: TEST_META_METRICS_ID,
traits: {
test_date: mockDateISOString,
},
});
metaMetricsController.identify({
test_date: mockDate,
});
mock.verify();
});
it('should not call segment.identify if user is not participating in metametrics', function () {
const metaMetricsController = getMetaMetricsController({
participateInMetaMetrics: false,
});
const mock = sinon.mock(segment);
mock.expects('identify').never();
metaMetricsController.identify(MOCK_TRAITS);
mock.verify();
});
it('should not call segment.identify if there are no valid traits to identify', async function () {
const metaMetricsController = getMetaMetricsController({
participateInMetaMetrics: true,
metaMetricsId: TEST_META_METRICS_ID,
});
const mock = sinon.mock(segment);
mock.expects('identify').never();
metaMetricsController.identify(MOCK_INVALID_TRAITS);
mock.verify();
});
});
describe('setParticipateInMetaMetrics', function () {
it('should update the value of participateInMetaMetrics', function () {
const metaMetricsController = getMetaMetricsController({
@ -525,23 +611,88 @@ describe('MetaMetricsController', function () {
describe('_buildUserTraitsObject', function () {
it('should return full user traits object on first call', function () {
const MOCK_ALL_TOKENS = {
'0x1': {
'0x1235ce91d74254f29d4609f25932fe6d97bf4842': [
{
address: '0xd2cea331e5f5d8ee9fb1055c297795937645de91',
},
{
address: '0xabc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9',
},
],
'0xe364b0f9d1879e53e8183055c9d7dd2b7375d86b': [
{
address: '0xd2cea331e5f5d8ee9fb1055c297795937645de91',
},
],
},
'0x4': {
'0x1235ce91d74254f29d4609f25932fe6d97bf4842': [
{
address: '0xd2cea331e5f5d8ee9fb1055c297795937645de91',
},
{
address: '0x12317F958D2ee523a2206206994597C13D831ec7',
},
],
},
};
const metaMetricsController = getMetaMetricsController();
const traits = metaMetricsController._buildUserTraitsObject({
addressBook: {
[MAINNET_CHAIN_ID]: [{ address: '0x' }],
[ROPSTEN_CHAIN_ID]: [{ address: '0x' }, { address: '0x0' }],
},
allCollectibles: {
'0xac706cE8A9BF27Afecf080fB298d0ee13cfb978A': {
56: [
{
address: '0xd2cea331e5f5d8ee9fb1055c297795937645de91',
tokenId: '100',
},
{
address: '0xd2cea331e5f5d8ee9fb1055c297795937645de91',
tokenId: '101',
},
{
address: '0x7488d2ce5deb26db021285b50b661d655eb3d3d9',
tokenId: '99',
},
],
},
'0xe04AB39684A24D8D4124b114F3bd6FBEB779cacA': {
69: [
{
address: '0x63d646bc7380562376d5de205123a57b1718184d',
tokenId: '14',
},
],
},
},
allTokens: MOCK_ALL_TOKENS,
frequentRpcListDetail: [
{ chainId: MAINNET_CHAIN_ID },
{ chainId: ROPSTEN_CHAIN_ID },
],
ledgerTransportType: 'web-hid',
identities: [{}, {}],
ledgerTransportType: 'web-hid',
openSeaEnabled: true,
threeBoxSyncingAllowed: false,
useCollectibleDetection: false,
theme: 'default',
});
assert.deepEqual(traits, {
[TRAITS.THREE_BOX_ENABLED]: false,
[TRAITS.ADDRESS_BOOK_ENTRIES]: 3,
[TRAITS.LEDGER_CONNECTION_TYPE]: 'web-hid',
[TRAITS.NUMBER_OF_ACCOUNTS]: 2,
[TRAITS.NETWORKS_ADDED]: [MAINNET_CHAIN_ID, ROPSTEN_CHAIN_ID],
[TRAITS.NFT_AUTODETECTION_ENABLED]: false,
[TRAITS.NUMBER_OF_ACCOUNTS]: 2,
[TRAITS.NUMBER_OF_NFT_COLLECTIONS]: 3,
[TRAITS.NUMBER_OF_TOKENS]: 5,
[TRAITS.OPENSEA_API_ENABLED]: true,
[TRAITS.THREE_BOX_ENABLED]: false,
[TRAITS.THEME]: 'default',
});
@ -550,53 +701,86 @@ describe('MetaMetricsController', function () {
it('should return only changed traits object on subsequent calls', function () {
const metaMetricsController = getMetaMetricsController();
metaMetricsController._buildUserTraitsObject({
addressBook: {
[MAINNET_CHAIN_ID]: [{ address: '0x' }],
[ROPSTEN_CHAIN_ID]: [{ address: '0x' }, { address: '0x0' }],
},
allTokens: {},
frequentRpcListDetail: [
{ chainId: MAINNET_CHAIN_ID },
{ chainId: ROPSTEN_CHAIN_ID },
],
ledgerTransportType: 'web-hid',
openSeaEnabled: true,
identities: [{}, {}],
threeBoxSyncingAllowed: false,
useCollectibleDetection: false,
theme: 'default',
});
const updatedTraits = metaMetricsController._buildUserTraitsObject({
addressBook: {
[MAINNET_CHAIN_ID]: [{ address: '0x' }, { address: '0x1' }],
[ROPSTEN_CHAIN_ID]: [{ address: '0x' }, { address: '0x0' }],
},
allTokens: {
'0x1': { '0xabcde': [{ '0x12345': { address: '0xtestAddress' } }] },
},
frequentRpcListDetail: [
{ chainId: MAINNET_CHAIN_ID },
{ chainId: ROPSTEN_CHAIN_ID },
],
ledgerTransportType: 'web-hid',
openSeaEnabled: false,
identities: [{}, {}, {}],
threeBoxSyncingAllowed: false,
useCollectibleDetection: false,
theme: 'default',
});
assert.deepEqual(updatedTraits, {
[TRAITS.ADDRESS_BOOK_ENTRIES]: 4,
[TRAITS.NUMBER_OF_ACCOUNTS]: 3,
[TRAITS.NUMBER_OF_TOKENS]: 1,
[TRAITS.OPENSEA_API_ENABLED]: false,
});
});
it('should return null if no traits changed', function () {
const metaMetricsController = getMetaMetricsController();
metaMetricsController._buildUserTraitsObject({
addressBook: {
[MAINNET_CHAIN_ID]: [{ address: '0x' }],
[ROPSTEN_CHAIN_ID]: [{ address: '0x' }, { address: '0x0' }],
},
allTokens: {},
frequentRpcListDetail: [
{ chainId: MAINNET_CHAIN_ID },
{ chainId: ROPSTEN_CHAIN_ID },
],
ledgerTransportType: 'web-hid',
openSeaEnabled: true,
identities: [{}, {}],
threeBoxSyncingAllowed: false,
useCollectibleDetection: true,
theme: 'default',
});
const updatedTraits = metaMetricsController._buildUserTraitsObject({
addressBook: {
[MAINNET_CHAIN_ID]: [{ address: '0x' }],
[ROPSTEN_CHAIN_ID]: [{ address: '0x' }, { address: '0x0' }],
},
allTokens: {},
frequentRpcListDetail: [
{ chainId: MAINNET_CHAIN_ID },
{ chainId: ROPSTEN_CHAIN_ID },
],
ledgerTransportType: 'web-hid',
openSeaEnabled: true,
identities: [{}, {}],
threeBoxSyncingAllowed: false,
useCollectibleDetection: true,
theme: 'default',
});

@ -38,7 +38,7 @@ export default class PreferencesController {
// 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,
useTokenDetection: Boolean(process.env.TOKEN_DETECTION_V2),
useCollectibleDetection: false,
openSeaEnabled: false,
advancedGasFee: null,

@ -361,13 +361,30 @@ export default class TransactionController extends EventEmitter {
return transactions[txId];
}
_checkIfTxStatusIsUnapproved(txId) {
/**
* @param {number} txId
* @returns {boolean}
*/
_isUnapprovedTransaction(txId) {
return (
this.txStateManager.getTransaction(txId).status ===
TRANSACTION_STATUSES.UNAPPROVED
);
}
/**
* @param {number} txId
* @param {string} fnName
*/
_throwErrorIfNotUnapprovedTx(txId, fnName) {
if (!this._isUnapprovedTransaction(txId)) {
throw new Error(
`TransactionsController: Can only call ${fnName} on an unapproved transaction.
Current tx status: ${this.txStateManager.getTransaction(txId).status}`,
);
}
}
_updateTransaction(txId, proposedUpdate, note) {
const txMeta = this.txStateManager.getTransaction(txId);
const updated = merge(txMeta, proposedUpdate);
@ -416,11 +433,7 @@ export default class TransactionController extends EventEmitter {
* @returns {TransactionMeta} the txMeta of the updated transaction
*/
updateEditableParams(txId, { data, from, to, value, gas, gasPrice }) {
if (!this._checkIfTxStatusIsUnapproved(txId)) {
throw new Error(
'Cannot call updateEditableParams on a transaction that is not in an unapproved state',
);
}
this._throwErrorIfNotUnapprovedTx(txId, 'updateEditableParams');
const editableParams = {
txParams: {
@ -474,11 +487,7 @@ export default class TransactionController extends EventEmitter {
userFeeLevel,
},
) {
if (!this._checkIfTxStatusIsUnapproved(txId)) {
throw new Error(
'Cannot call updateTransactionGasFees on a transaction that is not in an unapproved state',
);
}
this._throwErrorIfNotUnapprovedTx(txId, 'updateTransactionGasFees');
let txGasFees = {
txParams: {
@ -517,11 +526,10 @@ export default class TransactionController extends EventEmitter {
txId,
{ estimatedBaseFee, decEstimatedBaseFee },
) {
if (!this._checkIfTxStatusIsUnapproved(txId)) {
throw new Error(
'Cannot call updateTransactionEstimatedBaseFee on a transaction that is not in an unapproved state',
this._throwErrorIfNotUnapprovedTx(
txId,
'updateTransactionEstimatedBaseFee',
);
}
let txEstimateBaseFees = { estimatedBaseFee, decEstimatedBaseFee };
// only update what is defined
@ -543,11 +551,7 @@ export default class TransactionController extends EventEmitter {
* @returns {TransactionMeta} the txMeta of the updated transaction
*/
updateSwapApprovalTransaction(txId, { type, sourceTokenSymbol }) {
if (!this._checkIfTxStatusIsUnapproved(txId)) {
throw new Error(
'Cannot call updateSwapApprovalTransaction on a transaction that is not in an unapproved state',
);
}
this._throwErrorIfNotUnapprovedTx(txId, 'updateSwapApprovalTransaction');
let swapApprovalTransaction = { type, sourceTokenSymbol };
// only update what is defined
@ -589,11 +593,7 @@ export default class TransactionController extends EventEmitter {
approvalTxId,
},
) {
if (!this._checkIfTxStatusIsUnapproved(txId)) {
throw new Error(
'Cannot call updateSwapTransaction on a transaction that is not in an unapproved state',
);
}
this._throwErrorIfNotUnapprovedTx(txId, 'updateSwapTransaction');
let swapTransaction = {
sourceTokenSymbol,
@ -625,11 +625,7 @@ export default class TransactionController extends EventEmitter {
* @returns {TransactionMeta} the txMeta of the updated transaction
*/
updateTransactionUserSettings(txId, { userEditedGasLimit, userFeeLevel }) {
if (!this._checkIfTxStatusIsUnapproved(txId)) {
throw new Error(
'Cannot call updateTransactionUserSettings on a transaction that is not in an unapproved state',
);
}
this._throwErrorIfNotUnapprovedTx(txId, 'updateTransactionUserSettings');
let userSettings = { userEditedGasLimit, userFeeLevel };
// only update what is defined

@ -2186,10 +2186,10 @@ describe('Transaction Controller', function () {
assert.equal(result.userFeeLevel, 'high');
});
it('throws error if status is not unapproved', function () {
it('should not update and should throw error if status is not type "unapproved"', function () {
txStateManager.addTransaction({
id: '4',
status: TRANSACTION_STATUSES.APPROVED,
status: TRANSACTION_STATUSES.DROPPED,
metamaskNetworkId: currentNetworkId,
txParams: {
maxPriorityFeePerGas: '0x007',
@ -2200,14 +2200,18 @@ describe('Transaction Controller', function () {
estimateUsed: '0x009',
});
try {
txController.updateTransactionGasFees('4', { maxFeePerGas: '0x0088' });
} catch (e) {
assert.equal(
e.message,
'Cannot call updateTransactionGasFees on a transaction that is not in an unapproved state',
assert.throws(
() =>
txController.updateTransactionGasFees('4', {
maxFeePerGas: '0x0088',
}),
Error,
`TransactionsController: Can only call updateTransactionGasFees on an unapproved transaction.
Current tx status: ${TRANSACTION_STATUSES.DROPPED}`,
);
}
const transaction = txStateManager.getTransaction('4');
assert.equal(transaction.txParams.maxFeePerGas, '0x008');
});
it('does not update unknown parameters in update method', function () {

@ -0,0 +1,86 @@
import { EVENT_NAMES } from '../../../shared/constants/metametrics';
import { SECOND } from '../../../shared/constants/time';
const USER_PROMPTED_EVENT_NAME_MAP = {
eth_signTypedData_v4: EVENT_NAMES.SIGNATURE_REQUESTED,
eth_signTypedData_v3: EVENT_NAMES.SIGNATURE_REQUESTED,
eth_signTypedData: EVENT_NAMES.SIGNATURE_REQUESTED,
eth_personal_sign: EVENT_NAMES.SIGNATURE_REQUESTED,
eth_sign: EVENT_NAMES.SIGNATURE_REQUESTED,
eth_getEncryptionPublicKey: EVENT_NAMES.ENCRYPTION_PUBLIC_KEY_REQUESTED,
eth_decrypt: EVENT_NAMES.DECRYPTION_REQUESTED,
wallet_requestPermissions: EVENT_NAMES.PERMISSIONS_REQUESTED,
eth_requestAccounts: EVENT_NAMES.PERMISSIONS_REQUESTED,
};
const samplingTimeouts = {};
/**
* Returns a middleware that tracks inpage_provider usage using sampling for
* each type of event except those that require user interaction, such as
* signature requests
*
* @param {object} opts - options for the rpc method tracking middleware
* @param {Function} opts.trackEvent - trackEvent method from MetaMetricsController
* @param {Function} opts.getMetricsState - get the state of MetaMetricsController
* @returns {Function}
*/
export default function createRPCMethodTrackingMiddleware({
trackEvent,
getMetricsState,
}) {
return function rpcMethodTrackingMiddleware(
/** @type {any} */ req,
/** @type {any} */ res,
/** @type {Function} */ next,
) {
const startTime = Date.now();
const { origin } = req;
next((callback) => {
const endTime = Date.now();
if (!getMetricsState().participateInMetaMetrics) {
return callback();
}
if (USER_PROMPTED_EVENT_NAME_MAP[req.method]) {
const userRejected = res.error?.code === 4001;
trackEvent({
event: USER_PROMPTED_EVENT_NAME_MAP[req.method],
category: 'inpage_provider',
referrer: {
url: origin,
},
properties: {
method: req.method,
status: userRejected ? 'rejected' : 'approved',
error_code: res.error?.code,
error_message: res.error?.message,
has_result: typeof res.result !== 'undefined',
duration: endTime - startTime,
},
});
} else if (typeof samplingTimeouts[req.method] === 'undefined') {
trackEvent({
event: 'Provider Method Called',
category: 'inpage_provider',
referrer: {
url: origin,
},
properties: {
method: req.method,
error_code: res.error?.code,
error_message: res.error?.message,
has_result: typeof res.result !== 'undefined',
duration: endTime - startTime,
},
});
// Only record one call to this method every ten seconds to avoid
// overloading network requests.
samplingTimeouts[req.method] = setTimeout(() => {
delete samplingTimeouts[req.method];
}, SECOND * 10);
}
return callback();
});
};
}

@ -1,9 +1,6 @@
import Analytics from 'analytics-node';
import { SECOND } from '../../../shared/constants/time';
const isDevEnvironment = Boolean(
process.env.METAMASK_DEBUG && !process.env.IN_TEST,
);
const SEGMENT_WRITE_KEY = process.env.SEGMENT_WRITE_KEY ?? null;
const SEGMENT_HOST = process.env.SEGMENT_HOST ?? null;
@ -81,11 +78,10 @@ export const createSegmentMock = (flushAt = SEGMENT_FLUSH_AT) => {
return segmentMock;
};
export const segment =
!SEGMENT_WRITE_KEY || (isDevEnvironment && !SEGMENT_HOST)
? createSegmentMock(SEGMENT_FLUSH_AT, SEGMENT_FLUSH_INTERVAL)
: new Analytics(SEGMENT_WRITE_KEY, {
export const segment = SEGMENT_WRITE_KEY
? new Analytics(SEGMENT_WRITE_KEY, {
host: SEGMENT_HOST,
flushAt: SEGMENT_FLUSH_AT,
flushInterval: SEGMENT_FLUSH_INTERVAL,
});
})
: createSegmentMock(SEGMENT_FLUSH_AT, SEGMENT_FLUSH_INTERVAL);

@ -136,6 +136,7 @@ import {
buildSnapRestrictedMethodSpecifications,
///: END:ONLY_INCLUDE_IN
} from './controllers/permissions';
import createRPCMethodTrackingMiddleware from './lib/createRPCMethodTrackingMiddleware';
export const METAMASK_CONTROLLER_EVENTS = {
// Fired after state changes that impact the extension badge (unapproved msg count)
@ -232,8 +233,27 @@ export default class MetamaskController extends EventEmitter {
config: { provider: this.provider },
state: initState.TokensController,
});
this.assetsContractController = new AssetsContractController(
process.env.TOKEN_DETECTION_V2
? (this.assetsContractController = new AssetsContractController({
onPreferencesStateChange: (listener) =>
this.preferencesController.store.subscribe(listener),
onNetworkStateChange: (cb) =>
this.networkController.store.subscribe((networkState) => {
const modifiedNetworkState = {
...networkState,
provider: {
...networkState.provider,
chainId: hexToDecimal(networkState.provider.chainId),
},
};
return cb(modifiedNetworkState);
}),
config: {
provider: this.provider,
},
state: initState.AssetsContractController,
}))
: (this.assetsContractController = new AssetsContractController(
{
onPreferencesStateChange: (listener) =>
this.preferencesController.store.subscribe(listener),
@ -241,7 +261,7 @@ export default class MetamaskController extends EventEmitter {
{
provider: this.provider,
},
);
));
this.collectiblesController = new CollectiblesController(
{
@ -382,7 +402,24 @@ export default class MetamaskController extends EventEmitter {
const tokenListMessenger = this.controllerMessenger.getRestricted({
name: 'TokenListController',
});
this.tokenListController = new TokenListController({
process.env.TOKEN_DETECTION_V2
? (this.tokenListController = new TokenListController({
chainId: hexToDecimal(this.networkController.getCurrentChainId()),
onNetworkStateChange: (cb) =>
this.networkController.store.subscribe((networkState) => {
const modifiedNetworkState = {
...networkState,
provider: {
...networkState.provider,
chainId: hexToDecimal(networkState.provider.chainId),
},
};
return cb(modifiedNetworkState);
}),
messenger: tokenListMessenger,
state: initState.TokenListController,
}))
: (this.tokenListController = new TokenListController({
chainId: hexToDecimal(this.networkController.getCurrentChainId()),
useStaticTokenList: !this.preferencesController.store.getState()
.useTokenDetection,
@ -408,7 +445,7 @@ export default class MetamaskController extends EventEmitter {
}),
messenger: tokenListMessenger,
state: initState.TokenListController,
});
}));
this.phishingController = new PhishingController();
@ -591,7 +628,7 @@ export default class MetamaskController extends EventEmitter {
this.workerController = new IframeExecutionService({
onError: this.onExecutionEnvironmentError.bind(this),
iframeUrl: new URL(
'https://metamask.github.io/iframe-execution-environment/0.4.2',
'https://metamask.github.io/iframe-execution-environment/0.4.3',
),
messenger: this.controllerMessenger.getRestricted({
name: 'ExecutionService',
@ -656,13 +693,22 @@ export default class MetamaskController extends EventEmitter {
});
///: END:ONLY_INCLUDE_IN
this.detectTokensController = new DetectTokensController({
process.env.TOKEN_DETECTION_V2
? (this.detectTokensController = new DetectTokensController({
preferences: this.preferencesController,
tokensController: this.tokensController,
assetsContractController: this.assetsContractController,
network: this.networkController,
keyringMemStore: this.keyringController.memStore,
tokenList: this.tokenListController,
});
}))
: (this.detectTokensController = new DetectTokensController({
preferences: this.preferencesController,
tokensController: this.tokensController,
network: this.networkController,
keyringMemStore: this.keyringController.memStore,
tokenList: this.tokenListController,
}));
this.addressBookController = new AddressBookController(
undefined,
@ -1026,10 +1072,10 @@ export default class MetamaskController extends EventEmitter {
'SnapController:add',
),
clearSnapState: (fromSubject) =>
this.controllerMessenger(
'SnapController:updateSnap',
this.controllerMessenger.call(
'SnapController:updateSnapState',
fromSubject,
{},
null,
),
getMnemonic: this.getPrimaryKeyringMnemonic.bind(this),
getSnap: this.controllerMessenger.call.bind(
@ -1040,16 +1086,10 @@ export default class MetamaskController extends EventEmitter {
this.controllerMessenger,
'SnapController:getRpcMessageHandler',
),
getSnapState: async (...args) => {
// TODO:flask Just return the action result directly in the next
// @metamask/snap-controllers update.
return (
(await this.controllerMessenger.call(
getSnapState: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapController:getSnapState',
...args,
)) ?? null
);
},
),
showConfirmation: (origin, confirmationData) =>
this.approvalController.addAndShowApprovalRequest({
origin,
@ -1334,6 +1374,7 @@ export default class MetamaskController extends EventEmitter {
tokensController,
smartTransactionsController,
txController,
assetsContractController,
} = this;
return {
@ -1814,6 +1855,22 @@ export default class MetamaskController extends EventEmitter {
collectibleDetectionController,
)
: null,
/** Token Detection V2 */
addDetectedTokens: process.env.TOKEN_DETECTION_V2
? tokensController.addDetectedTokens.bind(tokensController)
: null,
importTokens: process.env.TOKEN_DETECTION_V2
? tokensController.importTokens.bind(tokensController)
: null,
ignoreTokens: process.env.TOKEN_DETECTION_V2
? tokensController.ignoreTokens.bind(tokensController)
: null,
getBalancesInSingleCall: process.env.TOKEN_DETECTION_V2
? assetsContractController.getBalancesInSingleCall.bind(
assetsContractController,
)
: null,
};
}
@ -3328,6 +3385,17 @@ export default class MetamaskController extends EventEmitter {
engine.push(createLoggerMiddleware({ origin }));
engine.push(this.permissionLogController.createMiddleware());
engine.push(
createRPCMethodTrackingMiddleware({
trackEvent: this.metaMetricsController.trackEvent.bind(
this.metaMetricsController,
),
getMetricsState: this.metaMetricsController.store.getState.bind(
this.metaMetricsController.store,
),
}),
);
// onboarding
if (subjectType === SUBJECT_TYPES.WEBSITE) {
engine.push(

@ -1,7 +1,6 @@
"project_id_env": CROWDIN_PROJECT_ID
"api_token_env": CROWDIN_PERSONAL_TOKEN
"base_path" : "."
"base_url" : "https://metamask.crowdin.com"
"preserve_hierarchy": true

@ -3,3 +3,54 @@
Several files which are needed for developing on MetaMask.
Usually each file or directory contains information about its scope / usage.
## Segment
### Debugging with the Mock Segment API
To start the [Mock Segment API](./mock-segment.js):
- Add/replace the `SEGMENT_HOST` and `SEGMENT_WRITE_KEY` variables in `.metamaskrc`
```
SEGMENT_HOST='http://localhost:9090'
SEGMENT_WRITE_KEY='FAKE'
```
- Build the project to the `./dist/` folder with `yarn dist`
- Run the Mock Segment API from the command line
```
node development/mock-segment.js
```
Events triggered whilst using the extension will be logged to the console of the Mock Segment API.
More information on the API and its usage can be found [here](./mock-segment.js#L28).
### Debugging in Segment
To debug in a production Segment environment:
- Create a free account on [Segment](https://segment.com/)
- Create a New Workspace
- Add a Source (Node.js)
- Copy the `Write Key` from the API Keys section under Settings
- Add/replace the `SEGMENT_HOST` and `SEGMENT_WRITE_KEY` variables in `.metamaskrc`
```
SEGMENT_HOST='https://api.segment.io'
SEGMENT_WRITE_KEY='COPIED_WRITE_KEY'
```
- Build the project to the `./dist/` folder with `yarn dist`
Events triggered whilst using the extension will be displayed in Segment's Debugger.
### Debugging Segment requests in MetaMask
To opt in to MetaMetrics;
- Unlock the extension
- Open the Account menu
- Click the `Settings` menu item
- Click the `Security & Privacy` menu item
- Toggle the `Participate in MetaMetrics` menu option to the `ON` position
You can inspect the requests in the `Network` tab of your browser's Developer Tools (background.html)
by filtering for `POST` requests to `/v1/batch`. The full url will be `http://localhost:9090/v1/batch`
or `https://api.segment.io/v1/batch` respectively.

@ -19,6 +19,8 @@ async function start() {
console.log('CIRCLE_SHA1', CIRCLE_SHA1);
const { CIRCLE_BUILD_NUM } = process.env;
console.log('CIRCLE_BUILD_NUM', CIRCLE_BUILD_NUM);
const { CIRCLE_WORKFLOW_JOB_ID } = process.env;
console.log('CIRCLE_WORKFLOW_JOB_ID', CIRCLE_WORKFLOW_JOB_ID);
if (!CIRCLE_PULL_REQUEST) {
console.warn(`No pull request detected for commit "${CIRCLE_SHA1}"`);
@ -27,7 +29,7 @@ async function start() {
const CIRCLE_PR_NUMBER = CIRCLE_PULL_REQUEST.split('/').pop();
const SHORT_SHA1 = CIRCLE_SHA1.slice(0, 7);
const BUILD_LINK_BASE = `https://${CIRCLE_BUILD_NUM}-42009758-gh.circle-artifacts.com/0`;
const BUILD_LINK_BASE = `https://output.circle-artifacts.com/output/job/${CIRCLE_WORKFLOW_JOB_ID}/artifacts/0`;
// build the github comment content

@ -13,8 +13,5 @@ To learn how to develop MetaMask-compatible applications, visit our [Developer D
- [How to add custom build to Chrome](./add-to-chrome.md)
- [How to add custom build to Firefox](./add-to-firefox.md)
- [Publishing Guide](./publishing.md)
- [How to live reload on local dependency changes](./developing-on-deps.md)
- [How to add new networks to the Provider Menu](./adding-new-networks.md)
- [How to port MetaMask to a new platform](./porting_to_new_environment.md)
- [How to generate a visualization of this repository's development](./development-visualization.md)
- [How to add a feature behind a secret feature flag](./secret-preferences.md)
- [Developing on MetaMask](../development/README.md)

@ -115,21 +115,21 @@
"@material-ui/core": "^4.11.0",
"@metamask/contract-metadata": "^1.31.0",
"@metamask/controllers": "^27.0.0",
"@metamask/design-tokens": "^1.4.2",
"@metamask/eth-ledger-bridge-keyring": "^0.10.0",
"@metamask/design-tokens": "^1.5.1",
"@metamask/eth-ledger-bridge-keyring": "^0.11.0",
"@metamask/eth-token-tracker": "^4.0.0",
"@metamask/etherscan-link": "^2.1.0",
"@metamask/iframe-execution-environment-service": "^0.10.6",
"@metamask/iframe-execution-environment-service": "^0.10.7",
"@metamask/jazzicon": "^2.0.0",
"@metamask/logo": "^3.1.1",
"@metamask/metamask-eth-abis": "^3.0.0",
"@metamask/obs-store": "^5.0.0",
"@metamask/post-message-stream": "^4.0.0",
"@metamask/providers": "^8.1.1",
"@metamask/rpc-methods": "^0.10.6",
"@metamask/rpc-methods": "^0.10.7",
"@metamask/slip44": "^2.0.0",
"@metamask/smart-transactions-controller": "^1.10.0",
"@metamask/snap-controllers": "^0.10.6",
"@metamask/snap-controllers": "^0.10.7",
"@ngraveio/bc-ur": "^1.1.6",
"@popperjs/core": "^2.4.0",
"@reduxjs/toolkit": "^1.6.2",
@ -282,7 +282,7 @@
"browser-util-inspect": "^0.2.0",
"browserify": "^16.5.1",
"chalk": "^3.0.0",
"chromedriver": "^99.0.0",
"chromedriver": "^100.0.0",
"concurrently": "^5.2.0",
"copy-webpack-plugin": "^6.0.3",
"cross-spawn": "^7.0.3",
@ -309,7 +309,7 @@
"fancy-log": "^1.3.3",
"fast-glob": "^3.2.2",
"fs-extra": "^8.1.0",
"ganache": "^v7.0.0-rc.0",
"ganache": "^v7.0.4",
"geckodriver": "^1.21.0",
"globby": "^11.0.4",
"gulp": "^4.0.2",

@ -156,14 +156,26 @@
/**
* @typedef {Object} Traits
* @property {string} [LEDGER_CONNECTION_TYPE] - when ledger live connnection
* type is changed we identify the ledger_connection_type trait
* @property {string} [NETWORKS_ADDED] - when user modifies networks we
* identify the networks_added trait
* @property {string} [NUMBER_OF_ACCOUNTS] - when identities change, we
* identify the new number_of_accounts trait
* @property {string} [THREE_BOX_ENABLED] - when 3box feature is toggled we
* identify the 3box_enabled trait
* @property {'address_book_entries'} ADDRESS_BOOK_ENTRIES - When the user
* adds or modifies addresses in address book the address_book_entries trait
* is identified.
* @property {'ledger_connection_type'} LEDGER_CONNECTION_TYPE - when ledger
* live connnection type is changed we identify the ledger_connection_type
* trait
* @property {'networks_added'} NETWORKS_ADDED - when user modifies networks
* we identify the networks_added trait
* @property {'nft_autodetection_enabled'} NFT_AUTODETECTION_ENABLED - when Autodetect NFTs
* feature is toggled we identify the nft_autodetection_enabled trait
* @property {'number_of_accounts'} NUMBER_OF_ACCOUNTS - when identities
* change, we identify the new number_of_accounts trait
* @property {'number_of_nft_collections'} NUMBER_OF_NFT_COLLECTIONS - user
* trait for number of unique NFT addresses
* @property {'number_of_tokens'} NUMBER_OF_TOKENS - when the number of tokens change, we
* identify the new number_of_tokens trait
* @property {'opensea_api_enabled'} OPENSEA_API_ENABLED - when the OpenSea API is enabled
* we identify the opensea_api_enabled trait
* @property {'three_box_enabled'} THREE_BOX_ENABLED - when 3box feature is
* toggled we identify the 3box_enabled trait
* @property {'theme'} THEME - when the user's theme changes we identify the theme trait
*/
@ -173,10 +185,15 @@
*/
export const TRAITS = {
ADDRESS_BOOK_ENTRIES: 'address_book_entries',
LEDGER_CONNECTION_TYPE: 'ledger_connection_type',
THREE_BOX_ENABLED: 'three_box_enabled',
NUMBER_OF_ACCOUNTS: 'number_of_accounts',
NETWORKS_ADDED: 'networks_added',
NFT_AUTODETECTION_ENABLED: 'nft_autodetection_enabled',
NUMBER_OF_ACCOUNTS: 'number_of_accounts',
NUMBER_OF_NFT_COLLECTIONS: 'number_of_nft_collections',
NUMBER_OF_TOKENS: 'number_of_tokens',
OPENSEA_API_ENABLED: 'opensea_api_enabled',
THREE_BOX_ENABLED: 'three_box_enabled',
THEME: 'theme',
};
@ -238,3 +255,10 @@ export const METAMETRICS_BACKGROUND_PAGE_OBJECT = {
export const REJECT_NOTFICIATION_CLOSE = 'Cancel Via Notification Close';
export const REJECT_NOTFICIATION_CLOSE_SIG =
'Cancel Sig Request Via Notification Close';
export const EVENT_NAMES = {
SIGNATURE_REQUESTED: 'Signature Requested',
ENCRYPTION_PUBLIC_KEY_REQUESTED: 'Encryption Public Key Requested',
DECRYPTION_REQUESTED: 'Decryption Requested',
PERMISSIONS_REQUESTED: 'Permissions Requested',
};

@ -39,6 +39,9 @@ export const KOVAN_DISPLAY_NAME = 'Kovan';
export const MAINNET_DISPLAY_NAME = 'Ethereum Mainnet';
export const GOERLI_DISPLAY_NAME = 'Goerli';
export const LOCALHOST_DISPLAY_NAME = 'Localhost 8545';
export const BSC_DISPLAY_NAME = 'Binance Smart Chain';
export const POLYGON_DISPLAY_NAME = 'Polygon';
export const AVALANCHE_DISPLAY_NAME = 'Avalanche';
const infuraProjectId = process.env.INFURA_PROJECT_ID;
export const getRpcUrl = ({ network, excludeProjectId = false }) =>
@ -127,6 +130,13 @@ export const CHAIN_ID_TO_RPC_URL_MAP = {
[LOCALHOST_CHAIN_ID]: LOCALHOST_RPC_URL,
};
export const CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP = {
[MAINNET_CHAIN_ID]: ETH_TOKEN_IMAGE_URL,
[AVALANCHE_CHAIN_ID]: AVAX_TOKEN_IMAGE_URL,
[BSC_CHAIN_ID]: BNB_TOKEN_IMAGE_URL,
[POLYGON_CHAIN_ID]: MATIC_TOKEN_IMAGE_URL,
};
export const CHAIN_ID_TO_NETWORK_ID_MAP = Object.values(
NETWORK_TYPE_TO_ID_MAP,
).reduce((chainIdToNetworkIdMap, { chainId, networkId }) => {

@ -114,14 +114,18 @@ const RINKEBY_DEFAULT_BLOCK_EXPLORER_URL = 'https://rinkeby.etherscan.io/';
const POLYGON_DEFAULT_BLOCK_EXPLORER_URL = 'https://polygonscan.com/';
const AVALANCHE_DEFAULT_BLOCK_EXPLORER_URL = 'https://snowtrace.io/';
export const ALLOWED_SWAPS_CHAIN_IDS = {
[MAINNET_CHAIN_ID]: true,
[SWAPS_TESTNET_CHAIN_ID]: true,
[BSC_CHAIN_ID]: true,
[POLYGON_CHAIN_ID]: true,
[RINKEBY_CHAIN_ID]: true,
[AVALANCHE_CHAIN_ID]: true,
};
export const ALLOWED_PROD_SWAPS_CHAIN_IDS = [
MAINNET_CHAIN_ID,
SWAPS_TESTNET_CHAIN_ID,
BSC_CHAIN_ID,
POLYGON_CHAIN_ID,
AVALANCHE_CHAIN_ID,
];
export const ALLOWED_DEV_SWAPS_CHAIN_IDS = [
...ALLOWED_PROD_SWAPS_CHAIN_IDS,
RINKEBY_CHAIN_ID,
];
export const ALLOWED_SMART_TRANSACTIONS_CHAIN_IDS = [
MAINNET_CHAIN_ID,
@ -197,3 +201,8 @@ export const RINKEBY = 'rinkeby';
export const AVALANCHE = 'avalanche';
export const SWAPS_CLIENT_ID = 'extension';
export const TOKEN_BUCKET_PRIORITY = {
OWNED: 'owned',
TOP: 'top',
};

@ -1,4 +1,10 @@
import { MAX_SAFE_CHAIN_ID } from '../constants/network';
import {
MAX_SAFE_CHAIN_ID,
BSC_CHAIN_ID,
POLYGON_CHAIN_ID,
AVALANCHE_CHAIN_ID,
MAINNET_CHAIN_ID,
} from '../constants/network';
/**
* Checks whether the given number primitive chain ID is safe.
@ -28,3 +34,21 @@ export function isPrefixedFormattedHexString(value) {
}
return /^0x[1-9a-f]+[0-9a-f]*$/iu.test(value);
}
/**
* Check if token detection is enabled for certain networks
*
* @param chainId - ChainID of network
* @returns Whether the current network supports token detection
*/
export function isTokenDetectionEnabledForNetwork(chainId) {
switch (chainId) {
case MAINNET_CHAIN_ID:
case BSC_CHAIN_ID:
case POLYGON_CHAIN_ID:
case AVALANCHE_CHAIN_ID:
return true;
default:
return false;
}
}

@ -46,6 +46,26 @@ export const UI_NOTIFICATIONS = {
width: '80%',
},
},
10: {
id: 10,
date: '2022-04-18',
image: {
src: 'images/token-detection.svg',
width: '100%',
},
},
11: {
id: 11,
date: '2022-04-18',
},
12: {
id: 12,
date: '2022-05-18',
image: {
src: 'images/darkmode-banner.png',
width: '100%',
},
},
};
export const getTranslatedUINoficiations = (t, locale) => {
@ -132,5 +152,35 @@ export const getTranslatedUINoficiations = (t, locale) => {
new Date(UI_NOTIFICATIONS[9].date),
),
},
10: {
...UI_NOTIFICATIONS[10],
title: t('notifications10Title'),
description: [
t('notifications10DescriptionOne'),
t('notifications10DescriptionTwo'),
t('notifications10DescriptionThree'),
],
actionText: t('notifications10ActionText'),
date: new Intl.DateTimeFormat(formattedLocale).format(
new Date(UI_NOTIFICATIONS[10].date),
),
},
11: {
...UI_NOTIFICATIONS[11],
title: t('notifications11Title'),
description: t('notifications11Description'),
date: new Intl.DateTimeFormat(formattedLocale).format(
new Date(UI_NOTIFICATIONS[11].date),
),
},
12: {
...UI_NOTIFICATIONS[12],
title: t('notifications12Title'),
description: t('notifications12Description'),
actionText: t('notifications12ActionText'),
date: new Intl.DateTimeFormat(formattedLocale).format(
new Date(UI_NOTIFICATIONS[12].date),
),
},
};
};

@ -1,73 +0,0 @@
{
"gasPricesBasic": {
"average": 85,
"fast": 200,
"safeLow": 80
},
"metametrics": {
"mockMetaMetricsResponse": true
},
"swaps": {
"featureFlags": {
"bsc": {
"mobile_active": false,
"extension_active": true,
"fallback_to_v1": true
},
"ethereum": {
"mobile_active": false,
"extension_active": true,
"fallback_to_v1": true
},
"polygon": {
"mobile_active": false,
"extension_active": true,
"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
}
}
}

@ -66,6 +66,9 @@
},
"8": {
"isShown": true
},
"12": {
"isShown": true
}
}
},

@ -56,6 +56,9 @@
},
"8": {
"isShown": true
},
"12": {
"isShown": true
}
}
},

@ -52,6 +52,9 @@
},
"8": {
"isShown": true
},
"12": {
"isShown": true
}
}
},

@ -70,6 +70,9 @@
"notifications": {
"8": {
"isShown": true
},
"12": {
"isShown": true
}
}
},
@ -123,10 +126,6 @@
"usePhishDetect": true,
"useStaticTokenList": false
},
"TokenListController": {
"tokenList": {},
"tokensChainsCache": {}
},
"TokensController": {
"allTokens": {
"0x539": {

@ -53,6 +53,9 @@
},
"8": {
"isShown": true
},
"12": {
"isShown": true
}
}
},

@ -53,6 +53,9 @@
},
"8": {
"isShown": true
},
"12": {
"isShown": true
}
}
},

@ -107,6 +107,9 @@
},
"8": {
"isShown": true
},
"12": {
"isShown": true
}
}
},

@ -0,0 +1,21 @@
{
"address": "0961ca10d49b9b8e371aa0bcf77fe5730b18f2e4",
"crypto": {
"ciphertext": "eb10547515f1bf3bb6eefed3cb39303c64a30560f378b789592b1ac632bbc14c",
"cipherparams": {
"iv": "25ebfc7358ce77462ef7b970e91309d6"
},
"cipher": "aes-128-ctr",
"kdf": "scrypt",
"kdfparams": {
"dklen": 32,
"salt": "42417a49fb6cbb10167d7bc4ed8ae71d02cb4ee6309a42417e0051e7472d45c5",
"n": 8192,
"r": 8,
"p": 1
},
"mac": "0491462fbca0c7a71d249de141736304d699749c2faf5ab575d6a502e26099d7"
},
"id": "03754e23-770b-43d3-a35e-bae1196e6f86",
"version": 3
}

@ -52,6 +52,9 @@
},
"8": {
"isShown": true
},
"12": {
"isShown": true
}
}
},
@ -91,361 +94,6 @@
}
]
},
"TokenListController": {
"tokenList": {
"0xbbbbca6a901c926f240b89eacb641d8aec7aeafd": {
"address": "0xbbbbca6a901c926f240b89eacb641d8aec7aeafd",
"symbol": "LRC",
"decimals": 18,
"name": "Loopring",
"iconUrl": "",
"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": "",
"aggregators": [
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
"0x6b3595068778dd592e39a122f4f5a5cf09c90fe2": {
"address": "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2",
"symbol": "SUSHI",
"decimals": 18,
"name": "SushiSwap",
"iconUrl": "",
"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": "",
"aggregators": [
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
"0xc00e94cb662c3520282e6f5717214004a7f26888": {
"address": "0xc00e94cb662c3520282e6f5717214004a7f26888",
"symbol": "COMP",
"decimals": 18,
"name": "Compound",
"iconUrl": "",
"aggregators": [
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
"0xba100000625a3754423978a60c9317c58a424e3d": {
"address": "0xba100000625a3754423978a60c9317c58a424e3d",
"symbol": "BAL",
"decimals": 18,
"name": "Balancer",
"iconUrl": "",
"aggregators": [
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
"0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0": {
"address": "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0",
"symbol": "MATIC",
"decimals": 18,
"name": "Polygon",
"iconUrl": "",
"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": "",
"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": "",
"aggregators": [
"airswapLight",
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 12
},
{
"address": "0x04fa0d235c4abf4bcf4787af4cf447de572ef828",
"symbol": "UMA",
"decimals": 18,
"name": "UMA",
"iconUrl": "",
"aggregators": [
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
{
"address": "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2",
"symbol": "SUSHI",
"decimals": 18,
"name": "SushiSwap",
"iconUrl": "",
"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": "",
"aggregators": [
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
{
"address": "0xc00e94cb662c3520282e6f5717214004a7f26888",
"symbol": "COMP",
"decimals": 18,
"name": "Compound",
"iconUrl": "",
"aggregators": [
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
{
"address": "0xba100000625a3754423978a60c9317c58a424e3d",
"symbol": "BAL",
"decimals": 18,
"name": "Balancer",
"iconUrl": "",
"aggregators": [
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
{
"address": "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0",
"symbol": "MATIC",
"decimals": 18,
"name": "Polygon",
"iconUrl": "",
"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": "",
"aggregators": [
"airswapLight",
"bancor",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
}
]
},
"3": {
"timestamp": 1628769543620
},
"1337": {
"timestamp": 1628769513476
}
}
},
"PreferencesController": {
"accountTokens": {
"0x5cfe73b6021e818b776b421b1c4db2474086a7e1": {

@ -52,6 +52,9 @@
},
"8": {
"isShown": true
},
"12": {
"isShown": true
}
}
},

@ -56,6 +56,9 @@
},
"8": {
"isShown": true
},
"12": {
"isShown": true
}
}
},

@ -52,6 +52,9 @@
},
"8": {
"isShown": true
},
"12": {
"isShown": true
}
}
},
@ -91,361 +94,6 @@
}
]
},
"TokenListController": {
"tokenList": {
"0xbbbbca6a901c926f240b89eacb641d8aec7aeafd": {
"address": "0xbbbbca6a901c926f240b89eacb641d8aec7aeafd",
"symbol": "LRC",
"decimals": 18,
"name": "Loopring",
"iconUrl": "",
"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": "",
"aggregators": [
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
"0x6b3595068778dd592e39a122f4f5a5cf09c90fe2": {
"address": "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2",
"symbol": "SUSHI",
"decimals": 18,
"name": "SushiSwap",
"iconUrl": "",
"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": "",
"aggregators": [
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
"0xc00e94cb662c3520282e6f5717214004a7f26888": {
"address": "0xc00e94cb662c3520282e6f5717214004a7f26888",
"symbol": "COMP",
"decimals": 18,
"name": "Compound",
"iconUrl": "",
"aggregators": [
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
"0xba100000625a3754423978a60c9317c58a424e3d": {
"address": "0xba100000625a3754423978a60c9317c58a424e3d",
"symbol": "BAL",
"decimals": 18,
"name": "Balancer",
"iconUrl": "",
"aggregators": [
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
"0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0": {
"address": "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0",
"symbol": "MATIC",
"decimals": 18,
"name": "Polygon",
"iconUrl": "",
"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": "",
"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": "",
"aggregators": [
"airswapLight",
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 12
},
{
"address": "0x04fa0d235c4abf4bcf4787af4cf447de572ef828",
"symbol": "UMA",
"decimals": 18,
"name": "UMA",
"iconUrl": "",
"aggregators": [
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
{
"address": "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2",
"symbol": "SUSHI",
"decimals": 18,
"name": "SushiSwap",
"iconUrl": "",
"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": "",
"aggregators": [
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
{
"address": "0xc00e94cb662c3520282e6f5717214004a7f26888",
"symbol": "COMP",
"decimals": 18,
"name": "Compound",
"iconUrl": "",
"aggregators": [
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
{
"address": "0xba100000625a3754423978a60c9317c58a424e3d",
"symbol": "BAL",
"decimals": 18,
"name": "Balancer",
"iconUrl": "",
"aggregators": [
"bancor",
"cmc",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
},
{
"address": "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0",
"symbol": "MATIC",
"decimals": 18,
"name": "Polygon",
"iconUrl": "",
"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": "",
"aggregators": [
"airswapLight",
"bancor",
"coinGecko",
"kleros",
"oneInch",
"paraswap",
"pmm",
"totle",
"zapper",
"zerion",
"zeroEx"
],
"occurrences": 11
}
]
},
"3": {
"timestamp": 1628769543620
},
"1337": {
"timestamp": 1628769513476
}
}
},
"PreferencesController": {
"accountTokens": {
"0x5cfe73b6021e818b776b421b1c4db2474086a7e1": {

@ -26,6 +26,13 @@
},
"network": "1337"
},
"NotificationController": {
"notifications": {
"12": {
"isShown": true
}
}
},
"CurrencyController": {
"conversionDate": 1617927806.941,
"conversionRate": 2084.64,

@ -53,6 +53,9 @@
},
"8": {
"isShown": true
},
"12": {
"isShown": true
}
}
},

@ -53,6 +53,9 @@
},
"8": {
"isShown": true
},
"12": {
"isShown": true
}
}
},

@ -63,6 +63,9 @@
},
"8": {
"isShown": true
},
"12": {
"isShown": true
}
}
},

@ -1,5 +1,29 @@
const blacklistedHosts = [
'goerli.infura.io',
'kovan.infura.io',
'mainnet.infura.io',
'rinkeby.infura.io',
'ropsten.infura.io',
];
async function setupMocking(server, testSpecificMock) {
await server.forAnyRequest().thenPassThrough();
await server.forAnyRequest().thenPassThrough({
beforeRequest: (req) => {
const { host } = req.headers;
if (blacklistedHosts.includes(host)) {
return {
url: 'http://localhost:8545',
};
}
return {};
},
});
await server.forPost('https://api.segment.io/v1/batch').thenCallback(() => {
return {
statusCode: 200,
};
});
await server
.forGet('https://gas-api.metaswap.codefi.network/networks/1/gasPrices')
@ -14,12 +38,6 @@ async function setupMocking(server, testSpecificMock) {
};
});
await server.forPost('https://api.segment.io/v1/batch').thenCallback(() => {
return {
statusCode: 200,
};
});
await server
.forGet(
'https://gas-api.metaswap.codefi.network/networks/1/suggestedGasFees',
@ -57,6 +75,85 @@ async function setupMocking(server, testSpecificMock) {
};
});
await server
.forGet('https://swap.metaswap.codefi.network/featureFlags')
.thenCallback(() => {
return {
statusCode: 200,
json: [
{
ethereum: {
mobile_active: true,
extension_active: true,
fallback_to_v1: false,
mobileActive: true,
extensionActive: true,
},
bsc: {
mobile_active: true,
extension_active: true,
fallback_to_v1: false,
mobileActive: true,
extensionActive: true,
},
polygon: {
mobile_active: true,
extension_active: true,
fallback_to_v1: false,
mobileActive: true,
extensionActive: true,
},
avalanche: {
mobile_active: true,
extension_active: true,
fallback_to_v1: false,
mobileActive: true,
extensionActive: true,
},
smart_transactions: {
mobile_active: false,
extension_active: false,
},
smartTransactions: {
mobileActive: false,
extensionActive: false,
},
updated_at: '2022-03-17T15:54:00.360Z',
},
],
};
});
await server
.forGet('https://token-api.metaswap.codefi.network/tokens/1337')
.thenCallback(() => {
return {
statusCode: 200,
json: [
{
address: '0x0d8775f648430679a709e98d2b0cb6250d2887ef',
symbol: 'BAT',
decimals: 18,
name: 'Basic Attention Token',
iconUrl:
'https://assets.coingecko.com/coins/images/677/thumb/basic-attention-token.png?1547034427',
aggregators: [
'aave',
'bancor',
'coinGecko',
'oneInch',
'paraswap',
'pmm',
'zapper',
'zerion',
'zeroEx',
],
occurrences: 9,
},
],
};
});
testSpecificMock(server);
}

@ -0,0 +1,114 @@
const { strict: assert } = require('assert');
const { convertToHexValue, withFixtures } = require('../helpers');
describe('Chain Interactions', function () {
it('should add the XDAI chain and not switch the network', async function () {
const ganacheOptions = {
accounts: [
{
secretKey:
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC',
balance: convertToHexValue(25000000000000000000),
},
],
};
await withFixtures(
{
dapp: true,
fixtures: 'connected-state',
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);
// trigger add chain confirmation
await driver.openNewPage('http://127.0.0.1:8080/');
await driver.clickElement('#addEthereumChain');
await driver.waitUntilXWindowHandles(3);
const windowHandles = await driver.getAllWindowHandles();
const extension = windowHandles[0];
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
);
// verify chain details
const [networkName, networkUrl, chainId] = await driver.findElements(
'.definition-list dd',
);
assert.equal(await networkName.getText(), 'xDAI Chain');
assert.equal(await networkUrl.getText(), 'https://dai.poa.network');
assert.equal(await chainId.getText(), '100');
// approve add chain, cancel switch chain
await driver.clickElement({ text: 'Approve', tag: 'button' });
await driver.clickElement({ text: 'Cancel', tag: 'button' });
// switch to extension
await driver.waitUntilXWindowHandles(2);
await driver.switchToWindow(extension);
// verify networks
const networkDisplay = await driver.findElement('.network-display');
await networkDisplay.click();
assert.equal(await networkDisplay.getText(), 'Localhost 8545');
const xDaiChain = await driver.findElements({
text: 'xDAI Chain',
tag: 'span',
});
assert.ok(xDaiChain.length, 1);
},
);
});
it('should add the XDAI chain and switch the network', async function () {
const ganacheOptions = {
accounts: [
{
secretKey:
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC',
balance: convertToHexValue(25000000000000000000),
},
],
};
await withFixtures(
{
dapp: true,
fixtures: 'connected-state',
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);
// trigger add chain confirmation
await driver.openNewPage('http://127.0.0.1:8080/');
await driver.clickElement('#addEthereumChain');
await driver.waitUntilXWindowHandles(3);
const windowHandles = await driver.getAllWindowHandles();
const extension = windowHandles[0];
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
);
// approve and switch chain
await driver.clickElement({ text: 'Approve', tag: 'button' });
await driver.clickElement({ text: 'Switch network', tag: 'button' });
// switch to extension
await driver.waitUntilXWindowHandles(2);
await driver.switchToWindow(extension);
// verify current network
const networkDisplay = await driver.findElement('.network-display');
assert.equal(await networkDisplay.getText(), 'xDAI Chain');
},
);
});
});

@ -1,4 +1,5 @@
const { strict: assert } = require('assert');
const path = require('path');
const {
convertToHexValue,
withFixtures,
@ -232,6 +233,78 @@ describe('Metamask Import UI', function () {
);
});
it('Import Account using json file', async function () {
const ganacheOptions = {
accounts: [
{
secretKey:
'0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9',
balance: convertToHexValue(25000000000000000000),
},
],
};
await withFixtures(
{
fixtures: 'import-ui',
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);
// Imports an account with JSON file
await driver.clickElement('.account-menu__icon');
await driver.clickElement({ text: 'Import Account', tag: 'div' });
await driver.clickElement('.new-account-import-form__select');
await driver.clickElement({ text: 'JSON File', tag: 'option' });
const fileInput = await driver.findElement('input[type="file"]');
const importJsonFile = path.join(
__dirname,
'..',
'fixtures',
'import-utc-json',
'test-json-import-account-file.json',
);
fileInput.sendKeys(importJsonFile);
await driver.fill('#json-password-box', 'foobarbazqux');
await driver.clickElement({ text: 'Import', tag: 'button' });
// should show the correct account name
const importedAccountName = await driver.findElement(
'.selected-account__name',
);
assert.equal(await importedAccountName.getText(), 'Account 4');
// should show the imported label
await driver.clickElement('.account-menu__icon');
// confirm 4th account is account 4, as expected
const accountMenuItemSelector = '.account-menu__account:nth-child(4)';
const fourthAccountName = await driver.findElement(
`${accountMenuItemSelector} .account-menu__name`,
);
assert.equal(await fourthAccountName.getText(), 'Account 4');
// confirm label is present on the same menu item
const importedLabel = await driver.findElement(
`${accountMenuItemSelector} .keyring-label`,
);
assert.equal(await importedLabel.getText(), 'IMPORTED');
const accountListItems = await driver.findElements(
'.account-menu__account',
);
assert.equal(accountListItems.length, 4);
},
);
});
it('Import Account using private key of an already active account should result in an error', async function () {
const testPrivateKey =
'0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9';

@ -1,4 +1,3 @@
/* eslint-disable mocha/no-skipped-tests */
const { strict: assert } = require('assert');
const { convertToHexValue, withFixtures } = require('../helpers');
@ -30,7 +29,7 @@ describe('Phishing Detection', function () {
},
],
};
it.skip('should display the MetaMask Phishing Detection page', async function () {
it('should display the MetaMask Phishing Detection page', async function () {
await withFixtures(
{
fixtures: 'imported-account',

@ -0,0 +1,273 @@
const { strict: assert } = require('assert');
const { convertToHexValue, withFixtures } = require('../helpers');
describe('Settings Search', function () {
const ganacheOptions = {
accounts: [
{
secretKey:
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC',
balance: convertToHexValue(25000000000000000000),
},
],
};
const settingsSearch = {
general: 'Primary Currency',
advanced: 'State Logs',
contacts: 'Contacts',
security: 'Reveal Secret',
alerts: 'Browsing a website',
networks: 'Ethereum Mainnet',
experimental: 'Token Detection',
about: 'Terms of Use',
};
it('should find element inside the General tab', async function () {
await withFixtures(
{
dapp: true,
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('.account-menu__icon');
await driver.clickElement({ text: 'Settings', tag: 'div' });
await driver.fill('#search-settings', settingsSearch.general);
const page = 'General';
await driver.clickElement({ text: page, tag: 'span' });
assert.equal(
await driver.isElementPresent({ text: page, tag: 'div' }),
true,
`${settingsSearch.general} item does not redirect to ${page} view`,
);
},
);
});
it('should find element inside the Advanced tab', async function () {
await withFixtures(
{
dapp: true,
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('.account-menu__icon');
await driver.clickElement({ text: 'Settings', tag: 'div' });
await driver.fill('#search-settings', settingsSearch.advanced);
// Check if element redirects to the correct page
const page = 'Advanced';
await driver.clickElement({ text: page, tag: 'span' });
assert.equal(
await driver.isElementPresent({ text: page, tag: 'div' }),
true,
`${settingsSearch.advanced} item does not redirect to ${page} view`,
);
},
);
});
it('should find element inside the Contacts tab', async function () {
await withFixtures(
{
dapp: true,
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('.account-menu__icon');
await driver.clickElement({ text: 'Settings', tag: 'div' });
await driver.fill('#search-settings', settingsSearch.contacts);
// Check if element redirects to the correct page
const page = 'Contacts';
await driver.clickElement({ text: page, tag: 'span' });
assert.equal(
await driver.isElementPresent({ text: page, tag: 'div' }),
true,
`${settingsSearch.contacts} item does not redirect to ${page} view`,
);
},
);
});
it('should find element inside the Security tab', async function () {
await withFixtures(
{
dapp: true,
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('.account-menu__icon');
await driver.clickElement({ text: 'Settings', tag: 'div' });
await driver.fill('#search-settings', settingsSearch.security);
// Check if element redirects to the correct page
const page = 'Security';
await driver.clickElement({ text: page, tag: 'span' });
assert.equal(
await driver.isElementPresent({ text: page, tag: 'div' }),
true,
`${settingsSearch.security} item does not redirect to ${page} view`,
);
},
);
});
it('should find element inside the Alerts tab', async function () {
await withFixtures(
{
dapp: true,
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('.account-menu__icon');
await driver.clickElement({ text: 'Settings', tag: 'div' });
await driver.fill('#search-settings', settingsSearch.alerts);
// Check if element redirects to the correct page
const page = 'Alerts';
await driver.clickElement({ text: page, tag: 'span' });
assert.equal(
await driver.isElementPresent({ text: page, tag: 'div' }),
true,
`${settingsSearch.alerts} item does not redirect to ${page} view`,
);
},
);
});
it('should find element inside the Networks tab', async function () {
await withFixtures(
{
dapp: true,
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('.account-menu__icon');
await driver.clickElement({ text: 'Settings', tag: 'div' });
await driver.fill('#search-settings', settingsSearch.networks);
// Check if element redirects to the correct page
const page = 'Networks';
await driver.clickElement({ text: page, tag: 'span' });
assert.equal(
await driver.isElementPresent({ text: page, tag: 'div' }),
true,
`${settingsSearch.networks} item does not redirect to ${page} view`,
);
},
);
});
it('should find element inside the Experimental tab', async function () {
await withFixtures(
{
dapp: true,
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('.account-menu__icon');
await driver.clickElement({ text: 'Settings', tag: 'div' });
await driver.fill('#search-settings', settingsSearch.experimental);
// Check if element redirects to the correct page
const page = 'Experimental';
await driver.clickElement({ text: page, tag: 'span' });
assert.equal(
await driver.isElementPresent({ text: page, tag: 'div' }),
true,
`${settingsSearch.experimental} item not redirect to ${page} view`,
);
},
);
});
it('should find element inside the About tab', async function () {
await withFixtures(
{
dapp: true,
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('.account-menu__icon');
await driver.clickElement({ text: 'Settings', tag: 'div' });
await driver.fill('#search-settings', settingsSearch.about);
// Check if element redirects to the correct page
const page = 'About';
await driver.clickElement({ text: page, tag: 'span' });
assert.equal(
await driver.isElementPresent({ text: page, tag: 'div' }),
true,
`${settingsSearch.about} item does not redirect to ${page} view`,
);
},
);
});
it('should display "Element not found" for a non-existing element', async function () {
await withFixtures(
{
dapp: true,
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('.account-menu__icon');
await driver.clickElement({ text: 'Settings', tag: 'div' });
await driver.fill('#search-settings', 'Lorem ipsum');
const found = await driver.isElementPresent({
text: 'No matching results found',
tag: 'span',
});
assert.equal(found, true, 'Non existent element was found');
},
);
});
});

@ -0,0 +1,94 @@
const { strict: assert } = require('assert');
const { withFixtures } = require('../helpers');
describe('Swap Eth for another Token', function () {
const ganacheOptions = {
accounts: [
{
secretKey:
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC',
balance: 25000000000000000000,
},
],
};
it('Completes a Swap between Eth and Matic', async function () {
await withFixtures(
{
fixtures: 'imported-account',
ganacheOptions,
title: this.test.title,
failOnConsoleError: false,
},
async ({ driver }) => {
await driver.navigate();
await driver.fill('#password', 'correct horse battery staple');
await driver.press('#password', driver.Key.ENTER);
await driver.clickElement(
'.wallet-overview__buttons .icon-button:nth-child(3)',
);
await driver.clickElement(
'[class*="dropdown-search-list"] + div[class*="MuiFormControl-root MuiTextField-root"]',
);
await driver.fill('input[placeholder*="0"]', '2');
await driver.clickElement(
'[class*="dropdown-search-list"] + div[class*="MuiFormControl-root MuiTextField-root"]',
);
await driver.clickElement(
'[class="dropdown-search-list__closed-primary-label dropdown-search-list__select-default"]',
);
await driver.clickElement('[placeholder="Search for a token"]');
await driver.clickElement('[placeholder="Search for a token"]');
await driver.fill('[placeholder="Search for a token"]', 'DAI');
await driver.waitForSelector(
'[class="searchable-item-list__primary-label"]',
);
await driver.clickElement(
'[class="searchable-item-list__primary-label"]',
);
await driver.clickElement({ text: 'Review Swap', tag: 'button' });
await driver.waitForSelector('[class*="box--align-items-center"]');
const estimatedEth = await driver.waitForSelector({
css: '[class*="box--align-items-center"]',
text: 'Estimated gas fee',
});
assert.equal(await estimatedEth.getText(), 'Estimated gas fee');
await driver.waitForSelector(
'[class="exchange-rate-display main-quote-summary__exchange-rate-display"]',
);
await driver.waitForSelector(
'[class="fee-card__info-tooltip-container"]',
);
await driver.waitForSelector({
css: '[class="countdown-timer__time"]',
text: '0:24',
});
await driver.clickElement({ text: 'Swap', tag: 'button' });
const sucessfulTransactionMessage = await driver.waitForSelector({
css: '[class="awaiting-swap__header"]',
text: 'Transaction complete',
});
assert.equal(
await sucessfulTransactionMessage.getText(),
'Transaction complete',
);
const sucessfulTransactionToken = await driver.waitForSelector({
css: '[class="awaiting-swap__amount-and-symbol"]',
text: 'DAI',
});
assert.equal(await sucessfulTransactionToken.getText(), 'DAI');
await driver.clickElement({ text: 'Close', tag: 'button' });
await driver.clickElement('[data-testid="home__activity-tab"]');
const swaptotal = await driver.waitForSelector({
css: '[class="transaction-list-item__primary-currency"]',
text: '-2 TESTETH',
});
assert.equal(await swaptotal.getText(), '-2 TESTETH');
const swaptotaltext = await driver.waitForSelector({
css: '[class="list-item__title"]',
text: 'Swap TESTETH to DAI',
});
assert.equal(await swaptotaltext.getText(), 'Swap TESTETH to DAI');
},
);
});
});

@ -1,5 +1,4 @@
const { Browser } = require('selenium-webdriver');
const fetchMockResponses = require('../../data/fetch-mocks.json');
const Driver = require('./driver');
const ChromeDriver = require('./chrome');
const FirefoxDriver = require('./firefox');
@ -12,7 +11,6 @@ async function buildWebDriver({ responsive, port, type } = {}) {
extensionId,
extensionUrl,
} = await buildBrowserWebDriver(browser, { responsive, port, type });
await setupFetchMocking(seleniumDriver);
const driver = new Driver(seleniumDriver, browser, extensionUrl);
return {
@ -35,53 +33,6 @@ async function buildBrowserWebDriver(browser, webDriverOptions) {
}
}
async function setupFetchMocking(driver) {
// define fetchMocking script, to be evaluated in the browser
function fetchMocking(mockResponses) {
window.origFetch = window.fetch.bind(window);
window.fetch = async (...args) => {
const url = args[0];
// api.metaswap.codefi.network/gasPrices
if (
url.match(/^http(s)?:\/\/api\.metaswap\.codefi\.network\/gasPrices/u)
) {
return { json: async () => clone(mockResponses.gasPricesBasic) };
} else if (url.match(/chromeextensionmm/u)) {
return { json: async () => clone(mockResponses.metametrics) };
} else if (url.match(/^https:\/\/(swap\.metaswap\.codefi\.network)/u)) {
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);
};
if (window.chrome && window.chrome.webRequest) {
window.chrome.webRequest.onBeforeRequest.addListener(
cancelInfuraRequest,
{ urls: ['https://*.infura.io/*'] },
['blocking'],
);
}
function cancelInfuraRequest(requestDetails) {
console.log(`fetchMocking - Canceling request: "${requestDetails.url}"`);
return { cancel: true };
}
function clone(obj) {
return JSON.parse(JSON.stringify(obj));
}
}
// fetchMockResponses are parsed last minute to ensure that objects are uniquely instantiated
const fetchMockResponsesJson = JSON.stringify(fetchMockResponses);
// eval the fetchMocking script in the browser
await driver.executeScript(`(${fetchMocking})(${fetchMockResponsesJson})`);
}
module.exports = {
buildWebDriver,
};

@ -40,6 +40,7 @@
@import 'gas-details-item/index';
@import 'gas-details-item/gas-details-item-title/index';
@import 'gas-timing/index';
@import 'hold-to-reveal-button/index';
@import 'home-notification/index';
@import 'info-box/index';
@import 'menu-bar/index';
@ -84,4 +85,8 @@
@import 'advanced-gas-fee-popover/advanced-gas-fee-input-subtext/index';
@import 'advanced-gas-fee-popover/advanced-gas-fee-defaults/index';
@import 'currency-input/index';
@import 'asset-list/detetcted-tokens-link/index';
@import 'detected-token/detected-token-address/index';
@import 'detected-token/detected-token-aggregators/index';
@import 'detected-token/detected-token-values/index';
@import 'detected-token/detected-token-details/index'

@ -5,27 +5,6 @@ export default {
title: 'Components/App/AppHeader',
id: __filename,
argTypes: {
history: {
control: 'object',
},
networkDropdownOpen: {
control: 'boolean',
},
showNetworkDropdown: {
action: 'showNetworkDropdown',
},
hideNetworkDropdown: {
action: 'hideNetworkDropdown',
},
toggleAccountMenu: {
action: 'toggleAccountMenu',
},
selectedAddress: {
control: 'text',
},
isUnlocked: {
control: 'boolean',
},
hideNetworkIndicator: {
control: 'boolean',
},
@ -35,9 +14,6 @@ export default {
disableNetworkIndicator: {
control: 'boolean',
},
isAccountMenuOpen: {
control: 'boolean',
},
onClick: {
action: 'onClick',
},

@ -14,7 +14,7 @@ import { SEND_ROUTE } from '../../../helpers/constants/routes';
import { SEVERITIES } from '../../../helpers/constants/design-system';
import { INVALID_ASSET_TYPE } from '../../../helpers/constants/error-keys';
import { ASSET_TYPES } from '../../../../shared/constants/transaction';
import { MetaMetricsContext } from '../../../contexts/metametrics.new';
import { MetaMetricsContext } from '../../../contexts/metametrics';
const AssetListItem = ({
className,

@ -25,7 +25,7 @@ import {
JUSTIFY_CONTENT,
} from '../../../helpers/constants/design-system';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { MetaMetricsContext } from '../../../contexts/metametrics.new';
import { MetaMetricsContext } from '../../../contexts/metametrics';
const AssetList = ({ onClickAsset }) => {
const t = useI18nContext();

@ -0,0 +1,36 @@
import React from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Box from '../../../ui/box/box';
import Button from '../../../ui/button';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import { getDetectedTokensInCurrentNetwork } from '../../../../selectors';
const DetectedTokensLink = ({ className = '', onClick }) => {
const t = useI18nContext();
const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork);
return (
<Box
className={classNames('detected-tokens-link', className)}
marginTop={1}
>
<Button
type="link"
className="detected-tokens-link__link"
onClick={onClick}
>
{t('numberOfNewTokensDetected', [detectedTokens.length])}
</Button>
</Box>
);
};
DetectedTokensLink.propTypes = {
onClick: PropTypes.func.isRequired,
className: PropTypes.string,
};
export default DetectedTokensLink;

@ -0,0 +1,5 @@
.detected-tokens-link {
& &__link {
@include H6;
}
}

@ -20,22 +20,28 @@ export default function CollectiblesDetectionNotice() {
const history = useHistory();
return (
<Box marginBottom={4} className="collectibles-detection-notice">
<Box className="collectibles-detection-notice">
<Dialog type="message" className="collectibles-detection-notice__message">
<button
onClick={() => setCollectiblesDetectionNoticeDismissed()}
className="collectibles-detection-notice__message__close-button"
className="fas fa-times collectibles-detection-notice__message__close-button"
data-testid="collectibles-detection-notice-close"
/>
<Box display={DISPLAY.FLEX}>
<Box paddingTop={2}>
<i style={{ fontSize: '1rem' }} className="fa fa-info-circle" />
<Box paddingTop={1}>
<i
style={{
fontSize: '1rem',
color: 'var(--color-primary-default)',
}}
className="fa fa-info-circle"
/>
</Box>
<Box paddingLeft={4}>
<Box paddingLeft={2}>
<Typography
color={COLORS.TEXT_DEFAULT}
align={TEXT_ALIGN.LEFT}
variant={TYPOGRAPHY.H6}
variant={TYPOGRAPHY.H7}
fontWeight={FONT_WEIGHT.BOLD}
>
{t('newNFTsDetected')}
@ -43,10 +49,10 @@ export default function CollectiblesDetectionNotice() {
<Typography
color={COLORS.TEXT_DEFAULT}
align={TEXT_ALIGN.LEFT}
variant={TYPOGRAPHY.H6}
variant={TYPOGRAPHY.H7}
boxProps={{ marginBottom: 4 }}
>
{t('newNFTsDetectedInfo')}
{t('newNFTDetectedMessage')}
</Typography>
<Button
type="link"

@ -1,26 +1,20 @@
.collectibles-detection-notice {
margin: 16px 16px 0 16px;
&__message {
position: relative;
padding: 0 1rem 1rem 1rem !important;
&__close-button {
background-color: transparent;
padding: 0.75rem 0.75rem 1rem 0.75rem !important;
&::after {
position: absolute;
content: '\00D7';
font-size: 29px;
font-weight: 200;
& &__close-button {
color: var(--color-icon-default);
background-color: transparent;
top: 0;
right: 12px;
background: none;
position: absolute;
cursor: pointer;
}
right: 8px;
}
a.collectibles-detection-notice__message__link {
@include H6;
@include H7;
width: 100%;
padding: 0;

@ -58,7 +58,7 @@ export default function CollectiblesTab({ onAddNFT }) {
}
return (
<div className="collectibles-tab">
<Box className="collectibles-tab">
{Object.keys(collections).length > 0 ||
previouslyOwnedCollection.collectibles.length > 0 ? (
<CollectiblesItems
@ -66,12 +66,13 @@ export default function CollectiblesTab({ onAddNFT }) {
previouslyOwnedCollection={previouslyOwnedCollection}
/>
) : (
<Box padding={[6, 12, 6, 12]}>
<>
{isMainnet &&
!useCollectibleDetection &&
!collectibleDetectionNoticeDismissed ? (
<CollectiblesDetectionNotice />
) : null}
<Box padding={12}>
<Box justifyContent={JUSTIFY_CONTENT.CENTER}>
<img src="./images/no-nfts.svg" />
</Box>
@ -100,6 +101,7 @@ export default function CollectiblesTab({ onAddNFT }) {
</Button>
</Box>
</Box>
</>
)}
<Box
marginBottom={4}
@ -152,7 +154,7 @@ export default function CollectiblesTab({ onAddNFT }) {
</Box>
</Box>
</Box>
</div>
</Box>
);
}

@ -1,7 +1,7 @@
import React, { useCallback, useContext, useState } from 'react';
import PropTypes from 'prop-types';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { MetaMetricsContext } from '../../../contexts/metametrics.new';
import { MetaMetricsContext } from '../../../contexts/metametrics';
import TextField from '../../ui/text-field';
import Button from '../../ui/button';
import CheckBox from '../../ui/check-box';
@ -108,7 +108,7 @@ export default function CreateNewVault({
return (
<form className="create-new-vault__form" onSubmit={onImport}>
<SrpInput onChange={setSeedPhrase} />
<SrpInput onChange={setSeedPhrase} srpText={t('secretRecoveryPhrase')} />
<div className="create-new-vault__create-password">
<TextField
id="password"

@ -0,0 +1,50 @@
import React, { useContext, useState } from 'react';
import PropTypes from 'prop-types';
import { I18nContext } from '../../../../contexts/i18n';
import Box from '../../../ui/box';
import Button from '../../../ui/button';
import Typography from '../../../ui/typography/typography';
import {
DISPLAY,
FONT_WEIGHT,
TYPOGRAPHY,
} from '../../../../helpers/constants/design-system';
const DetectedTokenAggregators = ({ aggregatorsList }) => {
const t = useContext(I18nContext);
const numOfHiddenAggregators = parseInt(aggregatorsList.length, 10) - 2;
const [displayMore, setDisplayMore] = useState(false);
return (
<Box display={DISPLAY.INLINE_FLEX} className="detected-token-aggregators">
<Typography variant={TYPOGRAPHY.H7} fontWeight={FONT_WEIGHT.NORMAL}>
{t('fromTokenLists', [
numOfHiddenAggregators > 0 && !displayMore ? (
<Typography variant={TYPOGRAPHY.H7} fontWeight={FONT_WEIGHT.NORMAL}>
{`${aggregatorsList.slice(0, 2).join(', ')}`}
<Button
type="link"
className="detected-token-aggregators__link"
onClick={() => setDisplayMore(true)}
key="detected-token-aggrgators-link"
>
{t('plusXMore', [numOfHiddenAggregators])}
</Button>
</Typography>
) : (
<Typography variant={TYPOGRAPHY.H7} fontWeight={FONT_WEIGHT.NORMAL}>
{`${aggregatorsList.join(', ')}.`}
</Typography>
),
])}
</Typography>
</Box>
);
};
DetectedTokenAggregators.propTypes = {
aggregatorsList: PropTypes.array.isRequired,
};
export default DetectedTokenAggregators;

@ -0,0 +1,43 @@
import React from 'react';
import { DISPLAY } from '../../../../helpers/constants/design-system';
import Box from '../../../ui/box';
import DetectedTokenAggregators from './detected-token-aggregators';
export default {
title: 'Components/App/DetectedToken/DetectedTokenAggregators',
id: __filename,
argTypes: {
aggregatorsList: { control: 'array' },
},
args: {
aggregatorsList1: [
'Aave',
'Bancor',
'CMC',
'Crypto.com',
'CoinGecko',
'1inch',
'Paraswap',
'PMM',
'Synthetix',
'Zapper',
'Zerion',
'0x',
],
aggregatorsList2: ['Aave', 'Bancor'],
},
};
const Template = (args) => {
return (
<Box display={DISPLAY.GRID}>
<DetectedTokenAggregators aggregatorsList={args.aggregatorsList1} />
<DetectedTokenAggregators aggregatorsList={args.aggregatorsList2} />
</Box>
);
};
export const DefaultStory = Template.bind({});
DefaultStory.storyName = 'Default';

@ -0,0 +1,43 @@
import * as React from 'react';
import {
renderWithProvider,
screen,
fireEvent,
} from '../../../../../test/jest';
import configureStore from '../../../../store/store';
import DetectedTokenAggregators from './detected-token-aggregators';
describe('DetectedTokenAggregators', () => {
const args = {
aggregatorsList: [
'Aave',
'Bancor',
'CMC',
'Crypto.com',
'CoinGecko',
'1inch',
'Paraswap',
'PMM',
'Synthetix',
'Zapper',
'Zerion',
'0x',
],
};
it('should render the detected token aggregators', async () => {
const store = configureStore({});
renderWithProvider(<DetectedTokenAggregators {...args} />, store);
expect(screen.getByText('From token lists:')).toBeInTheDocument();
expect(screen.getByText('Aave, Bancor')).toBeInTheDocument();
expect(screen.getByText('+ 10 more')).toBeInTheDocument();
fireEvent.click(screen.getByText('+ 10 more'));
expect(
screen.getByText(
'Aave, Bancor, CMC, Crypto.com, CoinGecko, 1inch, Paraswap, PMM, Synthetix, Zapper, Zerion, 0x.',
),
).toBeInTheDocument();
});
});

@ -0,0 +1,13 @@
.detected-token-aggregators {
.typography {
display: inline;
}
& &__link {
@include H7;
padding: 0;
display: inline;
margin-left: 4px;
}
}

@ -0,0 +1,41 @@
import React from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import Box from '../../../ui/box';
import Identicon from '../../../ui/identicon';
import DetectedTokenValues from '../detected-token-values/detected-token-values';
import DetectedTokenAddress from '../detected-token-address/detected-token-address';
import DetectedTokenAggregators from '../detected-token-aggregators/detected-token-aggregators';
import { DISPLAY } from '../../../../helpers/constants/design-system';
import { getTokenList } from '../../../../selectors';
const DetectedTokenDetails = ({ tokenAddress }) => {
const tokenList = useSelector(getTokenList);
const token = tokenList[tokenAddress];
return (
<Box display={DISPLAY.FLEX} className="detected-token-details">
<Identicon
className="detected-token-details__identicon"
address={tokenAddress}
diameter={40}
/>
<Box
display={DISPLAY.GRID}
marginLeft={2}
className="detected-token-details__data"
>
<DetectedTokenValues token={token} />
<DetectedTokenAddress address={token.address} />
<DetectedTokenAggregators aggregatorsList={token.aggregators} />
</Box>
</Box>
);
};
DetectedTokenDetails.propTypes = {
tokenAddress: PropTypes.string,
};
export default DetectedTokenDetails;

@ -0,0 +1,26 @@
import React from 'react';
import DetectedTokenDetails from './detected-token-details';
export default {
title: 'Components/App/DetectedToken/DetectedTokenDetails',
id: __filename,
argTypes: {
address: { control: 'text' },
},
args: {
address: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f',
},
};
const Template = (args) => {
return (
<div style={{ width: '320px' }}>
<DetectedTokenDetails tokenAddress={args.address} />
</div>
);
};
export const DefaultStory = Template.bind({});
DefaultStory.storyName = 'Default';

@ -0,0 +1,35 @@
import * as React from 'react';
import {
renderWithProvider,
screen,
fireEvent,
} from '../../../../../test/jest';
import configureStore from '../../../../store/store';
import testData from '../../../../../.storybook/test-data';
import DetectedTokenDetails from './detected-token-details';
describe('DetectedTokenDetails', () => {
const args = {
tokenAddress: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f',
};
it('should render the detected token details', async () => {
const store = configureStore(testData);
renderWithProvider(<DetectedTokenDetails {...args} />, store);
expect(screen.getByText('0 SNX')).toBeInTheDocument();
expect(screen.getByText('$0')).toBeInTheDocument();
expect(screen.getByText('Token address:')).toBeInTheDocument();
expect(screen.getByText('0xc01...2a6f')).toBeInTheDocument();
expect(screen.getByText('From token lists:')).toBeInTheDocument();
expect(screen.getByText('Aave, Bancor')).toBeInTheDocument();
expect(screen.getByText('+ 10 more')).toBeInTheDocument();
fireEvent.click(screen.getByText('+ 10 more'));
expect(
screen.getByText(
'Aave, Bancor, CMC, Crypto.com, CoinGecko, 1inch, Paraswap, PMM, Synthetix, Zapper, Zerion, 0x.',
),
).toBeInTheDocument();
});
});

@ -0,0 +1,9 @@
.detected-token-details {
&__identicon {
margin-top: 4px;
}
&__data {
flex-grow: 1;
}
}

@ -0,0 +1,57 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import Box from '../../../ui/box';
import Typography from '../../../ui/typography';
import CheckBox from '../../../ui/check-box';
import {
COLORS,
DISPLAY,
TYPOGRAPHY,
} from '../../../../helpers/constants/design-system';
import { useTokenTracker } from '../../../../hooks/useTokenTracker';
import { useTokenFiatAmount } from '../../../../hooks/useTokenFiatAmount';
const DetectedTokenValues = ({ token }) => {
const [selectedTokens, setSelectedTokens] = useState(false);
const { tokensWithBalances } = useTokenTracker([token]);
const balanceToRender = tokensWithBalances[0]?.string;
const balance = tokensWithBalances[0]?.balance;
const formattedFiatBalance = useTokenFiatAmount(
token.address,
balanceToRender,
token.symbol,
);
return (
<Box display={DISPLAY.INLINE_FLEX} className="detected-token-values">
<Box marginBottom={1}>
<Typography variant={TYPOGRAPHY.H4}>
{`${balance || '0'} ${token.symbol}`}
</Typography>
<Typography variant={TYPOGRAPHY.H7} color={COLORS.TEXT_ALTERNATIVE}>
{formattedFiatBalance || '$0'}
</Typography>
</Box>
<Box className="detected-token-values__checkbox">
<CheckBox
checked={selectedTokens}
onClick={() => setSelectedTokens((checked) => !checked)}
/>
</Box>
</Box>
);
};
DetectedTokenValues.propTypes = {
token: PropTypes.shape({
address: PropTypes.string.isRequired,
decimals: PropTypes.number,
symbol: PropTypes.string,
iconUrl: PropTypes.string,
aggregators: PropTypes.array,
}),
};
export default DetectedTokenValues;

@ -0,0 +1,43 @@
import React from 'react';
import DetectedTokenValues from './detected-token-values';
export default {
title: 'Components/App/DetectedToken/DetectedTokenValues',
id: __filename,
argTypes: {
address: { control: 'text' },
symbol: { control: 'text' },
decimals: { control: 'text' },
iconUrl: { control: 'text' },
aggregators: { control: 'array' },
},
args: {
address: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f',
symbol: 'SNX',
decimals: 18,
iconUrl: 'https://assets.coingecko.com/coins/images/3406/large/SNX.png',
aggregators: [
'aave',
'bancor',
'cmc',
'cryptocom',
'coinGecko',
'oneInch',
'paraswap',
'pmm',
'synthetix',
'zapper',
'zerion',
'zeroEx',
],
},
};
const Template = (args) => {
return <DetectedTokenValues token={args} />;
};
export const DefaultStory = Template.bind({});
DefaultStory.storyName = 'Default';

@ -0,0 +1,37 @@
import * as React from 'react';
import { renderWithProvider, screen } from '../../../../../test/jest';
import configureStore from '../../../../store/store';
import testData from '../../../../../.storybook/test-data';
import DetectedTokenValues from './detected-token-values';
describe('DetectedTokenValues', () => {
const args = {
address: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f',
symbol: 'SNX',
decimals: 18,
iconUrl: 'https://assets.coingecko.com/coins/images/3406/large/SNX.png',
aggregators: [
'aave',
'bancor',
'cmc',
'cryptocom',
'coinGecko',
'oneInch',
'paraswap',
'pmm',
'synthetix',
'zapper',
'zerion',
'zeroEx',
],
};
it('should render the detected token address', async () => {
const store = configureStore(testData);
renderWithProvider(<DetectedTokenValues token={args} />, store);
expect(screen.getByText('0 SNX')).toBeInTheDocument();
expect(screen.getByText('$0')).toBeInTheDocument();
});
});

@ -0,0 +1,5 @@
.detected-token-values {
&__checkbox {
margin-left: auto;
}
}

@ -35,7 +35,7 @@ import ActionableMessage from '../../ui/actionable-message/actionable-message';
import { I18nContext } from '../../../contexts/i18n';
import GasTiming from '../gas-timing';
import { MetaMetricsContext } from '../../../contexts/metametrics.new';
import { MetaMetricsContext } from '../../../contexts/metametrics';
export default function EditGasDisplay({
mode = EDIT_GAS_MODES.MODIFY_IN_PLACE,

@ -26,11 +26,11 @@
&__label {
white-space: nowrap;
width: 50%;
min-width: 50%;
}
&__value {
width: 50%;
min-width: 50%;
}
p {
@ -44,6 +44,7 @@
div {
display: flex;
flex-direction: row;
flex-wrap: wrap;
text-align: left;
}
}

@ -0,0 +1,192 @@
import React, { useCallback, useContext, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import Button from '../../ui/button';
import { I18nContext } from '../../../contexts/i18n';
import Box from '../../ui/box/box';
import {
ALIGN_ITEMS,
DISPLAY,
JUSTIFY_CONTENT,
} from '../../../helpers/constants/design-system';
const radius = 14;
const strokeWidth = 2;
const radiusWithStroke = radius - strokeWidth / 2;
export default function HoldToRevealButton({ buttonText, onLongPressed }) {
const t = useContext(I18nContext);
const isLongPressing = useRef(false);
const [isUnlocking, setIsUnlocking] = useState(false);
const [hasTriggeredUnlock, setHasTriggeredUnlock] = useState(false);
/**
* Prevent animation events from propogating up
*
* @param e - Native animation event - React.AnimationEvent<HTMLDivElement>
*/
const preventPropogation = (e) => {
e.stopPropagation();
};
/**
* Event for mouse click down
*/
const onMouseDown = () => {
isLongPressing.current = true;
};
/**
* Event for mouse click up
*/
const onMouseUp = () => {
isLongPressing.current = false;
};
/**
* 1. Progress cirle completed. Begin next animation phase (Shrink halo and show unlocked padlock)
*/
const onProgressComplete = () => {
isLongPressing.current && setIsUnlocking(true);
};
/**
* 2. Trigger onLongPressed callback. Begin next animation phase (Shrink unlocked padlock and fade in original content)
*
* @param e - Native animation event - React.AnimationEvent<HTMLDivElement>
*/
const triggerOnLongPressed = (e) => {
onLongPressed();
setHasTriggeredUnlock(true);
preventPropogation(e);
};
/**
* 3. Reset animation states
*/
const resetAnimationStates = () => {
setIsUnlocking(false);
setHasTriggeredUnlock(false);
};
const renderPreCompleteContent = useCallback(() => {
return (
<Box
className={`hold-to-reveal-button__absolute-fill ${
isUnlocking ? 'hold-to-reveal-button__invisible' : null
} ${
hasTriggeredUnlock ? 'hold-to-reveal-button__main-icon-show' : null
}`}
>
<Box className="hold-to-reveal-button__absolute-fill">
<svg className="hold-to-reveal-button__circle-svg">
<circle
className="hold-to-reveal-button__circle-background"
cx={radius}
cy={radius}
r={radiusWithStroke}
/>
</svg>
</Box>
<Box className="hold-to-reveal-button__absolute-fill">
<svg className="hold-to-reveal-button__circle-svg">
<circle
onTransitionEnd={onProgressComplete}
className="hold-to-reveal-button__circle-foreground"
cx={radius}
cy={radius}
r={radiusWithStroke}
/>
</svg>
</Box>
<Box
display={DISPLAY.FLEX}
alignItems={ALIGN_ITEMS.CENTER}
justifyContent={JUSTIFY_CONTENT.CENTER}
className="hold-to-reveal-button__lock-icon-container"
>
<img
src="images/lock-icon.svg"
alt={t('padlock')}
className="hold-to-reveal-button__lock-icon"
/>
</Box>
</Box>
);
}, [isUnlocking, hasTriggeredUnlock, t]);
const renderPostCompleteContent = useCallback(() => {
return isUnlocking ? (
<div
className={`hold-to-reveal-button__absolute-fill ${
hasTriggeredUnlock ? 'hold-to-reveal-button__unlock-icon-hide' : null
}`}
onAnimationEnd={resetAnimationStates}
>
<div
onAnimationEnd={preventPropogation}
className="hold-to-reveal-button__absolute-fill hold-to-reveal-button__circle-static-outer-container"
>
<svg className="hold-to-reveal-button__circle-svg">
<circle
className="hold-to-reveal-button__circle-static-outer"
cx={14}
cy={14}
r={14}
/>
</svg>
</div>
<div
onAnimationEnd={preventPropogation}
className="hold-to-reveal-button__absolute-fill hold-to-reveal-button__circle-static-inner-container"
>
<svg className="hold-to-reveal-button__circle-svg">
<circle
className="hold-to-reveal-button__circle-static-inner"
cx={14}
cy={14}
r={12}
/>
</svg>
</div>
<div
className="hold-to-reveal-button__unlock-icon-container"
onAnimationEnd={triggerOnLongPressed}
>
<img
src="images/unlock-icon.svg"
alt={t('padlock')}
className="hold-to-reveal-button__unlock-icon"
/>
</div>
</div>
) : null;
}, [isUnlocking, hasTriggeredUnlock, t]);
return (
<Button
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
type="primary"
icon={
<Box marginRight={2} className="hold-to-reveal-button__icon-container">
{renderPreCompleteContent()}
{renderPostCompleteContent()}
</Box>
}
className="hold-to-reveal-button__button-hold"
>
{buttonText}
</Button>
);
}
HoldToRevealButton.propTypes = {
/**
* Text to be displayed on the button
*/
buttonText: PropTypes.string.isRequired,
/**
* Function to be called after the animation is finished
*/
onLongPressed: PropTypes.func.isRequired,
};

@ -0,0 +1,22 @@
import React from 'react';
import HoldToRevealButton from './hold-to-reveal-button';
export default {
title: 'Components/App/HoldToRevealButton',
id: __filename,
argTypes: {
buttonText: { control: 'text' },
onLongPressed: { action: 'Revealing the SRP' },
},
};
export const DefaultStory = (args) => {
return <HoldToRevealButton {...args} />;
};
DefaultStory.storyName = 'Default';
DefaultStory.args = {
buttonText: 'Hold to reveal SRP',
onLongPressed: () => console.log('Revealed'),
};

@ -0,0 +1,72 @@
import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react';
import HoldToRevealButton from './hold-to-reveal-button';
describe('HoldToRevealButton', () => {
let props = {};
beforeEach(() => {
const mockOnLongPressed = jest.fn();
props = {
onLongPressed: mockOnLongPressed,
buttonText: 'Hold to reveal SRP',
};
});
it('should render a button with label', () => {
const { getByText } = render(<HoldToRevealButton {...props} />);
expect(getByText('Hold to reveal SRP')).toBeInTheDocument();
});
it('should render a button when mouse is down and up', () => {
const { getByText } = render(<HoldToRevealButton {...props} />);
const button = getByText('Hold to reveal SRP');
fireEvent.mouseDown(button);
expect(button).toBeDefined();
fireEvent.mouseUp(button);
expect(button).toBeDefined();
});
it('should not show the locked padlock when a button is long pressed and then should show it after it was lifted off before the animation concludes', () => {
const { getByText } = render(<HoldToRevealButton {...props} />);
const button = getByText('Hold to reveal SRP');
fireEvent.mouseDown(button);
waitFor(() => {
expect(button.firstChild).toHaveClass(
'hold-to-reveal-button__lock-icon-container',
);
});
fireEvent.mouseUp(button);
waitFor(() => {
expect(button.firstChild).not.toHaveClass(
'hold-to-reveal-button__lock-icon-container',
);
});
});
it('should show the unlocked padlock when a button is long pressed for the duration of the animation', () => {
const { getByText } = render(<HoldToRevealButton {...props} />);
const button = getByText('Hold to reveal SRP');
fireEvent.mouseDown(button);
waitFor(() => {
expect(button.firstChild).toHaveClass(
'hold-to-reveal-button__unlock-icon-container',
);
});
});
});

@ -0,0 +1 @@
export { default } from './hold-to-reveal-button';

@ -0,0 +1,164 @@
// Variables
$circle-radius: 14px;
$circle-diameter: $circle-radius * 2;
// Circumference ~ (2*PI*R). We reduced the number a little to create a snappier interaction
$circle-circumference: 82;
$circle-stroke-width: 2px;
// Keyframes
@keyframes collapse {
from {
transform: scale(1);
}
to {
transform: scale(0);
}
}
@keyframes expand {
from {
transform: scale(0);
}
to {
transform: scale(1);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.hold-to-reveal-button {
// Shared styles
&__absolute-fill {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
&__icon {
height: $circle-diameter;
width: $circle-diameter;
}
&__circle-shared {
fill: transparent;
stroke-width: $circle-stroke-width;
}
// Class styles
&__button-hold {
padding: 6px 13px 6px 9px !important;
transform: scale(1) !important;
transition: 0.5s transform !important;
&:active {
background-color: var(--primary-1) !important;
transform: scale(1.05) !important;
.hold-to-reveal-button__circle-foreground {
stroke-dashoffset: 0 !important;
}
.hold-to-reveal-button__lock-icon-container {
opacity: 0 !important;
}
}
}
&__absolute-fill {
@extend .hold-to-reveal-button__absolute-fill;
}
&__icon-container {
@extend .hold-to-reveal-button__icon;
position: relative;
}
&__main-icon-show {
animation: 0.4s fadeIn 1.2s forwards;
}
&__invisible {
opacity: 0;
}
&__circle-svg {
@extend .hold-to-reveal-button__icon;
transform: rotate(-90deg);
}
&__circle-background {
@extend .hold-to-reveal-button__circle-shared;
stroke: var(--primary-3);
}
&__circle-foreground {
@extend .hold-to-reveal-button__circle-shared;
stroke: var(--ui-white);
stroke-dasharray: $circle-circumference;
stroke-dashoffset: $circle-circumference;
transition: 1s stroke-dashoffset;
}
&__lock-icon-container {
@extend .hold-to-reveal-button__absolute-fill;
transition: 0.3s opacity;
opacity: 1;
}
&__lock-icon {
width: 7.88px;
height: 9px;
}
&__unlock-icon-hide {
animation: 0.3s collapse 1s forwards;
}
&__circle-static-outer-container {
animation: 0.25s collapse forwards;
}
&__circle-static-outer {
fill: var(--ui-white);
}
&__circle-static-inner-container {
animation: 0.125s collapse forwards;
}
&__circle-static-inner {
fill: var(--primary-1);
}
&__unlock-icon-container {
@extend .hold-to-reveal-button__absolute-fill;
display: flex;
align-items: center;
justify-content: center;
transform: scale(0);
animation: 0.175s expand 0.2s forwards;
}
&__unlock-icon {
width: 14px;
height: 11px;
}
}

@ -7,7 +7,7 @@ import Button from '../../ui/button';
import Box from '../../ui/box/box';
import { TEXT_ALIGN } from '../../../helpers/constants/design-system';
import { detectNewTokens } from '../../../store/actions';
import { MetaMetricsContext } from '../../../contexts/metametrics.new';
import { MetaMetricsContext } from '../../../contexts/metametrics';
export default function ImportTokenLink({ isMainnet }) {
const trackEvent = useContext(MetaMetricsContext);

@ -17,7 +17,7 @@ import {
import { useI18nContext } from '../../../hooks/useI18nContext';
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app';
import { MetaMetricsContext } from '../../../contexts/metametrics.new';
import { MetaMetricsContext } from '../../../contexts/metametrics';
export default function AccountOptionsMenu({ anchorElement, onClose }) {
const t = useI18nContext();

@ -9,7 +9,7 @@ import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app';
import { CONNECTED_ACCOUNTS_ROUTE } from '../../../helpers/constants/routes';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { getOriginOfCurrentTab } from '../../../selectors';
import { MetaMetricsContext } from '../../../contexts/metametrics.new';
import { MetaMetricsContext } from '../../../contexts/metametrics';
import AccountOptionsMenu from './account-options-menu';
export default function MenuBar() {

@ -6,30 +6,14 @@ export default {
id: __filename,
component: ConfirmRemoveAccount,
argTypes: {
hideModal: {
action: 'hideModal',
},
removeAccount: {
action: 'removeAccount',
},
identity: {
control: 'object',
},
chainId: {
control: 'text',
},
rpcPrefs: {
control: 'object',
},
},
args: {
identity: {
control: 'object',
},
chainId: 'chainId',
rpcPrefs: {
control: 'object',
},
},
};

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save