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

feature/default_network_editable
Dan Miller 2 years ago
commit d7adbcd8d7
  1. 1
      .browserslistrc
  2. 12
      .circleci/config.yml
  3. 2
      .circleci/scripts/firefox-install.sh
  4. 32
      .github/PULL_REQUEST_TEMPLATE.md
  5. 2
      .github/workflows/cla.yml
  6. 2
      .github/workflows/codeql-analysis.yml
  7. 2
      .github/workflows/crowdin_action.yml
  8. 4
      .storybook/initial-states/transactions.js
  9. 13
      README.md
  10. 2
      app/_locales/de/messages.json
  11. 70
      app/_locales/en/messages.json
  12. 994
      app/_locales/zh_TW/messages.json
  13. 830
      app/build-types/beta/images/beta-mascot.json
  14. BIN
      app/build-types/beta/images/icon-128.png
  15. BIN
      app/build-types/beta/images/icon-16.png
  16. BIN
      app/build-types/beta/images/icon-19.png
  17. BIN
      app/build-types/beta/images/icon-32.png
  18. BIN
      app/build-types/beta/images/icon-34.png
  19. BIN
      app/build-types/beta/images/icon-38.png
  20. BIN
      app/build-types/beta/images/icon-48.png
  21. BIN
      app/build-types/beta/images/icon-512.png
  22. BIN
      app/build-types/beta/images/icon-64.png
  23. BIN
      app/build-types/beta/images/info-logo.png
  24. 164
      app/build-types/beta/images/logo/metamask-fox.svg
  25. 70
      app/scripts/background.js
  26. 204
      app/scripts/contentscript.js
  27. 34
      app/scripts/controllers/app-state.js
  28. 165
      app/scripts/controllers/metametrics.js
  29. 129
      app/scripts/controllers/metametrics.test.js
  30. 5
      app/scripts/controllers/permissions/specifications.test.js
  31. 22
      app/scripts/controllers/preferences.js
  32. 12
      app/scripts/controllers/preferences.test.js
  33. 147
      app/scripts/lib/account-tracker.js
  34. 13
      app/scripts/lib/segment/analytics.js
  35. 8
      app/scripts/lib/stream-utils.js
  36. 17
      app/scripts/lib/util.js
  37. 164
      app/scripts/metamask-controller.js
  38. 46
      app/scripts/migrations/076.js
  39. 143
      app/scripts/migrations/076.test.js
  40. 2
      app/scripts/migrations/index.js
  41. 4
      app/scripts/sentry-install.js
  42. 55
      app/scripts/ui.js
  43. 2
      development/build/index.js
  44. 6
      development/build/styles.js
  45. 2
      development/generate-icon-names.js
  46. 68
      development/metamaskbot-build-announce.js
  47. 2
      development/ts-migration-dashboard/files-to-convert.json
  48. 1
      docs/QA_Guide.md
  49. 4
      docs/generating-fixture-data.md
  50. 133
      lavamoat/browserify/beta/policy.json
  51. 262
      lavamoat/browserify/flask/policy.json
  52. 133
      lavamoat/browserify/main/policy.json
  53. 126
      lavamoat/build-system/policy.json
  54. 17
      package.json
  55. 18
      patches/luxon+3.1.0.patch
  56. 7
      shared/constants/app.ts
  57. 2
      shared/constants/network.ts
  58. 1
      shared/constants/permissions.ts
  59. 37
      shared/constants/swaps.js
  60. 4
      shared/constants/tokens.js
  61. 4
      shared/constants/transaction.js
  62. 48
      shared/lib/error-utils.js
  63. 6
      shared/lib/error-utils.test.js
  64. 4
      shared/lib/swaps-utils.js
  65. 55
      shared/modules/browser-runtime.utils.js
  66. 54
      shared/modules/browser-runtime.utils.test.js
  67. 2
      shared/modules/transaction.utils.js
  68. 15
      shared/notifications/index.js
  69. 42
      test/data/mock-state.json
  70. 21
      test/e2e/fixture-builder.js
  71. 26
      test/e2e/helpers.js
  72. 16
      test/e2e/metamask-ui.spec.js
  73. 2
      test/e2e/restore/MetaMaskUserData.json
  74. 17
      test/e2e/run-all.js
  75. 2
      test/e2e/snaps/enums.js
  76. 122
      test/e2e/snaps/test-snap-bip-32.spec.js
  77. 74
      test/e2e/snaps/test-snap-bip-44.spec.js
  78. 38
      test/e2e/snaps/test-snap-confirm.spec.js
  79. 49
      test/e2e/snaps/test-snap-error.spec.js
  80. 75
      test/e2e/snaps/test-snap-installed.spec.js
  81. 58
      test/e2e/snaps/test-snap-managestate.spec.js
  82. 46
      test/e2e/snaps/test-snap-notification.spec.js
  83. 70
      test/e2e/snaps/test-snap-update.spec.js
  84. 8
      test/e2e/tests/custom-token-add-approve.spec.js
  85. 27
      test/e2e/tests/send-eth.spec.js
  86. 8
      test/e2e/tests/send-hex-address.spec.js
  87. 8
      test/e2e/tests/signature-request.spec.js
  88. 14
      test/e2e/webdriver/driver.js
  89. 6
      test/helpers/setup-helper.js
  90. 73
      test/jest/mock-store.js
  91. 17
      test/lib/render-helpers.js
  92. 1
      ui/components/app/app-components.scss
  93. 62
      ui/components/app/app-header/app-header.component.js
  94. 18
      ui/components/app/app-header/app-header.container.js
  95. 63
      ui/components/app/approve-content-card/approve-content-card.js
  96. 196
      ui/components/app/approve-content-card/approve-content-card.stories.js
  97. 36
      ui/components/app/beta-header/__snapshots__/beta-header.test.js.snap
  98. 24
      ui/components/app/beta-header/beta-header.stories.js
  99. 45
      ui/components/app/beta-header/beta-header.test.js
  100. 59
      ui/components/app/beta-header/index.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1 @@
chrome >= 66, firefox >= 68

@ -516,6 +516,7 @@ jobs:
test-e2e-chrome:
executor: node-browsers
parallelism: 8
steps:
- checkout
- run:
@ -540,9 +541,10 @@ jobs:
- store_artifacts:
path: test-artifacts
destination: test-artifacts
test-e2e-chrome-mv3:
executor: node-browsers
parallelism: 8
steps:
- checkout
- run:
@ -570,6 +572,7 @@ jobs:
test-e2e-firefox-snaps:
executor: node-browsers
parallelism: 2
steps:
- checkout
- run:
@ -597,6 +600,7 @@ jobs:
test-e2e-chrome-snaps:
executor: node-browsers
parallelism: 2
steps:
- checkout
- run:
@ -624,6 +628,7 @@ jobs:
test-e2e-firefox:
executor: node-browsers-medium-plus
parallelism: 8
steps:
- checkout
- run:
@ -792,6 +797,11 @@ jobs:
- store_artifacts:
path: development/ts-migration-dashboard/build
destination: ts-migration-dashboard
- run:
name: Set branch parent commit env var
command: |
echo "export PARENT_COMMIT=$(git rev-parse "$(git rev-list --topo-order --reverse HEAD ^origin/develop | head -1)"^)" >> $BASH_ENV
source $BASH_ENV
- run:
name: build:announce
command: ./development/metamaskbot-build-announce.js

@ -4,7 +4,7 @@ set -e
set -u
set -o pipefail
FIREFOX_VERSION='102.0'
FIREFOX_VERSION='106.0.4'
FIREFOX_BINARY="firefox-${FIREFOX_VERSION}.tar.bz2"
FIREFOX_BINARY_URL="https://ftp.mozilla.org/pub/firefox/releases/${FIREFOX_VERSION}/linux-x86_64/en-US/${FIREFOX_BINARY}"
FIREFOX_PATH='/opt/firefox'

@ -6,18 +6,6 @@ Thanks for the pull request. Take a moment to answer these questions so that rev
* What is the current state of things and why does it need to change?
* What is the solution your changes offer and how does it work?
Below is a template to give you some ideas. Feel free to use your own words!
Currently, ...
This is a problem because ...
In order to solve this problem, this pull request ...
-->
## More Information
<!--
Are there any issues, Slack conversations, Zendesk issues, user stories, etc. reviewers should consult to understand this pull request better? For instance:
* Fixes #12345
@ -46,14 +34,20 @@ How should reviewers and QA manually test your changes? For instance:
- Then do this
-->
## Pre-Merge Checklist
## Pre-merge author checklist
- [ ] I've clearly explained:
- [ ] What problem this PR is solving
- [ ] How this problem was solved
- [ ] How reviewers can test my changes
- [ ] Sufficient automated test coverage has been added
## Pre-merge reviewer checklist
- [ ] PR template is filled out
- [ ] **IF** this PR fixes a bug, a test that _would have_ caught the bug has been added
- [ ] Manual testing (e.g. pull and build branch, run in browser, test code being changed)
- [ ] PR is linked to the appropriate GitHub issue
- [ ] PR has been added to the appropriate release Milestone
- [ ] **IF** this PR fixes a bug in the release milestone, add this PR to the release milestone
### + If there are functional changes:
If further QA is required (e.g. new feature, complex testing steps, large refactor), add the `Extension QA Board` label.
- [ ] Manual testing complete & passed
- [ ] "Extension QA Board" label has been applied
In this case, a QA Engineer approval will be be required.

@ -8,7 +8,7 @@ on:
jobs:
CLABot:
if: github.event_name == 'pull_request_target' || contains(github.event.comment.html_url, '/pull/')
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: write

@ -23,7 +23,7 @@ on:
jobs:
analyze:
name: Analyze
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
permissions:
actions: read
contents: read

@ -13,7 +13,7 @@ on:
jobs:
synchronize-with-crowdin:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:

@ -1454,7 +1454,7 @@ export const MOCK_TRANSACTION_BY_TYPE = {
dappSuggestedGasFees: null,
sendFlowHistory: [
{
entry: 'sendFlow - user set asset type to COLLECTIBLE',
entry: 'sendFlow - user set asset type to NFT',
timestamp: 1653457317999,
},
{
@ -1504,7 +1504,7 @@ export const MOCK_TRANSACTION_BY_TYPE = {
dappSuggestedGasFees: null,
sendFlowHistory: [
{
entry: 'sendFlow - user set asset type to COLLECTIBLE',
entry: 'sendFlow - user set asset type to NFT',
timestamp: 1653457317999,
},
{

@ -15,7 +15,7 @@ To learn how to contribute to the MetaMask project itself, visit our [Internal D
## Building locally
- Install [Node.js](https://nodejs.org) version 16
- If you are using [nvm](https://github.com/creationix/nvm#installation) (recommended) running `nvm use` will automatically choose the right node version for you.
- If you are using [nvm](https://github.com/nvm-sh/nvm#installing-and-updating) (recommended) running `nvm use` will automatically choose the right node version for you.
- Install [Yarn](https://yarnpkg.com/en/docs/install)
- Install dependencies: `yarn setup` (not the usual install command)
- Copy the `.metamaskrc.dist` file to `.metamaskrc`
@ -61,13 +61,16 @@ You can run the linter by itself with `yarn lint`, and you can automatically fix
### Running E2E Tests
Our e2e test suite can be run on either Firefox or Chrome. In either case, start by creating a test build by running `yarn build:test`.
Our e2e test suite can be run on either Firefox or Chrome.
- Firefox e2e tests can be run with `yarn test:e2e:firefox`.
1. **required** `yarn build:test` to create a test build.
2. run tests, targetting the browser:
* Firefox e2e tests can be run with `yarn test:e2e:firefox`.
* Chrome e2e tests can be run with `yarn test:e2e:chrome`. The `chromedriver` package major version must match the major version of your local Chrome installation. If they don't match, update whichever is behind before running Chrome e2e tests.
- Chrome e2e tests can be run with `yarn test:e2e:chrome`. The `chromedriver` package major version must match the major version of your local Chrome installation. If they don't match, update whichever is behind before running Chrome e2e tests.
#### Running a single e2e test
- Single e2e tests can be run with `yarn test:e2e:single test/e2e/tests/TEST_NAME.spec.js` along with the options below.
Single e2e tests can be run with `yarn test:e2e:single test/e2e/tests/TEST_NAME.spec.js` along with the options below.
```console
--browser Set the browser used; either 'chrome' or 'firefox'.

@ -1853,7 +1853,7 @@
"message": "Richtungspfeil"
},
"lightTheme": {
"message": "Leicht"
"message": "Hell"
},
"likeToImportTokens": {
"message": "Möchtest du diese Token hinzufügen?"

@ -434,18 +434,35 @@
"beCareful": {
"message": "Be careful"
},
"beta": {
"message": "Beta"
},
"betaHeaderText": {
"message": "This is a BETA version. Please report bugs $1",
"description": "$1 represents the word 'here' in a hyperlink"
},
"betaMetamaskDescription": {
"message": "Trusted by millions, MetaMask is a secure wallet making the world of web3 accessible to all."
},
"betaMetamaskDescriptionDisclaimerHeading": {
"message": "Beta version disclaimer"
},
"betaMetamaskDescriptionExplanation": {
"message": "Use this version to test upcoming features before they’re released. Your use and feedback helps us build the best version of MetaMask possible. Your use of MetaMask Beta is subject to our standard $1 as well as our $2. As a Beta, there may be an increased risk of bugs. By proceeding, you accept and acknowledge these risks, as well as those risks found in our Terms and Beta Terms.",
"message": "This version allows you to test upcoming features before they’re released, which helps make MetaMask even better. As with all beta versions, there may be an increased risk of bugs. MetaMask Beta is subject to our $1 as well as our $2.",
"description": "$1 represents localization item betaMetamaskDescriptionExplanationTermsLinkText. $2 represents localization item betaMetamaskDescriptionExplanationBetaTermsLinkText"
},
"betaMetamaskDescriptionExplanation2": {
"message": "By proceeding, you accept and acknowledge these risks, our $1, and $2.",
"description": "$1 represents localization item betaMetamaskDescriptionExplanationTermsLinkText. $2 represents localization item betaMetamaskDescriptionExplanation2BetaTermsLinkText"
},
"betaMetamaskDescriptionExplanation2BetaTermsLinkText": {
"message": "Beta Terms"
},
"betaMetamaskDescriptionExplanationBetaTermsLinkText": {
"message": "Supplemental Beta Terms"
"message": "supplemental Beta Terms"
},
"betaMetamaskDescriptionExplanationTermsLinkText": {
"message": "Terms"
"message": "standard Terms"
},
"betaMetamaskVersion": {
"message": "MetaMask Beta Version"
@ -454,13 +471,13 @@
"message": "beta portfolio site"
},
"betaTerms": {
"message": "BETA Terms of use"
"message": "Beta Terms of use"
},
"betaWalletCreationSuccessReminder1": {
"message": "MetaMask BETA can’t recover your Secret Recovery Phrase."
"message": "MetaMask Beta can’t recover your Secret Recovery Phrase."
},
"betaWalletCreationSuccessReminder2": {
"message": "MetaMask BETA will never ask you for your Secret Recovery Phrase."
"message": "MetaMask Beta will never ask you for your Secret Recovery Phrase."
},
"betaWelcome": {
"message": "Welcome to MetaMask Beta"
@ -767,6 +784,12 @@
"contractInteraction": {
"message": "Contract interaction"
},
"contractNFT": {
"message": "NFT contract"
},
"contractRequestingAccess": {
"message": "Contract requesting access"
},
"contractRequestingSpendingCap": {
"message": "Contract requesting spending cap"
},
@ -1694,6 +1717,12 @@
"message": "Imported",
"description": "status showing that an account has been fully loaded into the keyring"
},
"improvedTokenAllowance": {
"message": "Improved token allowance experience"
},
"improvedTokenAllowanceDescription": {
"message": "Turn this on to go through the improved token allowance experience whenever a dapp requests an ERC20 approve"
},
"inYourSettings": {
"message": "in your Settings"
},
@ -2146,6 +2175,9 @@
"networkName": {
"message": "Network name"
},
"networkNameArbitrum": {
"message": "Arbitrum"
},
"networkNameAvalanche": {
"message": "Avalanche"
},
@ -2161,6 +2193,9 @@
"networkNameGoerli": {
"message": "Goerli"
},
"networkNameOptimism": {
"message": "Optimism"
},
"networkNamePolygon": {
"message": "Polygon"
},
@ -2380,6 +2415,15 @@
"notifications15Title": {
"message": "The Ethereum Merge is here!"
},
"notifications16ActionText": {
"message": "Try it out here"
},
"notifications16Description": {
"message": "We redesigned our token allowance confirmation to help you make more informed decisions."
},
"notifications16Title": {
"message": "Improved token allowance experience"
},
"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."
@ -2657,6 +2701,10 @@
"message": "Connect to the $1 Snap.",
"description": "The description for the `wallet_snap_*` permission. $1 is the name of the Snap."
},
"permission_cronjob": {
"message": "Schedule and execute periodic actions.",
"description": "The description for the `snap_cronjob` permission"
},
"permission_customConfirmation": {
"message": "Display a confirmation in MetaMask.",
"description": "The description for the `snap_confirm` permission"
@ -2940,6 +2988,9 @@
"message": "By revoking permission, the following $1 will no longer be able to access your $2",
"description": "$1 is either key 'account' or 'contract', and $2 is either a string or link of a given token symbol or name"
},
"revokeSpendingCap": {
"message": "Revoke spending cap for your"
},
"revokeSpendingCapTooltipText": {
"message": "This contract will be unable to spend any more of your current or future tokens."
},
@ -3263,6 +3314,10 @@
"snaps": {
"message": "Snaps"
},
"snapsInsightError": {
"message": "An error occured with $1: $2",
"description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message."
},
"snapsInsightLoading": {
"message": "Loading transaction insight..."
},
@ -3281,6 +3336,9 @@
"someNetworksMayPoseSecurity": {
"message": "Some networks may pose security and/or privacy risks. Understand the risks before adding & using a network."
},
"somethingIsWrong": {
"message": "Something's gone wrong. Try reloading the page."
},
"somethingWentWrong": {
"message": "Oops! Something went wrong."
},

File diff suppressed because it is too large Load Diff

@ -1,830 +0,0 @@
{
"chunks": [
{
"faces": [
[0, 1, 2],
[2, 3, 0],
[4, 5, 2],
[6, 3, 2],
[2, 5, 6],
[7, 8, 9],
[10, 3, 6],
[10, 50, 7],
[7, 3, 10],
[7, 9, 3],
[49, 0, 9],
[3, 9, 0],
[2, 1, 4]
],
"name": "left ear",
"gradient": "left-ear-gradient"
},
{
"faces": [
[53, 54, 55],
[55, 56, 53],
[57, 56, 55],
[58, 59, 55],
[55, 54, 58],
[60, 61, 62],
[63, 58, 54],
[63, 60, 89],
[60, 63, 54],
[60, 54, 61],
[88, 61, 53],
[54, 53, 61],
[55, 59, 57]
],
"name": "right ear",
"gradient": "right-ear-gradient"
},
{
"color": [22, 22, 22],
"faces": [[11, 12, 13]],
"name": "left eye"
},
{
"color": [22, 22, 22],
"faces": [[64, 65, 66]],
"name": "right eye"
},
{
"faces": [
[14, 15, 11],
[11, 16, 14]
],
"name": "left inner eye",
"gradient": "left-inner-eye-gradient"
},
{
"faces": [[17, 12, 18]],
"name": "left outer eye",
"gradient": "left-outer-eye-gradient"
},
{
"faces": [[41, 64, 37]],
"name": "right lower inner eye",
"gradient": "right-inner-eye-gradient"
},
{
"faces": [[67, 68, 66]],
"name": "right outer eye",
"gradient": "right-outer-eye-gradient"
},
{
"color": [192, 173, 158],
"faces": [
[19, 20, 21],
[21, 22, 19],
[20, 19, 23],
[23, 24, 20],
[23, 25, 24],
[19, 22, 26],
[26, 27, 19],
[23, 28, 29],
[23, 29, 30],
[25, 23, 30],
[29, 51, 52],
[52, 30, 29],
[27, 26, 69],
[69, 70, 27],
[70, 71, 72],
[72, 27, 70],
[72, 71, 73],
[51, 74, 72],
[52, 51, 72],
[73, 52, 72],
[19, 27, 74],
[74, 28, 19],
[51, 29, 28],
[28, 74, 51],
[74, 27, 72],
[28, 23, 19]
],
"name": "lower chin"
},
{
"color": [215, 193, 179],
"faces": [
[21, 20, 24],
[24, 31, 21]
],
"name": "left lower snout"
},
{
"color": [215, 193, 179],
"faces": [
[69, 71, 70],
[71, 69, 75]
],
"name": "right lower snout"
},
{
"faces": [[31, 24, 18]],
"name": "left upper snout",
"gradient": "left-upper-snout-gradient"
},
{
"faces": [
[6, 5, 16],
[16, 17, 6]
],
"name": "left forehead",
"gradient": "left-forehead-gradient"
},
{
"faces": [
[24, 32, 33],
[33, 34, 24]
],
"name": "left lower cheek",
"gradient": "left-lower-cheek-gradient"
},
{
"faces": [[5, 4, 35]],
"name": "left top ear",
"gradient": "left-top-ear-gradient"
},
{
"faces": [[75, 68, 71]],
"name": "right upper snout",
"gradient": "right-upper-snout-gradient"
},
{
"faces": [
[58, 67, 40],
[40, 59, 58]
],
"name": "right forhead",
"gradient": "right-forehead-gradient"
},
{
"faces": [
[71, 76, 77],
[77, 78, 71]
],
"name": "right lower cheek",
"gradient": "right-lower-cheek-gradient"
},
{
"faces": [[24, 34, 18]],
"name": "left middle cheek",
"gradient": "left-middle-cheek-gradient"
},
{
"color": [35, 151, 119],
"faces": [
[16, 13, 12],
[12, 17, 16],
[13, 16, 11]
],
"name": "left above eye"
},
{
"faces": [[71, 68, 76]],
"name": "right middle cheek",
"gradient": "right-middle-cheek-gradient"
},
{
"color": [35, 151, 119],
"faces": [
[40, 67, 66],
[66, 65, 40],
[65, 64, 40]
],
"name": "right above eye"
},
{
"color": [22, 22, 22],
"faces": [
[36, 15, 37],
[37, 38, 36],
[31, 39, 22],
[22, 21, 31],
[31, 15, 36],
[36, 39, 31],
[75, 69, 26],
[26, 80, 75],
[75, 80, 38],
[38, 37, 75],
[38, 80, 39],
[39, 36, 38],
[39, 80, 26],
[26, 22, 39]
],
"name": "nose"
},
{
"faces": [
[17, 33, 10],
[17, 18, 34],
[34, 33, 17],
[10, 6, 17]
],
"name": "left upper cheek",
"gradient": "left-upper-cheek-gradient"
},
{
"faces": [
[11, 15, 31],
[31, 18, 11],
[18, 12, 11]
],
"name": "left below eye",
"gradient": "left-below-eye-gradient"
},
{
"faces": [
[14, 16, 40],
[40, 41, 14],
[59, 5, 35],
[35, 79, 59],
[14, 41, 37],
[37, 15, 14],
[5, 59, 40],
[40, 16, 5]
],
"name": "forehead",
"gradient": "forehead-gradient"
},
{
"faces": [
[67, 63, 77],
[67, 77, 76],
[76, 68, 67],
[63, 67, 58]
],
"name": "right upper cheek",
"gradient": "right-upper-cheek-gradient"
},
{
"faces": [
[64, 68, 75],
[75, 37, 64],
[68, 64, 66]
],
"name": "right below eye",
"gradient": "right-below-eye-gradient"
},
{
"faces": [
[35, 4, 42],
[4, 1, 42],
[42, 43, 44],
[44, 35, 42],
[45, 43, 42],
[42, 10, 45],
[30, 32, 24],
[24, 25, 30],
[30, 33, 32],
[33, 30, 10],
[44, 43, 46],
[43, 45, 47],
[47, 46, 43],
[48, 47, 45],
[45, 30, 48],
[30, 45, 10],
[49, 42, 0],
[8, 7, 42],
[50, 42, 7],
[50, 10, 42],
[1, 0, 42],
[42, 9, 8],
[42, 49, 9],
[79, 81, 57],
[57, 81, 56],
[82, 79, 35],
[35, 44, 82],
[81, 79, 82],
[82, 83, 81],
[84, 63, 81],
[81, 83, 84],
[44, 46, 85],
[85, 82, 44],
[52, 73, 71],
[71, 78, 52],
[52, 78, 77],
[77, 63, 52],
[82, 85, 83],
[83, 85, 86],
[86, 84, 83],
[87, 52, 84],
[84, 86, 87],
[52, 63, 84],
[88, 53, 81],
[62, 81, 60],
[89, 60, 81],
[89, 81, 63],
[56, 81, 53],
[81, 62, 61],
[81, 61, 88],
[48, 87, 86],
[86, 47, 48],
[47, 86, 85],
[85, 46, 47],
[48, 30, 52],
[52, 87, 48]
],
"name": "back",
"gradient": "back-gradient"
},
{
"faces": [[57, 59, 79]],
"name": "right top ear",
"gradient": "right-top-ear-gradient"
},
{
"faces": [[64, 41, 40]],
"name": "right inner upper eye",
"gradient": "right-inner-eye-gradient"
}
],
"gradients": {
"forehead-gradient": {
"type": "linear",
"stops": [
{
"stop-color": "#23FE4A"
},
{
"offset": 1,
"stop-color": "#BAD8EF"
}
],
"x1": "50%",
"y1": "20.232164948453608%",
"x2": "50%",
"y2": "74.87123711340206%",
"gradientUnits": "userSpaceOnUse"
},
"right-upper-cheek-gradient": {
"type": "linear",
"stops": [
{
"stop-color": "#20B475"
},
{
"offset": 1,
"stop-color": "#70BDCE"
}
],
"x1": "77.19501199040768%",
"y1": "44.68123711340206%",
"x2": "77.19501199040768%",
"y2": "68.2861855670103%",
"gradientUnits": "userSpaceOnUse"
},
"left-upper-cheek-gradient": {
"type": "linear",
"stops": [
{
"stop-color": "#20B475"
},
{
"offset": 1,
"stop-color": "#70BDCE"
}
],
"x1": "22.820719424460435%",
"y1": "44.68123711340206%",
"x2": "22.820719424460435%",
"y2": "68.2861855670103%",
"gradientUnits": "userSpaceOnUse"
},
"right-below-eye-gradient": {
"type": "linear",
"stops": [
{
"stop-color": "#85BBE1"
},
{
"offset": 1,
"stop-color": "#7CCACA"
}
],
"x1": "54.34676258992806%",
"y1": "68.26917525773197%",
"x2": "65.3001438848921%",
"y2": "68.26917525773197%",
"gradientUnits": "userSpaceOnUse"
},
"left-below-eye-gradient": {
"type": "linear",
"stops": [
{
"stop-color": "#7CCACA"
},
{
"offset": 1,
"stop-color": "#85BBE1"
}
],
"x1": "34.731223021582736%",
"y1": "68.26917525773197%",
"x2": "45.65323741007194%",
"y2": "68.26917525773197%",
"gradientUnits": "userSpaceOnUse"
},
"right-ear-gradient": {
"type": "linear",
"stops": [
{
"stop-color": "#074F1E"
},
{
"offset": 0.4286,
"stop-color": "#05541C"
},
{
"offset": 0.62,
"stop-color": "#006A13"
},
{
"offset": 1,
"stop-color": "#007514"
}
],
"x1": "61.443549160671466%",
"y1": "44.51773195876289%",
"x2": "93.802206235012%",
"y2": "24.439072164948456%",
"gradientUnits": "userSpaceOnUse"
},
"left-ear-gradient": {
"type": "linear",
"stops": [
{
"stop-color": "#074F1E"
},
{
"offset": 0.4286,
"stop-color": "#05541C"
},
{
"offset": 0.62,
"stop-color": "#006A13"
},
{
"offset": 1,
"stop-color": "#007514"
}
],
"x1": "32.7432134292566%",
"y1": "44.33329896907217%",
"x2": "4.853390887290168%",
"y2": "19.18181443298969%",
"gradientUnits": "userSpaceOnUse"
},
"left-outer-eye-gradient": {
"type": "linear",
"stops": [
{
"stop-color": "#43C3A2"
},
{
"offset": 1,
"stop-color": "#4FAFC0"
},
{
"offset": 1,
"stop-color": "#4FAFC0"
}
],
"x1": "27.575539568345324%",
"y1": "60.519278350515464%",
"x2": "34.982350119904076%",
"y2": "60.519278350515464%",
"gradientUnits": "userSpaceOnUse"
},
"right-outer-eye-gradient": {
"type": "linear",
"stops": [
{
"stop-color": "#4FAFC0"
},
{
"offset": 1,
"stop-color": "#43C3A2"
}
],
"x1": "65.01764988009592%",
"y1": "60.519278350515464%",
"x2": "72.42446043165468%",
"y2": "60.519278350515464%",
"gradientUnits": "userSpaceOnUse"
},
"right-lower-cheek-gradient": {
"type": "linear",
"stops": [
{
"stop-color": "#59ADCB"
},
{
"offset": 1,
"stop-color": "#436CC8"
}
],
"x1": "77.93247002398083%",
"y1": "68.15113402061857%",
"x2": "77.93247002398083%",
"y2": "86.82577319587631%",
"gradientUnits": "userSpaceOnUse"
},
"left-lower-cheek-gradient": {
"type": "linear",
"stops": [
{
"stop-color": "#59ADCB"
},
{
"offset": 1,
"stop-color": "#436CC8"
}
],
"x1": "22.083165467625896%",
"y1": "68.15113402061857%",
"x2": "22.083165467625896%",
"y2": "86.82577319587631%",
"gradientUnits": "userSpaceOnUse"
},
"left-top-ear-gradient": {
"type": "linear",
"stops": [
{
"stop-color": "#0ED54A"
},
{
"offset": 1,
"stop-color": "#0ED54A"
}
],
"x1": "13.954513189448441%",
"y1": "22.055670103092787%",
"x2": "44.146762589928066%",
"y2": "22.055670103092787%",
"gradientUnits": "userSpaceOnUse"
},
"right-top-ear-gradient": {
"type": "linear",
"stops": [
{
"stop-color": "#0ED54A"
},
{
"offset": 1,
"stop-color": "#11EB36"
}
],
"x1": "55.85333333333334%",
"y1": "22.055670103092787%",
"x2": "86.04556354916068%",
"y2": "22.055670103092787%",
"gradientUnits": "userSpaceOnUse"
},
"left-forehead-gradient": {
"type": "linear",
"stops": [
{
"stop-color": "#15DC5D"
},
{
"offset": 1,
"stop-color": "#48CA9F"
}
],
"x1": "36.3947242206235%",
"y1": "34.11144329896908%",
"x2": "36.3947242206235%",
"y2": "53.59649484536083%",
"gradientUnits": "userSpaceOnUse"
},
"right-forehead-gradient": {
"type": "linear",
"stops": [
{
"stop-color": "#15DC5D"
},
{
"offset": 1,
"stop-color": "#48CA9F"
}
],
"x1": "63.6052757793765%",
"y1": "34.11144329896908%",
"x2": "63.6052757793765%",
"y2": "53.59649484536083%",
"gradientUnits": "userSpaceOnUse"
},
"left-upper-snout-gradient": {
"type": "linear",
"stops": [
{
"stop-color": "#54A8CF"
},
{
"offset": 1,
"stop-color": "#5393E3"
}
],
"x1": "38.829736211031175%",
"y1": "68.28865979381443%",
"x2": "38.829736211031175%",
"y2": "81.55670103092784%",
"gradientUnits": "userSpaceOnUse"
},
"right-upper-snout-gradient": {
"type": "linear",
"stops": [
{
"stop-color": "#54A8CF"
},
{
"offset": 1,
"stop-color": "#5393E3"
}
],
"x1": "61.17026378896883%",
"y1": "68.28865979381443%",
"x2": "61.17026378896883%",
"y2": "81.55670103092784%",
"gradientUnits": "userSpaceOnUse"
},
"right-middle-cheek-gradient": {
"type": "linear",
"stops": [
{
"stop-color": "#32819D"
},
{
"offset": 0.3363,
"stop-color": "#447DCD"
}
],
"x1": "69.9137649880096%",
"y1": "51.063505154639174%",
"x2": "69.9137649880096%",
"y2": "85.81041237113402%",
"gradientUnits": "userSpaceOnUse"
},
"left-middle-cheek-gradient": {
"type": "linear",
"stops": [
{
"stop-color": "#32819D"
},
{
"offset": 0.3363,
"stop-color": "#447DCD"
}
],
"x1": "30.086330935251798%",
"y1": "68.15092783505153%",
"x2": "30.086330935251798%",
"y2": "81.55752577319588%",
"gradientUnits": "userSpaceOnUse"
},
"right-inner-eye-gradient": {
"type": "linear",
"stops": [
{
"stop-color": "#53A9CB"
},
{
"offset": 1,
"stop-color": "#44C0A6"
}
],
"x1": "55.38244604316547%",
"y1": "74.87123711340206%",
"x2": "55.38244604316547%",
"y2": "53.59659793814433%",
"gradientUnits": "userSpaceOnUse"
},
"left-inner-eye-gradient": {
"type": "linear",
"stops": [
{
"stop-color": "#53A9CB"
},
{
"offset": 1,
"stop-color": "#44C0A6"
}
],
"x1": "43.58177458033573%",
"y1": "64.2339175257732%",
"x2": "45.65323741007194%",
"y2": "64.2339175257732%",
"gradientUnits": "userSpaceOnUse"
},
"back-gradient": {
"type": "linear",
"stops": [
{
"stop-color": "#27FC4E"
},
{
"offset": 1,
"stop-color": "#446FC9"
}
],
"x1": "50%",
"y1": "0%",
"x2": "50%",
"y2": "100%",
"gradientUnits": "userSpaceOnUse"
}
},
"positions": [
[111.024597, 52.604599, 46.225899],
[114.025002, 87.673302, 58.9818],
[66.192001, 80.898003, 55.394299],
[72.113297, 35.491798, 30.871401],
[97.804497, 116.560997, 73.978798],
[16.7623, 58.010899, 58.078201],
[52.608898, 30.3641, 42.556099],
[106.881401, 31.945499, 46.9133],
[113.484596, 38.6049, 49.121498],
[108.6633, 43.2332, 46.315399],
[101.216599, 15.9822, 46.308201],
[16.6605, -16.2883, 93.618698],
[40.775002, -10.2288, 85.276398],
[23.926901, -2.5103, 86.736504],
[11.1691, -7.0037, 99.377602],
[9.5692, -34.393902, 141.671997],
[12.596, 7.1655, 88.740997],
[61.180901, 8.8142, 76.996803],
[39.719501, -28.927099, 88.963799],
[13.7962, -68.575699, 132.057007],
[15.2674, -62.32, 129.688004],
[14.8446, -52.6096, 140.113007],
[12.8917, -49.771599, 144.740997],
[35.604198, -71.758003, 81.063904],
[47.462502, -68.606102, 63.369701],
[38.2486, -64.730202, 38.909901],
[-12.8917, -49.771599, 144.740997],
[-13.7962, -68.575699, 132.057007],
[17.802099, -71.758003, 81.063904],
[19.1243, -69.0168, 49.420101],
[38.2486, -66.275597, 17.776199],
[12.8928, -36.703499, 141.671997],
[109.283997, -93.589897, 27.824301],
[122.117996, -36.8894, 35.025002],
[67.7668, -30.197001, 78.417801],
[33.180698, 101.851997, 25.3186],
[9.4063, -35.589802, 150.722],
[-9.5692, -34.393902, 141.671997],
[-9.4063, -35.589802, 150.722],
[11.4565, -37.899399, 150.722],
[-12.596, 7.1655, 88.740997],
[-11.1691, -7.0037, 99.377602],
[70.236504, 62.836201, -3.9475],
[47.263401, 54.293999, -27.414801],
[28.7302, 91.731102, -24.972601],
[69.167603, 6.5862, -12.7757],
[28.7302, 49.1003, -48.3596],
[31.903, 5.692, -47.821999],
[35.075802, -34.432899, -16.280899],
[115.284103, 48.681499, 48.684101],
[110.842796, 28.4821, 49.176201],
[-19.1243, -69.0168, 49.420101],
[-38.2486, -66.275597, 17.776199],
[-111.024597, 52.604599, 46.225899],
[-72.113297, 35.491798, 30.871401],
[-66.192001, 80.898003, 55.394299],
[-114.025002, 87.673302, 58.9818],
[-97.804497, 116.560997, 73.978798],
[-52.608898, 30.3641, 42.556099],
[-16.7623, 58.010899, 58.078201],
[-106.881401, 31.945499, 46.9133],
[-108.6633, 43.2332, 46.315399],
[-113.484596, 38.6049, 49.121498],
[-101.216599, 15.9822, 46.308201],
[-16.6605, -16.2883, 93.618698],
[-23.926901, -2.5103, 86.736504],
[-40.775002, -10.2288, 85.276398],
[-61.180901, 8.8142, 76.996803],
[-39.719501, -28.927099, 88.963799],
[-14.8446, -52.6096, 140.113007],
[-15.2674, -62.32, 129.688004],
[-47.462502, -68.606102, 63.369701],
[-35.604198, -71.758003, 81.063904],
[-38.2486, -64.730202, 38.909901],
[-17.802099, -71.758003, 81.063904],
[-12.8928, -36.703499, 141.671997],
[-67.7668, -30.197001, 78.417801],
[-122.117996, -36.8894, 35.025002],
[-109.283997, -93.589897, 27.824301],
[-33.180698, 101.851997, 25.3186],
[-11.4565, -37.899399, 150.722],
[-70.236504, 62.836201, -3.9475],
[-28.7302, 91.731102, -24.972601],
[-47.263401, 54.293999, -27.414801],
[-69.167603, 6.5862, -12.7757],
[-28.7302, 49.1003, -48.3596],
[-31.903, 5.692, -47.821999],
[-35.075802, -34.432899, -16.280899],
[-115.284103, 48.681499, 48.684101],
[-110.842796, 28.4821, 49.176201]
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 847 B

After

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 861 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

@ -1,130 +1,36 @@
<svg width="35" height="33" viewBox="0 0 35 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M19.5938 17.6064L20.1705 9.73742L22.5185 4.13232H12.4812L14.8292 9.73742L15.4059 17.6064L15.5844 20.0884L15.5981 26.1981H19.4016L19.4153 20.0884L19.5938 17.6064Z" fill="url(#paint0_linear)"/>
<path d="M32.3226 16.0925L25.1413 14.006L27.3108 17.2654L24.0703 23.5387L28.3543 23.4842H34.7255L32.3226 16.0925Z" fill="url(#paint1_linear)"/>
<path d="M9.85875 14.006L2.6775 16.0925L0.28833 23.4842H6.65946L10.9298 23.5387L7.68927 17.2654L9.85875 14.006Z" fill="url(#paint2_linear)"/>
<path d="M20.3078 21.5476L19.4016 26.198L20.0607 26.6481L24.0701 23.5387L24.1937 20.4156L20.3078 21.5476Z" fill="url(#paint3_linear)"/>
<path d="M10.8198 20.4156L10.9297 23.5387L14.9391 26.6481L15.5982 26.198L14.6919 21.5476L10.8198 20.4156Z" fill="url(#paint4_linear)"/>
<path d="M33.8466 10.7329L35 5.16869L33.2699 0L20.0608 9.73731L24.9902 14.1286L32.3225 16.0925L33.9015 14.2514L33.215 13.7604L34.3134 12.7649L33.4759 12.1103L34.5743 11.2784L33.8466 10.7329Z" fill="url(#paint5_linear)"/>
<path d="M0 5.16869L1.16712 10.7329L0.425657 11.2784L1.52413 12.1103L0.686544 12.7649L1.78501 13.7604L1.09847 14.2514L2.67752 16.0925L10.0235 14.1286L14.9392 9.73731L1.73009 0L0 5.16869Z" fill="url(#paint6_linear)"/>
<path d="M7.68921 17.2653L10.8198 20.4156L10.9297 23.5386L7.68921 17.2653Z" fill="url(#paint7_linear)"/>
<path d="M27.3105 17.2653L24.0701 23.5386L24.1936 20.4156L27.3105 17.2653Z" fill="url(#paint8_linear)"/>
<path d="M24.7156 28.8983L20.0608 26.6481L20.4315 29.662L20.3903 30.9303L24.7156 28.8983Z" fill="#D7C1B3"/>
<path d="M10.2844 28.8983L14.6096 30.9303L14.5822 29.662L14.9392 26.6481L10.2844 28.8983Z" fill="#D7C1B3"/>
<path d="M28.3543 23.484L24.7156 28.8982L32.501 31.0256L34.7254 23.484H28.3543Z" fill="url(#paint9_linear)"/>
<path d="M0.28833 23.484L2.499 31.0256L10.2844 28.8982L6.65946 23.484H0.28833Z" fill="url(#paint10_linear)"/>
<path d="M1.72998 0L14.9391 9.73731L12.8657 4.13222L1.72998 0Z" fill="url(#paint11_linear)"/>
<path d="M22.1339 4.13222L20.0605 9.73731L33.2697 0L22.1339 4.13222Z" fill="url(#paint12_linear)"/>
<path d="M9.85869 14.0059L7.68921 17.2653L15.406 17.6063L14.9391 9.7373L9.85869 14.0059Z" fill="url(#paint13_linear)"/>
<path d="M25.141 14.0059L20.0606 9.7373L19.5938 17.6063L27.3105 17.2653L25.141 14.0059Z" fill="url(#paint14_linear)"/>
<path d="M10.2844 28.8983L14.9392 26.6481L10.9298 23.5387L10.2844 28.8983Z" fill="url(#paint15_linear)"/>
<path d="M20.0608 26.6481L24.7156 28.8983L24.0702 23.5387L20.0608 26.6481Z" fill="url(#paint16_linear)"/>
<path d="M10.2844 28.8982L10.9297 23.5386L6.65942 23.484L10.2844 28.8982Z" fill="url(#paint17_linear)"/>
<path d="M24.0701 23.5386L24.7154 28.8982L28.3541 23.484L24.0701 23.5386Z" fill="url(#paint18_linear)"/>
<path d="M27.3107 17.2653L19.594 17.6062L20.308 21.5475L21.4477 19.1745L24.1938 20.4156L27.3107 17.2653Z" fill="url(#paint19_linear)"/>
<path d="M10.8198 20.4156L13.5523 19.1745L14.692 21.5475L15.406 17.6062L7.68921 17.2653L10.8198 20.4156Z" fill="url(#paint20_linear)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.4156 20.0884L20.3081 21.5476L19.5941 17.6063L19.4156 20.0884ZM20.3081 21.5476L19.4156 20.0884L19.4019 26.1981L20.3081 21.5476Z" fill="url(#paint21_linear)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.4061 17.6063L14.6921 21.5476V21.5476L15.5984 26.1981L15.5846 20.0884L14.6921 21.5476L15.5846 20.0884L15.4061 17.6063Z" fill="url(#paint22_linear)"/>
<path d="M20.4151 30.914L20.4315 29.662L20.0883 29.362H14.9117L14.5822 29.662L14.6096 30.9303L10.2844 28.8983L11.7948 30.1257L14.8705 32.2396H20.1295L23.2052 30.1257L24.7156 28.8983L20.4151 30.914Z" fill="#C0AD9E"/>
<path d="M20.0606 26.648L19.4016 26.198H15.5981L14.939 26.648L14.582 29.662L14.9116 29.3619H20.0881L20.4314 29.662L20.0606 26.648Z" fill="#161616" stroke="#161616" stroke-width="0.142167" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M21.4475 19.1746L20.3079 21.5475L24.1937 20.4156L21.4475 19.1746Z" fill="#161616"/>
<path d="M13.5525 19.1746L14.6922 21.5475L10.8201 20.4156L13.5525 19.1746Z" fill="#161616"/>
</g>
<defs>
<linearGradient id="paint0_linear" x1="17.4998" y1="4.13232" x2="17.4998" y2="26.1981" gradientUnits="userSpaceOnUse">
<stop stop-color="#21FF47"/>
<stop offset="1" stop-color="#BCD8F0"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="29.3979" y1="14.006" x2="29.3979" y2="23.5387" gradientUnits="userSpaceOnUse">
<stop stop-color="#1CAD68"/>
<stop offset="1" stop-color="#77BDD4"/>
</linearGradient>
<linearGradient id="paint2_linear" x1="5.60904" y1="14.006" x2="5.60904" y2="23.5387" gradientUnits="userSpaceOnUse">
<stop stop-color="#1CAD68"/>
<stop offset="1" stop-color="#77BDD4"/>
</linearGradient>
<linearGradient id="paint3_linear" x1="21.7977" y1="20.4156" x2="21.7977" y2="26.6481" gradientUnits="userSpaceOnUse">
<stop stop-color="#7BCBC8"/>
<stop offset="1" stop-color="#87B9E6"/>
</linearGradient>
<linearGradient id="paint4_linear" x1="13.209" y1="20.4156" x2="13.209" y2="26.6481" gradientUnits="userSpaceOnUse">
<stop stop-color="#7BCBC8"/>
<stop offset="1" stop-color="#87B9E6"/>
</linearGradient>
<linearGradient id="paint5_linear" x1="27.5304" y1="0" x2="27.5304" y2="16.0925" gradientUnits="userSpaceOnUse">
<stop stop-color="#007715"/>
<stop offset="1" stop-color="#0A3424"/>
</linearGradient>
<linearGradient id="paint6_linear" x1="7.4696" y1="0" x2="7.4696" y2="16.0925" gradientUnits="userSpaceOnUse">
<stop stop-color="#007715"/>
<stop offset="1" stop-color="#0A3424"/>
</linearGradient>
<linearGradient id="paint7_linear" x1="9.30945" y1="17.2653" x2="9.30945" y2="23.5386" gradientUnits="userSpaceOnUse">
<stop stop-color="#42C4A0"/>
<stop offset="1" stop-color="#51ACC7"/>
</linearGradient>
<linearGradient id="paint8_linear" x1="25.6903" y1="17.2653" x2="25.6903" y2="23.5386" gradientUnits="userSpaceOnUse">
<stop stop-color="#42C4A0"/>
<stop offset="1" stop-color="#51ACC7"/>
</linearGradient>
<linearGradient id="paint9_linear" x1="29.7205" y1="23.484" x2="29.7205" y2="31.0256" gradientUnits="userSpaceOnUse">
<stop stop-color="#5BAECB"/>
<stop offset="1" stop-color="#4160C1"/>
</linearGradient>
<linearGradient id="paint10_linear" x1="5.28637" y1="23.484" x2="5.28637" y2="31.0256" gradientUnits="userSpaceOnUse">
<stop stop-color="#5BAECB"/>
<stop offset="1" stop-color="#4160C1"/>
</linearGradient>
<linearGradient id="paint11_linear" x1="8.33453" y1="0" x2="8.33453" y2="9.73731" gradientUnits="userSpaceOnUse">
<stop stop-color="#01EA28"/>
<stop offset="1" stop-color="#10D24F"/>
</linearGradient>
<linearGradient id="paint12_linear" x1="26.6651" y1="0" x2="26.6651" y2="9.73731" gradientUnits="userSpaceOnUse">
<stop stop-color="#01EA28"/>
<stop offset="1" stop-color="#10D24F"/>
</linearGradient>
<linearGradient id="paint13_linear" x1="11.5476" y1="9.7373" x2="11.5476" y2="17.6063" gradientUnits="userSpaceOnUse">
<stop stop-color="#12DB56"/>
<stop offset="1" stop-color="#4CC9A3"/>
</linearGradient>
<linearGradient id="paint14_linear" x1="23.4521" y1="9.7373" x2="23.4521" y2="17.6063" gradientUnits="userSpaceOnUse">
<stop stop-color="#12DB56"/>
<stop offset="1" stop-color="#4CC9A3"/>
</linearGradient>
<linearGradient id="paint15_linear" x1="12.6118" y1="23.5387" x2="12.6118" y2="28.8983" gradientUnits="userSpaceOnUse">
<stop stop-color="#5BAECB"/>
<stop offset="1" stop-color="#4160C1"/>
</linearGradient>
<linearGradient id="paint16_linear" x1="22.3882" y1="23.5387" x2="22.3882" y2="28.8983" gradientUnits="userSpaceOnUse">
<stop stop-color="#5BAECB"/>
<stop offset="1" stop-color="#4160C1"/>
</linearGradient>
<linearGradient id="paint17_linear" x1="8.79458" y1="23.484" x2="8.79458" y2="28.8982" gradientUnits="userSpaceOnUse">
<stop stop-color="#32809D"/>
<stop offset="1" stop-color="#477DD5"/>
</linearGradient>
<linearGradient id="paint18_linear" x1="26.2121" y1="23.484" x2="26.2121" y2="28.8982" gradientUnits="userSpaceOnUse">
<stop stop-color="#32809D"/>
<stop offset="1" stop-color="#477DD5"/>
</linearGradient>
<linearGradient id="paint19_linear" x1="23.4524" y1="17.2653" x2="23.4524" y2="21.5475" gradientUnits="userSpaceOnUse">
<stop stop-color="#239777"/>
<stop offset="1" stop-color="#2C8A8D"/>
</linearGradient>
<linearGradient id="paint20_linear" x1="11.5476" y1="17.2653" x2="11.5476" y2="21.5475" gradientUnits="userSpaceOnUse">
<stop stop-color="#239777"/>
<stop offset="1" stop-color="#2C8A8D"/>
</linearGradient>
<linearGradient id="paint21_linear" x1="19.855" y1="17.6063" x2="19.855" y2="26.1981" gradientUnits="userSpaceOnUse">
<stop stop-color="#42C4A0"/>
<stop offset="1" stop-color="#51ACC7"/>
</linearGradient>
<linearGradient id="paint22_linear" x1="15.1453" y1="17.6063" x2="15.1453" y2="26.1981" gradientUnits="userSpaceOnUse">
<stop stop-color="#42C4A0"/>
<stop offset="1" stop-color="#51ACC7"/>
</linearGradient>
<clipPath id="clip0">
<rect width="35" height="33" fill="white"/>
</clipPath>
</defs>
<svg width="158" height="129" viewBox="0 0 158 129" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M128.133 1L75.8844 39.6606L85.6003 16.8766L128.133 1Z" fill="#E17726" stroke="#E17726" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.61512 1L59.3988 40.0214L50.148 16.8766L7.61512 1Z" fill="#E27625" stroke="#E27625" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M109.322 90.641L95.42 111.879L125.188 120.075L133.715 91.1049L109.322 90.641Z" fill="#E27625" stroke="#E27625" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.08499 91.1049L10.5606 120.075L40.2767 111.879L26.4264 90.641L2.08499 91.1049Z" fill="#E27625" stroke="#E27625" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M38.6748 54.7639L30.406 67.2384L59.8637 68.5786L58.8818 36.877L38.6748 54.7639Z" fill="#E27625" stroke="#E27625" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M97.0738 54.7639L76.5567 36.5161L75.8848 68.5786L105.343 67.2384L97.0738 54.7639Z" fill="#E27625" stroke="#E27625" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M40.2766 111.879L58.1063 103.27L42.7573 91.3112L40.2766 111.879Z" fill="#E27625" stroke="#E27625" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M77.6414 103.27L95.4195 111.879L92.9905 91.3112L77.6414 103.27Z" fill="#E27625" stroke="#E27625" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M95.4198 111.879L77.6417 103.27L79.0888 114.817L78.9337 119.714L95.4198 111.879Z" fill="#D5BFB2" stroke="#D5BFB2" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M40.2772 111.879L56.8149 119.714L56.7115 114.817L58.1069 103.27L40.2772 111.879Z" fill="#D5BFB2" stroke="#D5BFB2" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M57.1248 83.682L42.3442 79.352L52.7837 74.5581L57.1248 83.682Z" fill="#233447" stroke="#233447" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M78.6233 83.682L82.9644 74.5581L93.4555 79.352L78.6233 83.682Z" fill="#233447" stroke="#233447" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M40.2768 111.879L42.8609 90.641L26.4265 91.1049L40.2768 111.879Z" fill="#CC6228" stroke="#CC6228" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M92.8869 90.641L95.4193 111.879L109.321 91.1049L92.8869 90.641Z" fill="#CC6228" stroke="#CC6228" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M105.342 67.2385L75.8843 68.5788L78.6233 83.6821L82.9645 74.5583L93.4556 79.3522L105.342 67.2385Z" fill="#CC6228" stroke="#CC6228" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M42.3441 79.3522L52.7835 74.5583L57.1246 83.6821L59.8637 68.5788L30.4059 67.2385L42.3441 79.3522Z" fill="#CC6228" stroke="#CC6228" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M30.406 67.2385L42.7576 91.3112L42.3442 79.3522L30.406 67.2385Z" fill="#E27525" stroke="#E27525" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M93.4563 79.3522L92.9912 91.3112L105.343 67.2385L93.4563 79.3522Z" fill="#E27525" stroke="#E27525" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M59.864 68.5789L57.1249 83.6823L60.5875 101.518L61.3627 78.012L59.864 68.5789Z" fill="#E27525" stroke="#E27525" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M75.8848 68.5789L74.4378 77.9605L75.1613 101.518L78.6239 83.6823L75.8848 68.5789Z" fill="#E27525" stroke="#E27525" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M78.624 83.682L75.1614 101.517L77.6421 103.27L92.9912 91.311L93.4563 79.3521L78.624 83.682Z" fill="#F5841F" stroke="#F5841F" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M42.3442 79.3521L42.7577 91.311L58.1067 103.27L60.5874 101.517L57.1248 83.682L42.3442 79.3521Z" fill="#F5841F" stroke="#F5841F" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M78.9341 119.714L79.0891 114.817L77.7454 113.683H58.0035L56.7115 114.817L56.8149 119.714L40.2772 111.879L46.0654 116.621L57.7968 124.714H77.9005L89.6836 116.621L95.4201 111.879L78.9341 119.714Z" fill="#C0AC9D" stroke="#C0AC9D" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M77.6417 103.27L75.161 101.517H60.5871L58.1065 103.27L56.7111 114.817L58.0031 113.683H77.745L79.0887 114.817L77.6417 103.27Z" fill="#161616" stroke="#161616" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M130.356 42.1864L134.749 20.8458L128.133 1L77.6418 38.3719L97.0736 54.764L124.516 62.7538L130.562 55.6918L127.927 53.7846L132.113 49.9701L128.909 47.4958L133.095 44.2998L130.356 42.1864Z" fill="#763E1A" stroke="#763E1A" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1 20.8458L5.44451 42.1864L2.60209 44.2998L6.83988 47.4958L3.6357 49.9701L7.8218 53.7846L5.18611 55.6918L11.2327 62.7538L38.675 54.764L58.1068 38.3719L7.61508 1L1 20.8458Z" fill="#763E1A" stroke="#763E1A" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M124.516 62.7538L97.0739 54.7639L105.343 67.2384L92.9911 91.311L109.322 91.1048H133.715L124.516 62.7538Z" fill="#F5841F" stroke="#F5841F" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M38.6747 54.7639L11.2325 62.7538L2.08507 91.1048H26.4265L42.7575 91.311L30.4059 67.2384L38.6747 54.7639Z" fill="#F5841F" stroke="#F5841F" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M75.8851 68.5787L77.6423 38.3719L85.601 16.8766H50.1483L58.1071 38.3719L59.8642 68.5787L60.5361 78.0634L60.5878 101.517H75.1616L75.2133 78.0634L75.8851 68.5787Z" fill="#F5841F" stroke="#F5841F" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M149.801 129H91.7084C87.3698 129 83.8491 125.479 83.8491 121.141V104.024C83.8491 99.6853 87.3698 96.1646 91.7084 96.1646H149.801C154.139 96.1646 157.66 99.6853 157.66 104.024V121.141C157.66 125.485 154.139 129 149.801 129Z" fill="#037DD6"/>
<path d="M96.0903 105.317H101.808C102.42 105.317 103.007 105.436 103.556 105.673C104.106 105.91 104.586 106.229 105.005 106.634C105.417 107.04 105.741 107.521 105.978 108.07C106.216 108.62 106.334 109.206 106.334 109.837C106.334 110.38 106.234 110.867 106.035 111.304C105.835 111.741 105.573 112.122 105.242 112.452C105.885 112.783 106.403 113.258 106.809 113.888C107.208 114.519 107.408 115.237 107.408 116.054C107.408 116.685 107.289 117.265 107.052 117.808C106.815 118.352 106.49 118.826 106.078 119.232C105.666 119.638 105.173 119.956 104.611 120.187C104.049 120.418 103.438 120.53 102.782 120.53H96.0965V105.317H96.0903ZM101.421 111.466C101.877 111.466 102.258 111.323 102.557 111.029C102.857 110.736 103.007 110.386 103.007 109.968C103.007 109.556 102.857 109.194 102.557 108.888C102.258 108.582 101.877 108.426 101.421 108.426H99.4924V111.466H101.421ZM102.495 117.396C102.951 117.396 103.332 117.253 103.631 116.959C103.931 116.666 104.081 116.304 104.081 115.88C104.081 115.468 103.925 115.105 103.619 114.806C103.313 114.506 102.932 114.356 102.47 114.356H99.4924V117.396H102.495Z" fill="#FCFCFC"/>
<path d="M109.693 105.316H119.331V108.575H113.095V111.297H118.045V114.555H113.095V117.277H119.331V120.536H109.693V105.316Z" fill="#FCFCFC"/>
<path d="M124.663 108.568H120.786V105.31H132.01V108.568H128.09V120.523H124.663V108.568Z" fill="#FCFCFC"/>
<path d="M137.428 105.316H140.512L147.023 120.523H143.465L142.117 117.308H135.818L134.47 120.523H130.911L137.428 105.316ZM137.048 114.356H140.881L138.977 109.792L137.048 114.356Z" fill="#FCFCFC"/>
</svg>

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

@ -15,6 +15,7 @@ import {
ENVIRONMENT_TYPE_POPUP,
ENVIRONMENT_TYPE_NOTIFICATION,
ENVIRONMENT_TYPE_FULLSCREEN,
EXTENSION_MESSAGES,
PLATFORM_FIREFOX,
} from '../../shared/constants/app';
import { SECOND } from '../../shared/constants/time';
@ -25,6 +26,7 @@ import {
EVENT_NAMES,
TRAITS,
} from '../../shared/constants/metametrics';
import { checkForLastErrorAndLog } from '../../shared/modules/browser-runtime.utils';
import { isManifestV3 } from '../../shared/modules/mv3.utils';
import { maskObject } from '../../shared/modules/object.utils';
import migrations from './migrations';
@ -79,7 +81,7 @@ const localStore = inTest ? new ReadOnlyNetworkStore() : new LocalStore();
let versionedData;
if (inTest || process.env.METAMASK_DEBUG) {
global.metamaskGetState = localStore.get.bind(localStore);
global.stateHooks.metamaskGetState = localStore.get.bind(localStore);
}
const phishingPageUrl = new URL(process.env.PHISHING_WARNING_PAGE_URL);
@ -88,19 +90,73 @@ const ONE_SECOND_IN_MILLISECONDS = 1_000;
// Timeout for initializing phishing warning page.
const PHISHING_WARNING_PAGE_TIMEOUT = ONE_SECOND_IN_MILLISECONDS;
const ACK_KEEP_ALIVE_MESSAGE = 'ACK_KEEP_ALIVE_MESSAGE';
const WORKER_KEEP_ALIVE_MESSAGE = 'WORKER_KEEP_ALIVE_MESSAGE';
/**
* In case of MV3 we attach a "onConnect" event listener as soon as the application is initialised.
* Reason is that in case of MV3 a delay in doing this was resulting in missing first connect event after service worker is re-activated.
*
* @param remotePort
*/
const initApp = async (remotePort) => {
browser.runtime.onConnect.removeListener(initApp);
await initialize(remotePort);
log.info('MetaMask initialization complete.');
};
/**
* Sends a message to the dapp(s) content script to signal it can connect to MetaMask background as
* the backend is not active. It is required to re-connect dapps after service worker re-activates.
* For non-dapp pages, the message will be sent and ignored.
*/
const sendReadyMessageToTabs = async () => {
const tabs = await browser.tabs
.query({
/**
* Only query tabs that our extension can run in. To do this, we query for all URLs that our
* extension can inject scripts in, which is by using the "<all_urls>" value and __without__
* the "tabs" manifest permission. If we included the "tabs" permission, this would also fetch
* URLs that we'd not be able to inject in, e.g. chrome://pages, chrome://extension, which
* is not what we'd want.
*
* You might be wondering, how does the "url" param work without the "tabs" permission?
*
* @see {@link https://bugs.chromium.org/p/chromium/issues/detail?id=661311#c1}
* "If the extension has access to inject scripts into Tab, then we can return the url
* of Tab (because the extension could just inject a script to message the location.href)."
*/
url: '<all_urls>',
windowType: 'normal',
})
.then((result) => {
checkForLastErrorAndLog();
return result;
})
.catch(() => {
checkForLastErrorAndLog();
});
/** @todo we should only sendMessage to dapp tabs, not all tabs. */
for (const tab of tabs) {
browser.tabs
.sendMessage(tab.id, {
name: EXTENSION_MESSAGES.READY,
})
.then(() => {
checkForLastErrorAndLog();
})
.catch(() => {
// An error may happen if the contentscript is blocked from loading,
// and thus there is no runtime.onMessage handler to listen to the message.
checkForLastErrorAndLog();
});
}
};
if (isManifestV3) {
browser.runtime.onConnect.addListener(initApp);
sendReadyMessageToTabs();
} else {
// initialization flow
initialize().catch(log.error);
@ -439,6 +495,14 @@ function setupController(initState, initLangCode, remoteSourcePort) {
// This ensures that UI is initialised only after background is ready
// It fixes the issue of blank screen coming when extension is loaded, the issue is very frequent in MV3
remotePort.postMessage({ name: 'CONNECTION_READY' });
// If we get a WORKER_KEEP_ALIVE message, we respond with an ACK
remotePort.onMessage.addListener((message) => {
if (message.name === WORKER_KEEP_ALIVE_MESSAGE) {
// To test un-comment this line and wait for 1 minute. An error should be shown on MetaMask UI.
remotePort.postMessage({ name: ACK_KEEP_ALIVE_MESSAGE });
}
});
}
if (processName === ENVIRONMENT_TYPE_POPUP) {
@ -742,7 +806,7 @@ browser.runtime.onInstalled.addListener(({ reason }) => {
});
function setupSentryGetStateGlobal(store) {
global.sentryHooks.getSentryState = function () {
global.stateHooks.getSentryState = function () {
const fullState = store.getState();
const debugState = maskObject({ metamask: fullState }, SENTRY_STATE);
return {

@ -5,6 +5,11 @@ import browser from 'webextension-polyfill';
import PortStream from 'extension-port-stream';
import { obj as createThoughStream } from 'through2';
import { EXTENSION_MESSAGES, MESSAGE_TYPE } from '../../shared/constants/app';
import {
checkForLastError,
checkForLastErrorAndWarn,
} from '../../shared/modules/browser-runtime.utils';
import { isManifestV3 } from '../../shared/modules/mv3.utils';
import shouldInjectProvider from '../../shared/modules/provider-injection';
@ -44,9 +49,6 @@ let legacyExtMux,
legacyPagePublicConfigChannel,
notificationTransformStream;
const WORKER_KEEP_ALIVE_INTERVAL = 1000;
const WORKER_KEEP_ALIVE_MESSAGE = 'WORKER_KEEP_ALIVE_MESSAGE';
const phishingPageUrl = new URL(process.env.PHISHING_WARNING_PAGE_URL);
let phishingExtChannel,
@ -82,6 +84,51 @@ function injectScript(content) {
}
}
/**
* SERVICE WORKER LOGIC
*/
const WORKER_KEEP_ALIVE_INTERVAL = 1000;
const WORKER_KEEP_ALIVE_MESSAGE = 'WORKER_KEEP_ALIVE_MESSAGE';
const TIME_45_MIN_IN_MS = 45 * 60 * 1000;
/**
* Don't run the keep-worker-alive logic for JSON-RPC methods called on initial load.
* This is to prevent the service worker from being kept alive when accounts are not
* connected to the dapp or when the user is not interacting with the extension.
* The keep-alive logic should not work for non-dapp pages.
*/
const IGNORE_INIT_METHODS_FOR_KEEP_ALIVE = [
MESSAGE_TYPE.GET_PROVIDER_STATE,
MESSAGE_TYPE.SEND_METADATA,
];
let keepAliveInterval;
let keepAliveTimer;
/**
* Running this method will ensure the service worker is kept alive for 45 minutes.
* The first message is sent immediately and subsequent messages are sent at an
* interval of WORKER_KEEP_ALIVE_INTERVAL.
*/
const runWorkerKeepAliveInterval = () => {
clearTimeout(keepAliveTimer);
keepAliveTimer = setTimeout(() => {
clearInterval(keepAliveInterval);
}, TIME_45_MIN_IN_MS);
clearInterval(keepAliveInterval);
browser.runtime.sendMessage({ name: WORKER_KEEP_ALIVE_MESSAGE });
keepAliveInterval = setInterval(() => {
if (browser.runtime.id) {
browser.runtime.sendMessage({ name: WORKER_KEEP_ALIVE_MESSAGE });
}
}, WORKER_KEEP_ALIVE_INTERVAL);
};
/**
* PHISHING STREAM LOGIC
*/
@ -93,6 +140,14 @@ function setupPhishingPageStreams() {
target: PHISHING_WARNING_PAGE,
});
if (isManifestV3) {
phishingPageStream.on('data', ({ data: { method } }) => {
if (!IGNORE_INIT_METHODS_FOR_KEEP_ALIVE.includes(method)) {
runWorkerKeepAliveInterval();
}
});
}
// create and connect channel muxers
// so we can handle the channels individually
phishingPageMux = new ObjectMultiplex();
@ -142,6 +197,9 @@ const setupPhishingExtStreams = () => {
error,
),
);
// eslint-disable-next-line no-use-before-define
phishingExtPort.onDisconnect.addListener(onDisconnectDestroyPhishingStreams);
};
/** Destroys all of the phishing extension streams */
@ -153,19 +211,42 @@ const destroyPhishingExtStreams = () => {
phishingExtChannel.removeAllListeners();
phishingExtChannel.destroy();
phishingExtStream = null;
};
/**
* Resets the extension stream with new streams to channel with the phishing page streams,
* and creates a new event listener to the reestablished extension port.
* This listener destroys the phishing extension streams when the extension port is disconnected,
* so that streams may be re-established later the phishing extension port is reconnected.
*/
const resetPhishingStreamAndListeners = () => {
phishingExtPort.onDisconnect.removeListener(resetPhishingStreamAndListeners);
const onDisconnectDestroyPhishingStreams = () => {
checkForLastErrorAndWarn();
phishingExtPort.onDisconnect.removeListener(
onDisconnectDestroyPhishingStreams,
);
destroyPhishingExtStreams();
setupPhishingExtStreams();
};
phishingExtPort.onDisconnect.addListener(resetPhishingStreamAndListeners);
/**
* When the extension background is loaded it sends the EXTENSION_MESSAGES.READY message to the browser tabs.
* This listener/callback receives the message to set up the streams after service worker in-activity.
*
* @param {object} msg
* @param {string} msg.name - custom property and name to identify the message received
* @returns {Promise|undefined}
*/
const onMessageSetUpPhishingStreams = (msg) => {
if (msg.name === EXTENSION_MESSAGES.READY) {
if (!phishingExtStream) {
setupPhishingExtStreams();
}
return Promise.resolve(
`MetaMask: handled "${EXTENSION_MESSAGES.READY}" for phishing streams`,
);
}
return undefined;
};
/**
@ -177,7 +258,7 @@ const initPhishingStreams = () => {
setupPhishingPageStreams();
setupPhishingExtStreams();
phishingExtPort.onDisconnect.addListener(resetPhishingStreamAndListeners);
browser.runtime.onMessage.addListener(onMessageSetUpPhishingStreams);
};
/**
@ -191,6 +272,14 @@ const setupPageStreams = () => {
target: INPAGE,
});
if (isManifestV3) {
pageStream.on('data', ({ data: { method } }) => {
if (!IGNORE_INIT_METHODS_FOR_KEEP_ALIVE.includes(method)) {
runWorkerKeepAliveInterval();
}
});
}
// create and connect channel muxers
// so we can handle the channels individually
pageMux = new ObjectMultiplex();
@ -231,7 +320,8 @@ const setupExtensionStreams = () => {
extensionPhishingStream = extensionMux.createStream('phishing');
extensionPhishingStream.once('data', redirectToPhishingWarning);
notifyInpageOfExtensionStreamConnect();
// eslint-disable-next-line no-use-before-define
extensionPort.onDisconnect.addListener(onDisconnectDestroyStreams);
};
/** Destroys all of the extension streams */
@ -243,10 +333,13 @@ const destroyExtensionStreams = () => {
extensionChannel.removeAllListeners();
extensionChannel.destroy();
extensionStream = null;
};
/**
* LEGACY STREAM LOGIC
* TODO:LegacyProvider: Delete
*/
// TODO:LegacyProvider: Delete
@ -256,6 +349,14 @@ const setupLegacyPageStreams = () => {
target: LEGACY_INPAGE,
});
if (isManifestV3) {
legacyPageStream.on('data', ({ data: { method } }) => {
if (!IGNORE_INIT_METHODS_FOR_KEEP_ALIVE.includes(method)) {
runWorkerKeepAliveInterval();
}
});
}
legacyPageMux = new ObjectMultiplex();
legacyPageMux.setMaxListeners(25);
@ -331,19 +432,47 @@ const destroyLegacyExtensionStreams = () => {
};
/**
* Resets the extension stream with new streams to channel with the in page streams,
* and creates a new event listener to the reestablished extension port.
* When the extension background is loaded it sends the EXTENSION_MESSAGES.READY message to the browser tabs.
* This listener/callback receives the message to set up the streams after service worker in-activity.
*
* @param {object} msg
* @param {string} msg.name - custom property and name to identify the message received
* @returns {Promise|undefined}
*/
const resetStreamAndListeners = () => {
extensionPort.onDisconnect.removeListener(resetStreamAndListeners);
const onMessageSetUpExtensionStreams = (msg) => {
if (msg.name === EXTENSION_MESSAGES.READY) {
if (!extensionStream) {
setupExtensionStreams();
setupLegacyExtensionStreams();
}
return Promise.resolve(`MetaMask: handled ${EXTENSION_MESSAGES.READY}`);
}
return undefined;
};
destroyExtensionStreams();
setupExtensionStreams();
/**
* This listener destroys the extension streams when the extension port is disconnected,
* so that streams may be re-established later when the extension port is reconnected.
*/
const onDisconnectDestroyStreams = () => {
const err = checkForLastError();
extensionPort.onDisconnect.removeListener(onDisconnectDestroyStreams);
destroyExtensionStreams();
destroyLegacyExtensionStreams();
setupLegacyExtensionStreams();
extensionPort.onDisconnect.addListener(resetStreamAndListeners);
/**
* If an error is found, reset the streams. When running two or more dapps, resetting the service
* worker may cause the error, "Error: Could not establish connection. Receiving end does not
* exist.", due to a race-condition. The disconnect event may be called by runtime.connect which
* may cause issues. We suspect that this is a chromium bug as this event should only be called
* once the port and connections are ready. Delay time is arbitrary.
*/
if (err) {
console.warn(`${err} Resetting the streams.`);
setTimeout(setupExtensionStreams, 1000);
}
};
/**
@ -353,13 +482,12 @@ const resetStreamAndListeners = () => {
*/
const initStreams = () => {
setupPageStreams();
setupExtensionStreams();
// TODO:LegacyProvider: Delete
setupLegacyPageStreams();
setupExtensionStreams();
setupLegacyExtensionStreams();
extensionPort.onDisconnect.addListener(resetStreamAndListeners);
browser.runtime.onMessage.addListener(onMessageSetUpExtensionStreams);
};
// TODO:LegacyProvider: Delete
@ -389,26 +517,6 @@ function logStreamDisconnectWarning(remoteLabel, error) {
);
}
/**
* The function send message to inpage to notify it of extension stream connection
*/
function notifyInpageOfExtensionStreamConnect() {
window.postMessage(
{
target: INPAGE, // the post-message-stream "target"
data: {
// this object gets passed to obj-multiplex
name: PROVIDER, // the obj-multiplex channel name
data: {
jsonrpc: '2.0',
method: 'METAMASK_EXTENSION_STREAM_CONNECT',
},
},
},
window.location.origin,
);
}
/**
* This function must ONLY be called in pump destruction/close callbacks.
* Notifies the inpage context that streams have failed, via window.postMessage.
@ -446,12 +554,6 @@ function redirectToPhishingWarning(data = {}) {
window.location.href = `${baseUrl}#${querystring}`;
}
const initKeepWorkerAlive = () => {
setInterval(() => {
browser.runtime.sendMessage({ name: WORKER_KEEP_ALIVE_MESSAGE });
}, WORKER_KEEP_ALIVE_INTERVAL);
};
const start = () => {
const isDetectedPhishingSite =
window.location.origin === phishingPageUrl.origin &&
@ -463,9 +565,7 @@ const start = () => {
}
if (shouldInjectProvider()) {
if (isManifestV3) {
initKeepWorkerAlive();
} else {
if (!isManifestV3) {
injectScript(inpageBundle);
}
initStreams();

@ -4,6 +4,7 @@ import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller';
import { MINUTE } from '../../../shared/constants/time';
import { AUTO_LOCK_TIMEOUT_ALARM } from '../../../shared/constants/alarms';
import { isManifestV3 } from '../../../shared/modules/mv3.utils';
import { isBeta } from '../../../ui/helpers/utils/build-types';
export default class AppStateController extends EventEmitter {
/**
@ -36,6 +37,7 @@ export default class AppStateController extends EventEmitter {
enableEIP1559V2NoticeDismissed: false,
showTestnetMessageInDropdown: true,
showPortfolioTooltip: true,
showBetaHeader: isBeta(),
trezorModel: null,
...initState,
qrHardware: {},
@ -191,11 +193,9 @@ export default class AppStateController extends EventEmitter {
const { timeoutMinutes } = this.store.getState();
if (this.timer) {
if (isManifestV3) {
chrome.alarms.clear(AUTO_LOCK_TIMEOUT_ALARM);
} else {
clearTimeout(this.timer);
}
clearTimeout(this.timer);
} else if (isManifestV3) {
chrome.alarms.clear(AUTO_LOCK_TIMEOUT_ALARM);
}
if (!timeoutMinutes) {
@ -207,16 +207,11 @@ export default class AppStateController extends EventEmitter {
delayInMinutes: timeoutMinutes,
periodInMinutes: timeoutMinutes,
});
chrome.alarms.onAlarm.addListener(() => {
chrome.alarms.getAll((alarms) => {
const hasAlarm = alarms.find(
(alarm) => alarm.name === AUTO_LOCK_TIMEOUT_ALARM,
);
if (hasAlarm) {
this.onInactiveTimeout();
chrome.alarms.clear(AUTO_LOCK_TIMEOUT_ALARM);
}
});
chrome.alarms.onAlarm.addListener((alarmInfo) => {
if (alarmInfo.name === AUTO_LOCK_TIMEOUT_ALARM) {
this.onInactiveTimeout();
chrome.alarms.clear(AUTO_LOCK_TIMEOUT_ALARM);
}
});
} else {
this.timer = setTimeout(
@ -291,6 +286,15 @@ export default class AppStateController extends EventEmitter {
this.store.updateState({ showPortfolioTooltip });
}
/**
* Sets whether the beta notification heading on the home page
*
* @param showBetaHeader
*/
setShowBetaHeader(showBetaHeader) {
this.store.updateState({ showBetaHeader });
}
/**
* Sets a property indicating the model of the user's Trezor hardware wallet
*

@ -20,7 +20,7 @@ import {
import { SECOND } from '../../../shared/constants/time';
import { isManifestV3 } from '../../../shared/modules/mv3.utils';
import { METAMETRICS_FINALIZE_EVENT_FRAGMENT_ALARM } from '../../../shared/constants/alarms';
import { checkAlarmExists } from '../lib/util';
import { checkAlarmExists, generateRandomId, isValidDate } from '../lib/util';
const EXTENSION_UNINSTALL_URL = 'https://metamask.io/uninstalled';
@ -32,6 +32,22 @@ const defaultCaptureException = (err) => {
});
};
// The function is used to build a unique messageId for segment messages
// It uses actionId and uniqueIdentifier from event if present
const buildUniqueMessageId = (args) => {
let messageId = '';
if (args.uniqueIdentifier) {
messageId += `${args.uniqueIdentifier}-`;
}
if (args.actionId) {
messageId += args.actionId;
}
if (messageId.length) {
return messageId;
}
return generateRandomId();
};
const exceptionsToFilter = {
[`You must pass either an "anonymousId" or a "userId".`]: true,
};
@ -60,6 +76,8 @@ const exceptionsToFilter = {
* @property {Array} [eventsBeforeMetricsOptIn] - Array of queued events added before
* a user opts into metrics.
* @property {object} [traits] - Traits that are not derived from other state keys.
* @property {Record<string any>} [previousUserTraits] - The user traits the last
* time they were computed.
*/
export default class MetaMetricsController {
@ -110,6 +128,7 @@ export default class MetaMetricsController {
this.environment = environment;
const abandonedFragments = omitBy(initState?.fragments, 'persist');
const segmentApiCalls = initState?.segmentApiCalls || {};
this.store = new ObservableStore({
participateInMetaMetrics: null,
@ -120,6 +139,9 @@ export default class MetaMetricsController {
fragments: {
...initState?.fragments,
},
segmentApiCalls: {
...segmentApiCalls,
},
});
preferencesStore.subscribe(({ currentLocale }) => {
@ -142,6 +164,15 @@ export default class MetaMetricsController {
this.finalizeEventFragment(fragment.id, { abandoned: true });
});
// Code below submits any pending segmentApiCalls to Segment if/when the controller is re-instantiated
if (isManifestV3) {
Object.values(segmentApiCalls).forEach(
({ eventType, payload, callback }) => {
this._submitSegmentAPICall(eventType, payload, callback);
},
);
}
// Close out event fragments that were created but not progressed. An
// interval is used to routinely check if a fragment has not been updated
// within the fragment's timeout window. When creating a new event fragment
@ -162,17 +193,10 @@ export default class MetaMetricsController {
});
}
});
chrome.alarms.onAlarm.addListener(() => {
chrome.alarms.getAll((alarms) => {
const hasAlarm = checkAlarmExists(
alarms,
METAMETRICS_FINALIZE_EVENT_FRAGMENT_ALARM,
);
if (hasAlarm) {
this.finalizeAbandonedFragments();
}
});
chrome.alarms.onAlarm.addListener((alarmInfo) => {
if (alarmInfo.name === METAMETRICS_FINALIZE_EVENT_FRAGMENT_ALARM) {
this.finalizeAbandonedFragments();
}
});
} else {
setInterval(() => {
@ -225,14 +249,6 @@ export default class MetaMetricsController {
);
}
const existingFragment = this.getExistingEventFragment(
options.actionId,
options.uniqueIdentifier,
);
if (existingFragment) {
return existingFragment;
}
const { fragments } = this.store.getState();
const id = options.uniqueIdentifier ?? uuidv4();
@ -260,6 +276,8 @@ export default class MetaMetricsController {
value: fragment.value,
currency: fragment.currency,
environmentType: fragment.environmentType,
actionId: options.actionId,
uniqueIdentifier: options.uniqueIdentifier,
});
}
@ -281,26 +299,6 @@ export default class MetaMetricsController {
return fragment;
}
/**
* Returns the fragment stored in memory with provided id or undefined if it
* does not exist.
*
* @param {string} actionId - actionId passed from UI
* @param {string} uniqueIdentifier - uniqueIdentifier of the event
* @returns {[MetaMetricsEventFragment]}
*/
getExistingEventFragment(actionId, uniqueIdentifier) {
const { fragments } = this.store.getState();
const existingFragment = Object.values(fragments).find(
(fragment) =>
fragment.actionId === actionId &&
fragment.uniqueIdentifier === uniqueIdentifier,
);
return existingFragment;
}
/**
* Updates an event fragment in state
*
@ -361,6 +359,8 @@ export default class MetaMetricsController {
value: fragment.value,
currency: fragment.currency,
environmentType: fragment.environmentType,
actionId: fragment.actionId,
uniqueIdentifier: fragment.uniqueIdentifier,
});
const { fragments } = this.store.getState();
delete fragments[id];
@ -447,7 +447,10 @@ export default class MetaMetricsController {
* @param {MetaMetricsPageOptions} [options] - options for handling the page
* view
*/
trackPage({ name, params, environmentType, page, referrer }, options) {
trackPage(
{ name, params, environmentType, page, referrer, actionId },
options,
) {
try {
if (this.state.participateInMetaMetrics === false) {
return;
@ -462,7 +465,8 @@ export default class MetaMetricsController {
const { metaMetricsId } = this.state;
const idTrait = metaMetricsId ? 'userId' : 'anonymousId';
const idValue = metaMetricsId ?? METAMETRICS_ANONYMOUS_ID;
this.segment.page({
this._submitSegmentAPICall('page', {
messageId: buildUniqueMessageId({ actionId }),
[idTrait]: idValue,
name,
properties: {
@ -653,6 +657,7 @@ export default class MetaMetricsController {
} = rawPayload;
return {
event,
messageId: buildUniqueMessageId(rawPayload),
properties: {
// These values are omitted from properties because they have special meaning
// in segment. https://segment.com/docs/connections/spec/track/#properties.
@ -682,7 +687,7 @@ export default class MetaMetricsController {
* @returns {MetaMetricsTraits | null} traits that have changed since last update
*/
_buildUserTraitsObject(metamaskState) {
const { traits } = this.store.getState();
const { traits, previousUserTraits } = this.store.getState();
/** @type {MetaMetricsTraits} */
const currentTraits = {
[TRAITS.ADDRESS_BOOK_ENTRIES]: sum(
@ -703,15 +708,14 @@ export default class MetaMetricsController {
},
[],
),
[TRAITS.NFT_AUTODETECTION_ENABLED]: metamaskState.useCollectibleDetection,
[TRAITS.NFT_AUTODETECTION_ENABLED]: metamaskState.useNftDetection,
[TRAITS.NUMBER_OF_ACCOUNTS]: Object.values(metamaskState.identities)
.length,
[TRAITS.NUMBER_OF_NFT_COLLECTIONS]: this._getAllUniqueNFTAddressesLength(
metamaskState.allCollectibles,
metamaskState.allNfts,
),
[TRAITS.NUMBER_OF_NFTS]: this._getAllNFTsFlattened(
metamaskState.allCollectibles,
).length,
[TRAITS.NUMBER_OF_NFTS]: this._getAllNFTsFlattened(metamaskState.allNfts)
.length,
[TRAITS.NUMBER_OF_TOKENS]: this._getNumberOfTokens(metamaskState),
[TRAITS.OPENSEA_API_ENABLED]: metamaskState.openSeaEnabled,
[TRAITS.THREE_BOX_ENABLED]: false, // deprecated, hard-coded as false
@ -719,17 +723,17 @@ export default class MetaMetricsController {
[TRAITS.TOKEN_DETECTION_ENABLED]: metamaskState.useTokenDetection,
};
if (!this.previousTraits) {
this.previousTraits = currentTraits;
if (!previousUserTraits) {
this.store.updateState({ previousUserTraits: currentTraits });
return currentTraits;
}
if (this.previousTraits && !isEqual(this.previousTraits, currentTraits)) {
if (previousUserTraits && !isEqual(previousUserTraits, currentTraits)) {
const updates = pickBy(
currentTraits,
(v, k) => !isEqual(this.previousTraits[k], v),
(v, k) => !isEqual(previousUserTraits[k], v),
);
this.previousTraits = currentTraits;
this.store.updateState({ previousUserTraits: currentTraits });
return updates;
}
@ -762,11 +766,11 @@ export default class MetaMetricsController {
* Returns an array of all of the collectibles/NFTs the user
* possesses across all networks and accounts.
*
* @param {object} allCollectibles
* @param {object} allNfts
* @returns {[]}
*/
_getAllNFTsFlattened = memoize((allCollectibles = {}) => {
return Object.values(allCollectibles).reduce((result, chainNFTs) => {
_getAllNFTsFlattened = memoize((allNfts = {}) => {
return Object.values(allNfts).reduce((result, chainNFTs) => {
return result.concat(...Object.values(chainNFTs));
}, []);
});
@ -775,11 +779,11 @@ export default class MetaMetricsController {
* Returns the number of unique collectible/NFT addresses the user
* possesses across all networks and accounts.
*
* @param {object} allCollectibles
* @param {object} allNfts
* @returns {number}
*/
_getAllUniqueNFTAddressesLength(allCollectibles = {}) {
const allNFTAddresses = this._getAllNFTsFlattened(allCollectibles).map(
_getAllUniqueNFTAddressesLength(allNfts = {}) {
const allNFTAddresses = this._getAllNFTsFlattened(allNfts).map(
(nft) => nft.address,
);
const uniqueAddresses = new Set(allNFTAddresses);
@ -815,7 +819,7 @@ export default class MetaMetricsController {
}
try {
this.segment.identify({
this._submitSegmentAPICall('identify', {
userId: metaMetricsId,
traits: userTraits,
});
@ -944,10 +948,49 @@ export default class MetaMetricsController {
return resolve();
};
this.segment.track(payload, callback);
this._submitSegmentAPICall('track', payload, callback);
if (flushImmediately) {
this.segment.flush();
}
});
}
// Method below submits the request to analytics SDK.
// It will also add event to controller store
// and pass a callback to remove it from store once request is submitted to segment
// Saving segmentApiCalls in controller store in MV3 ensures that events are tracked
// even if service worker terminates before events are submiteed to segment.
_submitSegmentAPICall(eventType, payload, callback) {
const messageId = payload.messageId || generateRandomId();
let timestamp = new Date();
if (payload.timestamp) {
const payloadDate = new Date(payload.timestamp);
if (isValidDate(payloadDate)) {
timestamp = payloadDate;
}
}
const modifiedPayload = { ...payload, messageId, timestamp };
this.store.updateState({
segmentApiCalls: {
...this.store.getState().segmentApiCalls,
[messageId]: {
eventType,
payload: {
...modifiedPayload,
timestamp: modifiedPayload.timestamp.toString(),
},
callback,
},
},
});
const modifiedCallback = (result) => {
const { segmentApiCalls } = this.store.getState();
delete segmentApiCalls[messageId];
this.store.updateState({
segmentApiCalls,
});
return callback?.(result);
};
this.segment[eventType](modifiedPayload, modifiedCallback);
}
}

@ -9,6 +9,7 @@ import {
} from '../../../shared/constants/metametrics';
import waitUntilCalled from '../../../test/lib/wait-until-called';
import { CHAIN_IDS, CURRENCY_SYMBOLS } from '../../../shared/constants/network';
import * as Utils from '../lib/util';
import MetaMetricsController from './metametrics';
import { NETWORK_EVENTS } from './network';
@ -19,6 +20,7 @@ const NETWORK = 'Mainnet';
const FAKE_CHAIN_ID = '0x1338';
const LOCALE = 'en_US';
const TEST_META_METRICS_ID = '0xabc';
const DUMMY_ACTION_ID = 'DUMMY_ACTION_ID';
const MOCK_TRAITS = {
test_boolean: true,
@ -124,9 +126,10 @@ function getMetaMetricsController({
metaMetricsId = TEST_META_METRICS_ID,
preferencesStore = getMockPreferencesStore(),
networkController = getMockNetworkController(),
segmentInstance,
} = {}) {
return new MetaMetricsController({
segment,
segment: segmentInstance || segment,
getNetworkIdentifier:
networkController.getNetworkIdentifier.bind(networkController),
getCurrentChainId:
@ -145,10 +148,17 @@ function getMetaMetricsController({
testid: SAMPLE_PERSISTED_EVENT,
testid2: SAMPLE_NON_PERSISTED_EVENT,
},
events: {},
},
});
}
describe('MetaMetricsController', function () {
const now = new Date();
let clock;
beforeEach(function () {
clock = sinon.useFakeTimers(now.getTime());
sinon.stub(Utils, 'generateRandomId').returns('DUMMY_RANDOM_ID');
});
describe('constructor', function () {
it('should properly initialize', function () {
const mock = sinon.mock(segment);
@ -163,6 +173,8 @@ describe('MetaMetricsController', function () {
...DEFAULT_EVENT_PROPERTIES,
test: true,
},
messageId: Utils.generateRandomId(),
timestamp: new Date(),
});
const metaMetricsController = getMetaMetricsController();
assert.strictEqual(metaMetricsController.version, VERSION);
@ -233,15 +245,18 @@ describe('MetaMetricsController', function () {
});
const mock = sinon.mock(segment);
mock
.expects('identify')
.once()
.withArgs({ userId: TEST_META_METRICS_ID, traits: MOCK_TRAITS });
mock.expects('identify').once().withArgs({
userId: TEST_META_METRICS_ID,
traits: MOCK_TRAITS,
messageId: Utils.generateRandomId(),
timestamp: new Date(),
});
metaMetricsController.identify({
...MOCK_TRAITS,
...MOCK_INVALID_TRAITS,
});
mock.verify();
});
@ -263,6 +278,8 @@ describe('MetaMetricsController', function () {
traits: {
test_date: mockDateISOString,
},
messageId: Utils.generateRandomId(),
timestamp: new Date(),
});
metaMetricsController.identify({
@ -358,6 +375,8 @@ describe('MetaMetricsController', function () {
test: 1,
...DEFAULT_EVENT_PROPERTIES,
},
messageId: Utils.generateRandomId(),
timestamp: new Date(),
});
metaMetricsController.submitEvent(
{
@ -388,6 +407,8 @@ describe('MetaMetricsController', function () {
test: 1,
...DEFAULT_EVENT_PROPERTIES,
},
messageId: Utils.generateRandomId(),
timestamp: new Date(),
});
metaMetricsController.submitEvent(
{
@ -417,6 +438,8 @@ describe('MetaMetricsController', function () {
legacy_event: true,
...DEFAULT_EVENT_PROPERTIES,
},
messageId: Utils.generateRandomId(),
timestamp: new Date(),
});
metaMetricsController.submitEvent(
{
@ -439,12 +462,14 @@ describe('MetaMetricsController', function () {
.once()
.withArgs({
event: 'Fake Event',
userId: TEST_META_METRICS_ID,
context: DEFAULT_TEST_CONTEXT,
properties: {
test: 1,
...DEFAULT_EVENT_PROPERTIES,
},
context: DEFAULT_TEST_CONTEXT,
userId: TEST_META_METRICS_ID,
messageId: Utils.generateRandomId(),
timestamp: new Date(),
});
metaMetricsController.submitEvent({
event: 'Fake Event',
@ -519,6 +544,8 @@ describe('MetaMetricsController', function () {
foo: 'bar',
...DEFAULT_EVENT_PROPERTIES,
},
messageId: Utils.generateRandomId(),
timestamp: new Date(),
}),
);
assert.ok(
@ -527,6 +554,8 @@ describe('MetaMetricsController', function () {
userId: TEST_META_METRICS_ID,
context: DEFAULT_TEST_CONTEXT,
properties: DEFAULT_EVENT_PROPERTIES,
messageId: Utils.generateRandomId(),
timestamp: new Date(),
}),
);
});
@ -547,6 +576,8 @@ describe('MetaMetricsController', function () {
params: null,
...DEFAULT_PAGE_PROPERTIES,
},
messageId: Utils.generateRandomId(),
timestamp: new Date(),
});
metaMetricsController.trackPage({
name: 'home',
@ -590,6 +621,8 @@ describe('MetaMetricsController', function () {
params: null,
...DEFAULT_PAGE_PROPERTIES,
},
messageId: Utils.generateRandomId(),
timestamp: new Date(),
});
metaMetricsController.trackPage(
{
@ -602,6 +635,50 @@ describe('MetaMetricsController', function () {
);
mock.verify();
});
it('multiple trackPage call with same actionId should result in same messageId being sent to segment', function () {
const mock = sinon.mock(segment);
const metaMetricsController = getMetaMetricsController({
preferencesStore: getMockPreferencesStore({
participateInMetaMetrics: null,
}),
});
mock
.expects('page')
.twice()
.withArgs({
name: 'home',
userId: TEST_META_METRICS_ID,
context: DEFAULT_TEST_CONTEXT,
properties: {
params: null,
...DEFAULT_PAGE_PROPERTIES,
},
messageId: DUMMY_ACTION_ID,
timestamp: new Date(),
});
metaMetricsController.trackPage(
{
name: 'home',
params: null,
actionId: DUMMY_ACTION_ID,
environmentType: ENVIRONMENT_TYPE_BACKGROUND,
page: METAMETRICS_BACKGROUND_PAGE_OBJECT,
},
{ isOptInPath: true },
);
metaMetricsController.trackPage(
{
name: 'home',
params: null,
actionId: DUMMY_ACTION_ID,
environmentType: ENVIRONMENT_TYPE_BACKGROUND,
page: METAMETRICS_BACKGROUND_PAGE_OBJECT,
},
{ isOptInPath: true },
);
mock.verify();
});
});
describe('_buildUserTraitsObject', function () {
@ -640,7 +717,7 @@ describe('MetaMetricsController', function () {
[CHAIN_IDS.MAINNET]: [{ address: '0x' }],
[CHAIN_IDS.GOERLI]: [{ address: '0x' }, { address: '0x0' }],
},
allCollectibles: {
allNfts: {
'0xac706cE8A9BF27Afecf080fB298d0ee13cfb978A': {
56: [
{
@ -675,7 +752,7 @@ describe('MetaMetricsController', function () {
identities: [{}, {}],
ledgerTransportType: 'web-hid',
openSeaEnabled: true,
useCollectibleDetection: false,
useNftDetection: false,
theme: 'default',
useTokenDetection: true,
});
@ -713,7 +790,7 @@ describe('MetaMetricsController', function () {
ledgerTransportType: 'web-hid',
openSeaEnabled: true,
identities: [{}, {}],
useCollectibleDetection: false,
useNftDetection: false,
theme: 'default',
useTokenDetection: true,
});
@ -733,7 +810,7 @@ describe('MetaMetricsController', function () {
ledgerTransportType: 'web-hid',
openSeaEnabled: false,
identities: [{}, {}, {}],
useCollectibleDetection: false,
useNftDetection: false,
theme: 'default',
useTokenDetection: true,
});
@ -761,7 +838,7 @@ describe('MetaMetricsController', function () {
ledgerTransportType: 'web-hid',
openSeaEnabled: true,
identities: [{}, {}],
useCollectibleDetection: true,
useNftDetection: true,
theme: 'default',
useTokenDetection: true,
});
@ -779,7 +856,7 @@ describe('MetaMetricsController', function () {
ledgerTransportType: 'web-hid',
openSeaEnabled: true,
identities: [{}, {}],
useCollectibleDetection: true,
useNftDetection: true,
theme: 'default',
useTokenDetection: true,
});
@ -788,9 +865,35 @@ describe('MetaMetricsController', function () {
});
});
describe('submitting segmentApiCalls to segment SDK', function () {
it('should add event to store when submitting to SDK', function () {
const metaMetricsController = getMetaMetricsController({});
metaMetricsController.trackPage({}, { isOptIn: true });
const { segmentApiCalls } = metaMetricsController.store.getState();
assert(Object.keys(segmentApiCalls).length > 0);
});
it('should remove event from store when callback is invoked', function () {
const segmentInstance = createSegmentMock(2, 10000);
const stubFn = (_, cb) => {
cb();
};
sinon.stub(segmentInstance, 'track').callsFake(stubFn);
sinon.stub(segmentInstance, 'page').callsFake(stubFn);
const metaMetricsController = getMetaMetricsController({
segmentInstance,
});
metaMetricsController.trackPage({}, { isOptIn: true });
const { segmentApiCalls } = metaMetricsController.store.getState();
assert(Object.keys(segmentApiCalls).length === 0);
});
});
afterEach(function () {
// flush the queues manually after each test
segment.flush();
clock.restore();
sinon.restore();
});
});

@ -16,7 +16,7 @@ describe('PermissionController specifications', () => {
describe('caveat specifications', () => {
it('getCaveatSpecifications returns the expected specifications object', () => {
const caveatSpecifications = getCaveatSpecifications({});
expect(Object.keys(caveatSpecifications)).toHaveLength(4);
expect(Object.keys(caveatSpecifications)).toHaveLength(5);
expect(
caveatSpecifications[CaveatTypes.restrictReturnedAccounts].type,
).toStrictEqual(CaveatTypes.restrictReturnedAccounts);
@ -30,6 +30,9 @@ describe('PermissionController specifications', () => {
expect(caveatSpecifications.snapKeyring.type).toStrictEqual(
SnapCaveatType.SnapKeyring,
);
expect(caveatSpecifications.snapCronjob.type).toStrictEqual(
SnapCaveatType.SnapCronjob,
);
});
describe('restrictReturnedAccounts', () => {

@ -39,7 +39,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,
useCollectibleDetection: false,
useNftDetection: false,
openSeaEnabled: false,
advancedGasFee: null,
@ -69,6 +69,7 @@ export default class PreferencesController {
? LEDGER_TRANSPORT_TYPES.WEBHID
: LEDGER_TRANSPORT_TYPES.U2F,
theme: 'light',
improvedTokenAllowanceEnabled: false,
...opts.initState,
};
@ -141,12 +142,12 @@ export default class PreferencesController {
}
/**
* Setter for the `useCollectibleDetection` property
* Setter for the `useNftDetection` property
*
* @param {boolean} useCollectibleDetection - Whether or not the user prefers to autodetect collectibles.
* @param {boolean} useNftDetection - Whether or not the user prefers to autodetect collectibles.
*/
setUseCollectibleDetection(useCollectibleDetection) {
this.store.updateState({ useCollectibleDetection });
setUseNftDetection(useNftDetection) {
this.store.updateState({ useNftDetection });
}
/**
@ -187,6 +188,17 @@ export default class PreferencesController {
this.store.updateState({ theme: val });
}
/**
* Setter for the `improvedTokenAllowanceEnabled` property
*
* @param improvedTokenAllowanceEnabled
*/
setImprovedTokenAllowanceEnabled(improvedTokenAllowanceEnabled) {
this.store.updateState({
improvedTokenAllowanceEnabled,
});
}
/**
* Add new methodData to state, to avoid requesting this information again through Infura
*

@ -308,21 +308,21 @@ describe('preferences controller', function () {
});
});
describe('setUseCollectibleDetection', function () {
describe('setUseNftDetection', function () {
it('should default to false', function () {
const state = preferencesController.store.getState();
assert.equal(state.useCollectibleDetection, false);
assert.equal(state.useNftDetection, false);
});
it('should set the useCollectibleDetection property in state', function () {
it('should set the useNftDetection property in state', function () {
assert.equal(
preferencesController.store.getState().useCollectibleDetection,
preferencesController.store.getState().useNftDetection,
false,
);
preferencesController.setOpenSeaEnabled(true);
preferencesController.setUseCollectibleDetection(true);
preferencesController.setUseNftDetection(true);
assert.equal(
preferencesController.store.getState().useCollectibleDetection,
preferencesController.store.getState().useNftDetection,
true,
);
});

@ -14,7 +14,10 @@ import log from 'loglevel';
import pify from 'pify';
import { ethers } from 'ethers';
import SINGLE_CALL_BALANCES_ABI from 'single-call-balance-checker-abi';
import { CHAIN_IDS } from '../../../shared/constants/network';
import {
CHAIN_IDS,
LOCALHOST_RPC_URL,
} from '../../../shared/constants/network';
import {
SINGLE_CALL_BALANCES_ADDRESS,
@ -50,6 +53,7 @@ export default class AccountTracker {
* @param {object} opts.provider - An EIP-1193 provider instance that uses the current global network
* @param {object} opts.blockTracker - A block tracker, which emits events for each new block
* @param {Function} opts.getCurrentChainId - A function that returns the `chainId` for the current global network
* @param {Function} opts.getNetworkIdentifier - A function that returns the current network
*/
constructor(opts = {}) {
const initState = {
@ -69,6 +73,7 @@ export default class AccountTracker {
// bind function for easier listener syntax
this._updateForBlock = this._updateForBlock.bind(this);
this.getCurrentChainId = opts.getCurrentChainId;
this.getNetworkIdentifier = opts.getNetworkIdentifier;
this.ethersProvider = new ethers.providers.Web3Provider(this._provider);
}
@ -199,73 +204,79 @@ export default class AccountTracker {
const { accounts } = this.store.getState();
const addresses = Object.keys(accounts);
const chainId = this.getCurrentChainId();
switch (chainId) {
case CHAIN_IDS.MAINNET:
await this._updateAccountsViaBalanceChecker(
addresses,
SINGLE_CALL_BALANCES_ADDRESS,
);
break;
case CHAIN_IDS.GOERLI:
await this._updateAccountsViaBalanceChecker(
addresses,
SINGLE_CALL_BALANCES_ADDRESS_GOERLI,
);
break;
case CHAIN_IDS.SEPOLIA:
await this._updateAccountsViaBalanceChecker(
addresses,
SINGLE_CALL_BALANCES_ADDRESS_SEPOLIA,
);
break;
case CHAIN_IDS.BSC:
await this._updateAccountsViaBalanceChecker(
addresses,
SINGLE_CALL_BALANCES_ADDRESS_BSC,
);
break;
case CHAIN_IDS.OPTIMISM:
await this._updateAccountsViaBalanceChecker(
addresses,
SINGLE_CALL_BALANCES_ADDRESS_OPTIMISM,
);
break;
case CHAIN_IDS.POLYGON:
await this._updateAccountsViaBalanceChecker(
addresses,
SINGLE_CALL_BALANCES_ADDRESS_POLYGON,
);
break;
case CHAIN_IDS.AVALANCHE:
await this._updateAccountsViaBalanceChecker(
addresses,
SINGLE_CALL_BALANCES_ADDRESS_AVALANCHE,
);
break;
case CHAIN_IDS.FANTOM:
await this._updateAccountsViaBalanceChecker(
addresses,
SINGLE_CALL_BALANCES_ADDRESS_FANTOM,
);
break;
case CHAIN_IDS.ARBITRUM:
await this._updateAccountsViaBalanceChecker(
addresses,
SINGLE_CALL_BALANCES_ADDRESS_ARBITRUM,
);
break;
default:
await Promise.all(addresses.map(this._updateAccount.bind(this)));
const networkId = this.getNetworkIdentifier();
const rpcUrl = 'http://127.0.0.1:8545';
if (networkId === LOCALHOST_RPC_URL || networkId === rpcUrl) {
await Promise.all(addresses.map(this._updateAccount.bind(this)));
} else {
switch (chainId) {
case CHAIN_IDS.MAINNET:
await this._updateAccountsViaBalanceChecker(
addresses,
SINGLE_CALL_BALANCES_ADDRESS,
);
break;
case CHAIN_IDS.GOERLI:
await this._updateAccountsViaBalanceChecker(
addresses,
SINGLE_CALL_BALANCES_ADDRESS_GOERLI,
);
break;
case CHAIN_IDS.SEPOLIA:
await this._updateAccountsViaBalanceChecker(
addresses,
SINGLE_CALL_BALANCES_ADDRESS_SEPOLIA,
);
break;
case CHAIN_IDS.BSC:
await this._updateAccountsViaBalanceChecker(
addresses,
SINGLE_CALL_BALANCES_ADDRESS_BSC,
);
break;
case CHAIN_IDS.OPTIMISM:
await this._updateAccountsViaBalanceChecker(
addresses,
SINGLE_CALL_BALANCES_ADDRESS_OPTIMISM,
);
break;
case CHAIN_IDS.POLYGON:
await this._updateAccountsViaBalanceChecker(
addresses,
SINGLE_CALL_BALANCES_ADDRESS_POLYGON,
);
break;
case CHAIN_IDS.AVALANCHE:
await this._updateAccountsViaBalanceChecker(
addresses,
SINGLE_CALL_BALANCES_ADDRESS_AVALANCHE,
);
break;
case CHAIN_IDS.FANTOM:
await this._updateAccountsViaBalanceChecker(
addresses,
SINGLE_CALL_BALANCES_ADDRESS_FANTOM,
);
break;
case CHAIN_IDS.ARBITRUM:
await this._updateAccountsViaBalanceChecker(
addresses,
SINGLE_CALL_BALANCES_ADDRESS_ARBITRUM,
);
break;
default:
await Promise.all(addresses.map(this._updateAccount.bind(this)));
}
}
}

@ -2,21 +2,10 @@ import removeSlash from 'remove-trailing-slash';
import looselyValidate from '@segment/loosely-validate-event';
import { isString } from 'lodash';
import isRetryAllowed from 'is-retry-allowed';
import { generateRandomId } from '../util';
const noop = () => ({});
// Taken from https://stackoverflow.com/a/1349426/3696652
const characters =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const generateRandomId = () => {
let result = '';
const charactersLength = characters.length;
for (let i = 0; i < 20; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
};
// Method below is inspired from axios-retry https://github.com/softonic/axios-retry
function isNetworkError(error) {
return (

@ -9,6 +9,14 @@ import pump from 'pump';
*/
export function setupMultiplex(connectionStream) {
const mux = new ObjectMultiplex();
/**
* We are using this streams to send keep alive message between backend/ui without setting up a multiplexer
* We need to tell the multiplexer to ignore them, else we get the " orphaned data for stream " warnings
* https://github.com/MetaMask/object-multiplex/blob/280385401de84f57ef57054d92cfeb8361ef2680/src/ObjectMultiplex.ts#L63
*/
mux.ignoreStream('CONNECTION_READY');
mux.ignoreStream('ACK_KEEP_ALIVE_MESSAGE');
mux.ignoreStream('WORKER_KEEP_ALIVE_MESSAGE');
pump(connectionStream, mux, connectionStream, (err) => {
if (err) {
console.error(err);

@ -96,6 +96,7 @@ function BnMultiplyByFraction(targetBN, numerator, denominator) {
* Returns an Error if extension.runtime.lastError is present
* this is a workaround for the non-standard error object that's used
*
* @deprecated use checkForLastError in shared/modules/browser-runtime.utils.js
* @returns {Error|undefined}
*/
function checkForError() {
@ -174,3 +175,19 @@ export {
getChainType,
checkAlarmExists,
};
// Taken from https://stackoverflow.com/a/1349426/3696652
const characters =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
export const generateRandomId = () => {
let result = '';
const charactersLength = characters.length;
for (let i = 0; i < 20; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
};
export const isValidDate = (d) => {
return d instanceof Date && !isNaN(d);
};

@ -34,9 +34,9 @@ import {
TokenListController,
TokensController,
TokenRatesController,
CollectiblesController,
NftController,
AssetsContractController,
CollectibleDetectionController,
NftDetectionController,
PermissionController,
SubjectMetadataController,
PermissionsRequestNotFoundError,
@ -49,6 +49,7 @@ import {
import SmartTransactionsController from '@metamask/smart-transactions-controller';
///: BEGIN:ONLY_INCLUDE_IN(flask)
import {
CronjobController,
SnapController,
IframeExecutionService,
} from '@metamask/snap-controllers';
@ -313,7 +314,7 @@ export default class MetamaskController extends EventEmitter {
initState.AssetsContractController,
);
this.collectiblesController = new CollectiblesController(
this.nftController = new NftController(
{
onPreferencesStateChange:
this.preferencesController.store.subscribe.bind(
@ -344,14 +345,14 @@ export default class MetamaskController extends EventEmitter {
this.assetsContractController.getERC1155TokenURI.bind(
this.assetsContractController,
),
onCollectibleAdded: ({ address, symbol, tokenId, standard, source }) =>
onNftAdded: ({ address, symbol, tokenId, standard, source }) =>
this.metaMetricsController.trackEvent({
event: EVENT_NAMES.NFT_ADDED,
category: EVENT.CATEGORIES.WALLET,
properties: {
token_contract_address: address,
token_symbol: symbol,
asset_type: ASSET_TYPES.COLLECTIBLE,
asset_type: ASSET_TYPES.NFT,
token_standard: standard,
source,
},
@ -361,34 +362,29 @@ export default class MetamaskController extends EventEmitter {
}),
},
{},
initState.CollectiblesController,
initState.NftController,
);
this.collectiblesController.setApiKey(process.env.OPENSEA_KEY);
this.nftController.setApiKey(process.env.OPENSEA_KEY);
process.env.COLLECTIBLES_V1 &&
(this.collectibleDetectionController = new CollectibleDetectionController(
{
onCollectiblesStateChange: (listener) =>
this.collectiblesController.subscribe(listener),
onPreferencesStateChange:
this.preferencesController.store.subscribe.bind(
this.preferencesController.store,
),
onNetworkStateChange: this.networkController.store.subscribe.bind(
this.networkController.store,
(this.nftDetectionController = new NftDetectionController({
onNftsStateChange: (listener) => this.nftController.subscribe(listener),
onPreferencesStateChange:
this.preferencesController.store.subscribe.bind(
this.preferencesController.store,
),
getOpenSeaApiKey: () => this.collectiblesController.openSeaApiKey,
getBalancesInSingleCall:
this.assetsContractController.getBalancesInSingleCall.bind(
this.assetsContractController,
),
addCollectible: this.collectiblesController.addCollectible.bind(
this.collectiblesController,
onNetworkStateChange: this.networkController.store.subscribe.bind(
this.networkController.store,
),
getOpenSeaApiKey: () => this.nftController.openSeaApiKey,
getBalancesInSingleCall:
this.assetsContractController.getBalancesInSingleCall.bind(
this.assetsContractController,
),
getCollectiblesState: () => this.collectiblesController.state,
},
));
addNft: this.nftController.addNft.bind(this.nftController),
getNftState: () => this.nftController.state,
}));
this.metaMetricsController = new MetaMetricsController({
segment,
@ -539,6 +535,9 @@ export default class MetamaskController extends EventEmitter {
getCurrentChainId: this.networkController.getCurrentChainId.bind(
this.networkController,
),
getNetworkIdentifier: this.networkController.getNetworkIdentifier.bind(
this.networkController,
),
});
// start and stop polling for balances based on activeControllerConnections
@ -664,7 +663,7 @@ export default class MetamaskController extends EventEmitter {
///: BEGIN:ONLY_INCLUDE_IN(flask)
this.snapExecutionService = new IframeExecutionService({
iframeUrl: new URL(
'https://metamask.github.io/iframe-execution-environment/0.9.1',
'https://metamask.github.io/iframe-execution-environment/0.10.0',
),
messenger: this.controllerMessenger.getRestricted({
name: 'ExecutionService',
@ -750,6 +749,24 @@ export default class MetamaskController extends EventEmitter {
},
},
});
// --- Snaps Cronjob Controller configuration
const cronjobControllerMessenger = this.controllerMessenger.getRestricted({
name: 'CronjobController',
allowedEvents: [
'SnapController:snapInstalled',
'SnapController:snapUpdated',
'SnapController:snapRemoved',
],
allowedActions: [
`${this.permissionController.name}:getPermissions`,
'SnapController:handleRequest',
'SnapController:getAll',
],
});
this.cronjobController = new CronjobController({
state: initState.CronjobController,
messenger: cronjobControllerMessenger,
});
///: END:ONLY_INCLUDE_IN
this.detectTokensController = new DetectTokensController({
preferences: this.preferencesController,
@ -875,12 +892,10 @@ export default class MetamaskController extends EventEmitter {
const transactionDataTokenId =
getTokenIdParam(transactionData) ??
getTokenValueParam(transactionData);
const { allCollectibles } = this.collectiblesController.state;
const { allNfts } = this.nftController.state;
// check if its a known collectible
const knownCollectible = allCollectibles?.[userAddress]?.[
chainId
].find(
const knownCollectible = allNfts?.[userAddress]?.[chainId].find(
({ address, tokenId }) =>
isEqualCaseInsensitive(address, contractAddress) &&
tokenId === transactionDataTokenId,
@ -888,7 +903,7 @@ export default class MetamaskController extends EventEmitter {
// if it is we check and update ownership status.
if (knownCollectible) {
this.collectiblesController.checkAndUpdateSingleCollectibleOwnershipStatus(
this.nftController.checkAndUpdateSingleNftOwnershipStatus(
knownCollectible,
false,
{ userAddress, chainId },
@ -896,7 +911,7 @@ export default class MetamaskController extends EventEmitter {
}
}
const metamaskState = await this.getState();
const metamaskState = this.getState();
if (txReceipt && txReceipt.status === '0x0') {
this.metaMetricsController.trackEvent(
@ -1038,9 +1053,10 @@ export default class MetamaskController extends EventEmitter {
TokenListController: this.tokenListController,
TokensController: this.tokensController,
SmartTransactionsController: this.smartTransactionsController,
CollectiblesController: this.collectiblesController,
NftController: this.nftController,
///: BEGIN:ONLY_INCLUDE_IN(flask)
SnapController: this.snapController,
CronjobController: this.cronjobController,
NotificationController: this.notificationController,
///: END:ONLY_INCLUDE_IN
});
@ -1079,9 +1095,10 @@ export default class MetamaskController extends EventEmitter {
TokenListController: this.tokenListController,
TokensController: this.tokensController,
SmartTransactionsController: this.smartTransactionsController,
CollectiblesController: this.collectiblesController,
NftController: this.nftController,
///: BEGIN:ONLY_INCLUDE_IN(flask)
SnapController: this.snapController,
CronjobController: this.cronjobController,
NotificationController: this.notificationController,
///: END:ONLY_INCLUDE_IN
},
@ -1130,10 +1147,6 @@ export default class MetamaskController extends EventEmitter {
return {
...buildSnapEndowmentSpecifications(),
...buildSnapRestrictedMethodSpecifications({
addSnap: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapController:add',
),
clearSnapState: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapController:clearSnapState',
@ -1486,8 +1499,8 @@ export default class MetamaskController extends EventEmitter {
addressBookController,
alertController,
appStateController,
collectiblesController,
collectibleDetectionController,
nftController,
nftDetectionController,
currencyRateController,
detectTokensController,
ensController,
@ -1526,10 +1539,9 @@ export default class MetamaskController extends EventEmitter {
setUseTokenDetection: preferencesController.setUseTokenDetection.bind(
preferencesController,
),
setUseCollectibleDetection:
preferencesController.setUseCollectibleDetection.bind(
preferencesController,
),
setUseNftDetection: preferencesController.setUseNftDetection.bind(
preferencesController,
),
setOpenSeaEnabled: preferencesController.setOpenSeaEnabled.bind(
preferencesController,
),
@ -1632,41 +1644,32 @@ export default class MetamaskController extends EventEmitter {
preferencesController,
),
setTheme: preferencesController.setTheme.bind(preferencesController),
setImprovedTokenAllowanceEnabled:
preferencesController.setImprovedTokenAllowanceEnabled.bind(
preferencesController,
),
// AssetsContractController
getTokenStandardAndDetails: this.getTokenStandardAndDetails.bind(this),
// CollectiblesController
addCollectible: collectiblesController.addCollectible.bind(
collectiblesController,
),
// NftController
addNft: nftController.addNft.bind(nftController),
addCollectibleVerifyOwnership:
collectiblesController.addCollectibleVerifyOwnership.bind(
collectiblesController,
),
addNftVerifyOwnership:
nftController.addNftVerifyOwnership.bind(nftController),
removeAndIgnoreCollectible:
collectiblesController.removeAndIgnoreCollectible.bind(
collectiblesController,
),
removeAndIgnoreNft: nftController.removeAndIgnoreNft.bind(nftController),
removeCollectible: collectiblesController.removeCollectible.bind(
collectiblesController,
),
removeNft: nftController.removeNft.bind(nftController),
checkAndUpdateAllCollectiblesOwnershipStatus:
collectiblesController.checkAndUpdateAllCollectiblesOwnershipStatus.bind(
collectiblesController,
),
checkAndUpdateAllNftsOwnershipStatus:
nftController.checkAndUpdateAllNftsOwnershipStatus.bind(nftController),
checkAndUpdateSingleCollectibleOwnershipStatus:
collectiblesController.checkAndUpdateSingleCollectibleOwnershipStatus.bind(
collectiblesController,
checkAndUpdateSingleNftOwnershipStatus:
nftController.checkAndUpdateSingleNftOwnershipStatus.bind(
nftController,
),
isCollectibleOwner: collectiblesController.isCollectibleOwner.bind(
collectiblesController,
),
isNftOwner: nftController.isNftOwner.bind(nftController),
// AddressController
setAddressBook: addressBookController.set.bind(addressBookController),
@ -1697,6 +1700,8 @@ export default class MetamaskController extends EventEmitter {
),
setShowPortfolioTooltip:
appStateController.setShowPortfolioTooltip.bind(appStateController),
setShowBetaHeader:
appStateController.setShowBetaHeader.bind(appStateController),
setCollectiblesDetectionNoticeDismissed:
appStateController.setCollectiblesDetectionNoticeDismissed.bind(
appStateController,
@ -1946,10 +1951,8 @@ export default class MetamaskController extends EventEmitter {
),
// DetectCollectibleController
detectCollectibles: process.env.COLLECTIBLES_V1
? collectibleDetectionController.detectCollectibles.bind(
collectibleDetectionController,
)
detectNfts: process.env.COLLECTIBLES_V1
? nftDetectionController.detectNfts.bind(nftDetectionController)
: null,
/** Token Detection V2 */
@ -2040,10 +2043,10 @@ export default class MetamaskController extends EventEmitter {
}
}
async addCustomNetwork(customRpc) {
async addCustomNetwork(customRpc, actionId) {
const { chainId, chainName, rpcUrl, ticker, blockExplorerUrl } = customRpc;
await this.preferencesController.addToFrequentRpcList(
this.preferencesController.addToFrequentRpcList(
rpcUrl,
chainId,
ticker,
@ -2076,6 +2079,7 @@ export default class MetamaskController extends EventEmitter {
sensitiveProperties: {
rpc_url: rpcUrlOrigin,
},
actionId,
});
}
@ -2758,7 +2762,7 @@ export default class MetamaskController extends EventEmitter {
const allAccounts = await this.keyringController.getAccounts();
this.preferencesController.setAddresses(allAccounts);
// set new account as selected
await this.preferencesController.setSelectedAddress(firstAccount);
this.preferencesController.setSelectedAddress(firstAccount);
}
// ---------------------------------------------------------------------------
@ -3220,7 +3224,7 @@ export default class MetamaskController extends EventEmitter {
customGasSettings,
options,
);
const state = await this.getState();
const state = this.getState();
return state;
}
@ -3243,7 +3247,7 @@ export default class MetamaskController extends EventEmitter {
customGasSettings,
options,
);
const state = await this.getState();
const state = this.getState();
return state;
}

@ -0,0 +1,46 @@
import { cloneDeep } from 'lodash';
const version = 76;
/**
* Update to `@metamask/controllers@33.0.0` (rename "Collectible" to "NFT").
*/
export default {
version,
async migrate(originalVersionedData) {
const versionedData = cloneDeep(originalVersionedData);
versionedData.meta.version = version;
const state = versionedData.data;
const newState = transformState(state);
versionedData.data = newState;
return versionedData;
},
};
function transformState(state) {
if (state.CollectiblesController) {
const {
allCollectibleContracts,
allCollectibles,
ignoredCollectibles,
...remainingState
} = state.CollectiblesController;
state.NftController = {
...(allCollectibleContracts
? { allNftContracts: allCollectibleContracts }
: {}),
...(allCollectibles ? { allNfts: allCollectibles } : {}),
...(ignoredCollectibles ? { ignoredNfts: ignoredCollectibles } : {}),
...remainingState,
};
delete state.CollectiblesController;
}
if (state.PreferencesController?.useCollectibleDetection) {
state.PreferencesController.useNftDetection =
state.PreferencesController.useCollectibleDetection;
delete state.PreferencesController.useCollectibleDetection;
}
return state;
}

@ -0,0 +1,143 @@
import migration76 from './076';
describe('migration #76', () => {
it('should update the version metadata', async () => {
const oldStorage = {
meta: {
version: 75,
},
data: {},
};
const newStorage = await migration76.migrate(oldStorage);
expect(newStorage.meta).toStrictEqual({
version: 76,
});
});
it('should migrate known controller state properties', async () => {
const oldStorage = {
meta: {
version: 75,
},
data: {
CollectiblesController: {
allCollectibleContracts: 'foo',
allCollectibles: 'bar',
ignoredCollectibles: 'baz',
},
PreferencesController: {
useCollectibleDetection: 'foobar',
},
},
};
const newStorage = await migration76.migrate(oldStorage);
expect(newStorage).toStrictEqual({
meta: {
version: 76,
},
data: {
NftController: {
allNftContracts: 'foo',
allNfts: 'bar',
ignoredNfts: 'baz',
},
PreferencesController: {
useNftDetection: 'foobar',
},
},
});
});
it('should migrate unknown controller state properties', async () => {
const oldStorage = {
meta: {
version: 75,
},
data: {
CollectiblesController: {
allCollectibleContracts: 'foo',
allCollectibles: 'bar',
ignoredCollectibles: 'baz',
extra: 'extra',
},
PreferencesController: {
extra: 'extra',
useCollectibleDetection: 'foobar',
},
},
};
const newStorage = await migration76.migrate(oldStorage);
expect(newStorage).toStrictEqual({
meta: {
version: 76,
},
data: {
NftController: {
allNftContracts: 'foo',
allNfts: 'bar',
ignoredNfts: 'baz',
extra: 'extra',
},
PreferencesController: {
extra: 'extra',
useNftDetection: 'foobar',
},
},
});
});
it('should handle missing controller state', async () => {
const oldStorage = {
meta: {
version: 75,
},
data: {
CollectiblesController: {
extra: 'extra',
},
PreferencesController: {
extra: 'extra',
},
},
};
const newStorage = await migration76.migrate(oldStorage);
expect(newStorage).toStrictEqual({
meta: {
version: 76,
},
data: {
NftController: {
extra: 'extra',
},
PreferencesController: {
extra: 'extra',
},
},
});
});
it('should handle missing CollectiblesController and PreferencesController', async () => {
const oldStorage = {
meta: {
version: 75,
},
data: {
FooController: { a: 'b' },
},
};
const newStorage = await migration76.migrate(oldStorage);
expect(newStorage).toStrictEqual({
meta: {
version: 76,
},
data: {
FooController: { a: 'b' },
},
});
});
});

@ -79,6 +79,7 @@ import m072 from './072';
import m073 from './073';
import m074 from './074';
import m075 from './075';
import m076 from './076';
const migrations = [
m002,
@ -155,6 +156,7 @@ const migrations = [
m073,
m074,
m075,
m076,
];
export default migrations;

@ -1,10 +1,10 @@
import setupSentry from './lib/setupSentry';
// The root compartment will populate this with hooks
global.sentryHooks = {};
global.stateHooks = {};
// setup sentry error reporting
global.sentry = setupSentry({
release: process.env.METAMASK_VERSION,
getState: () => global.sentryHooks?.getSentryState?.() || {},
getState: () => global.stateHooks?.getSentryState?.() || {},
});

@ -30,22 +30,65 @@ const container = document.getElementById('app-content');
const ONE_SECOND_IN_MILLISECONDS = 1_000;
const WORKER_KEEP_ALIVE_INTERVAL = ONE_SECOND_IN_MILLISECONDS;
// Service Worker Keep Alive Message Constants
const WORKER_KEEP_ALIVE_MESSAGE = 'WORKER_KEEP_ALIVE_MESSAGE';
const ACK_KEEP_ALIVE_WAIT_TIME = 60_000; // 1 minute
const ACK_KEEP_ALIVE_MESSAGE = 'ACK_KEEP_ALIVE_MESSAGE';
// Timeout for initializing phishing warning page.
const PHISHING_WARNING_PAGE_TIMEOUT = ONE_SECOND_IN_MILLISECONDS;
const PHISHING_WARNING_SW_STORAGE_KEY = 'phishing-warning-sw-registered';
let lastMessageRecievedTimestamp = Date.now();
/*
* As long as UI is open it will keep sending messages to service worker
* In service worker as this message is received
* if service worker is inactive it is reactivated and script re-loaded
* Time has been kept to 1000ms but can be reduced for even faster re-activation of service worker
*/
let extensionPort;
let timeoutHandle;
if (isManifestV3) {
setInterval(() => {
// Checking for SW aliveness (or stuckness) flow
// 1. Check if we have an extensionPort, if yes
// 2a. Send a keep alive message to the background via extensionPort
// 2b. Add a listener to it (if not already added)
// 3a. Set a timeout to check if we have received an ACK from background
// 3b. If we have not received an ACK within Xs, we know the background is stuck or dead
// 4. If we recieve an ACK_KEEP_ALIVE_MESSAGE from the service worker, we know it is alive
const ackKeepAliveListener = (message) => {
if (message.name === ACK_KEEP_ALIVE_MESSAGE) {
lastMessageRecievedTimestamp = Date.now();
clearTimeout(timeoutHandle);
}
};
const handle = setInterval(() => {
browser.runtime.sendMessage({ name: WORKER_KEEP_ALIVE_MESSAGE });
if (extensionPort !== null && extensionPort !== undefined) {
extensionPort.postMessage({ name: WORKER_KEEP_ALIVE_MESSAGE });
if (extensionPort.onMessage.hasListener(ackKeepAliveListener) === false) {
extensionPort.onMessage.addListener(ackKeepAliveListener);
}
}
timeoutHandle = setTimeout(() => {
if (
Date.now() - lastMessageRecievedTimestamp >
ACK_KEEP_ALIVE_WAIT_TIME
) {
clearInterval(handle);
displayCriticalError(
'somethingIsWrong',
new Error("Something's gone wrong. Try reloading the page."),
);
}
}, ACK_KEEP_ALIVE_WAIT_TIME);
}, WORKER_KEEP_ALIVE_INTERVAL);
}
@ -61,7 +104,7 @@ async function start() {
let isUIInitialised = false;
// setup stream to background
let extensionPort = browser.runtime.connect({ name: windowType });
extensionPort = browser.runtime.connect({ name: windowType });
let connectionStream = new PortStream(extensionPort);
const activeTab = await queryCurrentActiveTab(windowType);
@ -208,7 +251,7 @@ async function start() {
initializeUi(tab, connectionStream, (err, store) => {
if (err) {
// if there's an error, store will be = metamaskState
displayCriticalError(err, store);
displayCriticalError('troubleStarting', err, store);
return;
}
isUIInitialised = true;
@ -226,7 +269,7 @@ async function start() {
function updateUiStreams() {
connectToAccountManager(connectionStream, (err, backgroundConnection) => {
if (err) {
displayCriticalError(err);
displayCriticalError('troubleStarting', err);
return;
}
@ -277,8 +320,8 @@ function initializeUi(activeTab, connectionStream, cb) {
});
}
async function displayCriticalError(err, metamaskState) {
const html = await getErrorHtml(SUPPORT_LINK, metamaskState);
async function displayCriticalError(errorKey, err, metamaskState) {
const html = await getErrorHtml(errorKey, SUPPORT_LINK, metamaskState);
container.innerHTML = html;

@ -71,7 +71,7 @@ async function defineAndRunBuildTasks() {
version,
} = await parseArgv();
const browserPlatforms = ['firefox', 'chrome', 'brave', 'opera'];
const browserPlatforms = ['firefox', 'chrome'];
const browserVersionMap = getBrowserVersionMap(browserPlatforms, version);

@ -58,10 +58,8 @@ function createStyleTasks({ livereload }) {
};
async function buildScss() {
await Promise.all([
buildScssPipeline(src, dest, devMode, false),
buildScssPipeline(src, dest, devMode, true),
]);
await buildScssPipeline(src, dest, devMode, false);
await buildScssPipeline(src, dest, devMode, true);
}
}
}

@ -4,7 +4,7 @@
* Reads all the icon svg files in app/images/icons
* and returns an object of icon name key value pairs
* stored in the environment variable ICON_NAMES
* Used with the Icon component in ./ui/component-library/icon
* Used with the Icon component in ./ui/components/component-library/icon/icon.js
*/
const fs = require('fs');
const path = require('path');

@ -23,6 +23,8 @@ async function start() {
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);
const { PARENT_COMMIT } = process.env;
console.log('PARENT_COMMIT', PARENT_COMMIT);
if (!CIRCLE_PULL_REQUEST) {
console.warn(`No pull request detected for commit "${CIRCLE_SHA1}"`);
@ -36,7 +38,7 @@ async function start() {
// build the github comment content
// links to extension builds
const platforms = ['chrome', 'firefox', 'opera'];
const platforms = ['chrome', 'firefox'];
const buildLinks = platforms
.map((platform) => {
const url = `${BUILD_LINK_BASE}/builds/metamask-${platform}-${VERSION}.zip`;
@ -87,6 +89,9 @@ async function start() {
.map((key) => `<li>${key}: ${bundles[key].join(', ')}</li>`)
.join('')}</ul>`;
const bundleSizeDataUrl =
'https://raw.githubusercontent.com/MetaMask/extension_bundlesize_stats/main/stats/bundle_size_data.json';
const coverageUrl = `${BUILD_LINK_BASE}/coverage/index.html`;
const coverageLink = `<a href="${coverageUrl}">Report</a>`;
@ -243,6 +248,67 @@ async function start() {
console.log(`No results for ${summaryPlatform} found; skipping benchmark`);
}
try {
const prBundleSizeStats = JSON.parse(
await fs.readFile(
path.resolve(
__dirname,
'..',
path.join('test-artifacts', 'chrome', 'mv3', 'bundle_size.json'),
),
'utf-8',
),
);
const devBundleSizeStats = await (
await fetch(bundleSizeDataUrl, {
method: 'GET',
})
).json();
const prSizes = {
background: prBundleSizeStats.background.size,
ui: prBundleSizeStats.ui.size,
common: prBundleSizeStats.common.size,
};
const devSizes = Object.keys(prSizes).reduce((sizes, part) => {
sizes[part] = devBundleSizeStats[PARENT_COMMIT][part] || 0;
return sizes;
}, {});
const diffs = Object.keys(prSizes).reduce((output, part) => {
output[part] = prSizes[part] - devSizes[part];
return output;
}, {});
const sizeDiffRows = Object.keys(diffs).map(
(part) => `${part}: ${diffs[part]} bytes`,
);
const sizeDiffHiddenContent = `<ul>${sizeDiffRows
.map((row) => `<li>${row}</li>`)
.join('\n')}</ul>`;
const sizeDiff = diffs.background + diffs.common;
const sizeDiffWarning =
sizeDiff > 0
? `🚨 Warning! Bundle size has increased!`
: `🚀 Bundle size reduced!`;
const sizeDiffExposedContent =
sizeDiff === 0
? `Bundle size diffs`
: `Bundle size diffs [${sizeDiffWarning}]`;
const sizeDiffBody = `<details><summary>${sizeDiffExposedContent}</summary>${sizeDiffHiddenContent}</details>\n\n`;
commentBody += sizeDiffBody;
} catch (error) {
console.error(`Error constructing bundle size diffs results: '${error}'`);
}
try {
const highlights = await getHighlights({ artifactBase: BUILD_LINK_BASE });
if (highlights) {

@ -698,7 +698,7 @@
"ui/components/app/signature-request/signature-request-header/signature-request-header.component.js",
"ui/components/app/signature-request/signature-request-header/signature-request-header.stories.js",
"ui/components/app/signature-request/signature-request-message/index.js",
"ui/components/app/signature-request/signature-request-message/signature-request-message.component.js",
"ui/components/app/signature-request/signature-request-message/signature-request-message.js",
"ui/components/app/signature-request/signature-request.component.js",
"ui/components/app/signature-request/signature-request.component.test.js",
"ui/components/app/signature-request/signature-request.container.js",

@ -2,6 +2,7 @@
Steps to mark a full pass of QA complete.
* Browsers: Opera, Chrome, Firefox, Edge.
* Use the Chrome build for all Chromium-derived browsers (e.g. Opera and Edge)
* OS: Ubuntu, Mac OSX, Windows
* Load older version of MetaMask and attempt to simulate updating the extension.
* Open Developer Console in background and popup, inspect errors.

@ -4,12 +4,12 @@ Fixture data can be generated by following these steps:
1. Load the unpacked extension in development or test mode
2. Inspecting the background context of the extension
3. Call `metamaskGetState`, then call [`copy`][1] on the results
3. Call `stateHooks.metamaskGetState`, then call [`copy`][1] on the results
You can then paste the contents directly in your fixture file.
```js
copy(await metamaskGetState())
copy(await stateHooks.metamaskGetState())
```

@ -1238,14 +1238,145 @@
"packages": {
"@ethersproject/bignumber": true,
"@ethersproject/bignumber>@ethersproject/bytes": true,
"@metamask/controllers": true,
"@metamask/controllers>@ethersproject/providers": true,
"@metamask/controllers>isomorphic-fetch": true,
"@metamask/smart-transactions-controller>@metamask/controllers": true,
"@metamask/smart-transactions-controller>bignumber.js": true,
"@metamask/smart-transactions-controller>fast-json-patch": true,
"lodash": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers": {
"globals": {
"Headers": true,
"URL": true,
"clearInterval": true,
"clearTimeout": true,
"console.error": true,
"console.log": true,
"fetch": true,
"setInterval": true,
"setTimeout": true
},
"packages": {
"@ethereumjs/common": true,
"@ethereumjs/tx": true,
"@metamask/contract-metadata": true,
"@metamask/controllers>@ethersproject/abi": true,
"@metamask/controllers>@ethersproject/contracts": true,
"@metamask/controllers>@ethersproject/providers": true,
"@metamask/controllers>abort-controller": true,
"@metamask/controllers>async-mutex": true,
"@metamask/controllers>eth-json-rpc-infura": true,
"@metamask/controllers>eth-phishing-detect": true,
"@metamask/controllers>isomorphic-fetch": true,
"@metamask/controllers>multiformats": true,
"@metamask/controllers>web3": true,
"@metamask/controllers>web3-provider-engine": true,
"@metamask/metamask-eth-abis": true,
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry": true,
"@metamask/smart-transactions-controller>@metamask/controllers>ethereumjs-wallet": true,
"@metamask/smart-transactions-controller>@metamask/controllers>nanoid": true,
"browserify>buffer": true,
"browserify>events": true,
"deep-freeze-strict": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
"eth-keyring-controller": true,
"eth-query": true,
"eth-rpc-errors": true,
"eth-sig-util": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true,
"immer": true,
"json-rpc-engine": true,
"jsonschema": true,
"punycode": true,
"single-call-balance-checker-abi": true,
"uuid": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry": {
"packages": {
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs": {
"globals": {
"clearInterval": true,
"setInterval": true
},
"packages": {
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>bn.js": true,
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>ethjs-abi": true,
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>ethjs-contract": true,
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>ethjs-query": true,
"browserify>buffer": true,
"ethjs>ethjs-filter": true,
"ethjs>ethjs-provider-http": true,
"ethjs>ethjs-unit": true,
"ethjs>ethjs-util": true,
"ethjs>js-sha3": true,
"ethjs>number-to-bn": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>ethjs-abi": {
"packages": {
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>bn.js": true,
"browserify>buffer": true,
"ethjs>js-sha3": true,
"ethjs>number-to-bn": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>ethjs-contract": {
"packages": {
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>ethjs-contract>ethjs-abi": true,
"ethjs-query>babel-runtime": true,
"ethjs>ethjs-filter": true,
"ethjs>ethjs-util": true,
"ethjs>js-sha3": true,
"promise-to-callback": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>ethjs-contract>ethjs-abi": {
"packages": {
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>bn.js": true,
"browserify>buffer": true,
"ethjs>js-sha3": true,
"ethjs>number-to-bn": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>ethjs-query": {
"globals": {
"console": true
},
"packages": {
"ethjs-query>babel-runtime": true,
"ethjs-query>ethjs-format": true,
"ethjs-query>ethjs-rpc": true,
"promise-to-callback": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers>ethereumjs-wallet": {
"packages": {
"@metamask/smart-transactions-controller>@metamask/controllers>ethereumjs-wallet>uuid": true,
"@truffle/codec>utf8": true,
"browserify>buffer": true,
"browserify>crypto-browserify": true,
"ethereumjs-util": true,
"ethereumjs-util>ethereum-cryptography": true,
"ethereumjs-wallet>aes-js": true,
"ethereumjs-wallet>bs58check": true,
"ethereumjs-wallet>randombytes": true,
"ethers>@ethersproject/json-wallets>scrypt-js": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers>ethereumjs-wallet>uuid": {
"globals": {
"crypto": true,
"msCrypto": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers>nanoid": {
"globals": {
"crypto.getRandomValues": true

@ -1366,6 +1366,50 @@
"watchify>xtend": true
}
},
"@metamask/post-message-stream": {
"globals": {
"WorkerGlobalScope": true,
"addEventListener": true,
"location.origin": true,
"onmessage": "write",
"postMessage": true,
"removeEventListener": true
},
"packages": {
"@metamask/post-message-stream>@metamask/utils": true,
"@metamask/post-message-stream>readable-stream": true
}
},
"@metamask/post-message-stream>@metamask/utils": {
"packages": {
"eslint>fast-deep-equal": true
}
},
"@metamask/post-message-stream>readable-stream": {
"packages": {
"@metamask/post-message-stream>readable-stream>safe-buffer": true,
"@metamask/post-message-stream>readable-stream>string_decoder": true,
"@storybook/api>util-deprecate": true,
"browserify>browser-resolve": true,
"browserify>events": true,
"browserify>process": true,
"browserify>timers-browserify": true,
"pumpify>inherits": true,
"readable-stream>core-util-is": true,
"readable-stream>isarray": true,
"vinyl>cloneable-readable>process-nextick-args": true
}
},
"@metamask/post-message-stream>readable-stream>safe-buffer": {
"packages": {
"browserify>buffer": true
}
},
"@metamask/post-message-stream>readable-stream>string_decoder": {
"packages": {
"@metamask/post-message-stream>readable-stream>safe-buffer": true
}
},
"@metamask/providers>@metamask/object-multiplex": {
"globals": {
"console.warn": true
@ -1528,7 +1572,7 @@
"@metamask/rpc-methods>@metamask/key-tree>@scure/bip39": true,
"@metamask/snap-utils>@noble/hashes": true,
"@metamask/snap-utils>@scure/base": true,
"browserify>buffer": true
"eth-block-tracker>@metamask/utils": true
}
},
"@metamask/rpc-methods>@metamask/key-tree>@noble/ed25519": {
@ -1570,14 +1614,145 @@
"packages": {
"@ethersproject/bignumber": true,
"@ethersproject/bignumber>@ethersproject/bytes": true,
"@metamask/controllers": true,
"@metamask/controllers>@ethersproject/providers": true,
"@metamask/controllers>isomorphic-fetch": true,
"@metamask/smart-transactions-controller>@metamask/controllers": true,
"@metamask/smart-transactions-controller>bignumber.js": true,
"@metamask/smart-transactions-controller>fast-json-patch": true,
"lodash": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers": {
"globals": {
"Headers": true,
"URL": true,
"clearInterval": true,
"clearTimeout": true,
"console.error": true,
"console.log": true,
"fetch": true,
"setInterval": true,
"setTimeout": true
},
"packages": {
"@ethereumjs/common": true,
"@ethereumjs/tx": true,
"@metamask/contract-metadata": true,
"@metamask/controllers>@ethersproject/abi": true,
"@metamask/controllers>@ethersproject/contracts": true,
"@metamask/controllers>@ethersproject/providers": true,
"@metamask/controllers>abort-controller": true,
"@metamask/controllers>async-mutex": true,
"@metamask/controllers>eth-json-rpc-infura": true,
"@metamask/controllers>eth-phishing-detect": true,
"@metamask/controllers>isomorphic-fetch": true,
"@metamask/controllers>multiformats": true,
"@metamask/controllers>web3": true,
"@metamask/controllers>web3-provider-engine": true,
"@metamask/metamask-eth-abis": true,
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry": true,
"@metamask/smart-transactions-controller>@metamask/controllers>ethereumjs-wallet": true,
"@metamask/smart-transactions-controller>@metamask/controllers>nanoid": true,
"browserify>buffer": true,
"browserify>events": true,
"deep-freeze-strict": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
"eth-keyring-controller": true,
"eth-query": true,
"eth-rpc-errors": true,
"eth-sig-util": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true,
"immer": true,
"json-rpc-engine": true,
"jsonschema": true,
"punycode": true,
"single-call-balance-checker-abi": true,
"uuid": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry": {
"packages": {
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs": {
"globals": {
"clearInterval": true,
"setInterval": true
},
"packages": {
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>bn.js": true,
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>ethjs-abi": true,
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>ethjs-contract": true,
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>ethjs-query": true,
"browserify>buffer": true,
"ethjs>ethjs-filter": true,
"ethjs>ethjs-provider-http": true,
"ethjs>ethjs-unit": true,
"ethjs>ethjs-util": true,
"ethjs>js-sha3": true,
"ethjs>number-to-bn": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>ethjs-abi": {
"packages": {
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>bn.js": true,
"browserify>buffer": true,
"ethjs>js-sha3": true,
"ethjs>number-to-bn": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>ethjs-contract": {
"packages": {
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>ethjs-contract>ethjs-abi": true,
"ethjs-query>babel-runtime": true,
"ethjs>ethjs-filter": true,
"ethjs>ethjs-util": true,
"ethjs>js-sha3": true,
"promise-to-callback": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>ethjs-contract>ethjs-abi": {
"packages": {
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>bn.js": true,
"browserify>buffer": true,
"ethjs>js-sha3": true,
"ethjs>number-to-bn": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>ethjs-query": {
"globals": {
"console": true
},
"packages": {
"ethjs-query>babel-runtime": true,
"ethjs-query>ethjs-format": true,
"ethjs-query>ethjs-rpc": true,
"promise-to-callback": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers>ethereumjs-wallet": {
"packages": {
"@metamask/smart-transactions-controller>@metamask/controllers>ethereumjs-wallet>uuid": true,
"@truffle/codec>utf8": true,
"browserify>buffer": true,
"browserify>crypto-browserify": true,
"ethereumjs-util": true,
"ethereumjs-util>ethereum-cryptography": true,
"ethereumjs-wallet>aes-js": true,
"ethereumjs-wallet>bs58check": true,
"ethereumjs-wallet>randombytes": true,
"ethers>@ethersproject/json-wallets>scrypt-js": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers>ethereumjs-wallet>uuid": {
"globals": {
"crypto": true,
"msCrypto": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers>nanoid": {
"globals": {
"crypto.getRandomValues": true
@ -1612,11 +1787,11 @@
"setTimeout": true
},
"packages": {
"@metamask/post-message-stream": true,
"@metamask/providers>@metamask/object-multiplex": true,
"@metamask/rpc-methods": true,
"@metamask/snap-controllers>@metamask/browser-passworder": true,
"@metamask/snap-controllers>@metamask/controllers": true,
"@metamask/snap-controllers>@metamask/post-message-stream": true,
"@metamask/snap-controllers>@xstate/fsm": true,
"@metamask/snap-controllers>concat-stream": true,
"@metamask/snap-controllers>gunzip-maybe": true,
@ -1775,45 +1950,6 @@
"msCrypto": true
}
},
"@metamask/snap-controllers>@metamask/post-message-stream": {
"globals": {
"WorkerGlobalScope": true,
"addEventListener": true,
"location.origin": true,
"onmessage": "write",
"postMessage": true,
"removeEventListener": true
},
"packages": {
"@metamask/snap-controllers>@metamask/post-message-stream>@metamask/utils": true,
"@metamask/snap-controllers>@metamask/post-message-stream>readable-stream": true
}
},
"@metamask/snap-controllers>@metamask/post-message-stream>@metamask/utils": {
"packages": {
"eslint>fast-deep-equal": true
}
},
"@metamask/snap-controllers>@metamask/post-message-stream>readable-stream": {
"packages": {
"@metamask/snap-controllers>@metamask/post-message-stream>readable-stream>string_decoder": true,
"@metamask/snap-controllers>json-rpc-middleware-stream>readable-stream>safe-buffer": true,
"@storybook/api>util-deprecate": true,
"browserify>browser-resolve": true,
"browserify>events": true,
"browserify>process": true,
"browserify>timers-browserify": true,
"pumpify>inherits": true,
"readable-stream>core-util-is": true,
"readable-stream>isarray": true,
"vinyl>cloneable-readable>process-nextick-args": true
}
},
"@metamask/snap-controllers>@metamask/post-message-stream>readable-stream>string_decoder": {
"packages": {
"@metamask/snap-controllers>json-rpc-middleware-stream>readable-stream>safe-buffer": true
}
},
"@metamask/snap-controllers>concat-stream": {
"packages": {
"@metamask/snap-controllers>concat-stream>readable-stream": true,
@ -1915,38 +2051,8 @@
"setTimeout": true
},
"packages": {
"@metamask/snap-controllers>json-rpc-middleware-stream>readable-stream": true,
"json-rpc-engine>@metamask/safe-event-emitter": true
}
},
"@metamask/snap-controllers>json-rpc-middleware-stream>readable-stream": {
"packages": {
"@metamask/snap-controllers>json-rpc-middleware-stream>readable-stream>process-nextick-args": true,
"@metamask/snap-controllers>json-rpc-middleware-stream>readable-stream>safe-buffer": true,
"@metamask/snap-controllers>json-rpc-middleware-stream>readable-stream>string_decoder": true,
"@storybook/api>util-deprecate": true,
"browserify>browser-resolve": true,
"browserify>events": true,
"browserify>process": true,
"browserify>timers-browserify": true,
"pumpify>inherits": true,
"readable-stream>core-util-is": true,
"readable-stream>isarray": true
}
},
"@metamask/snap-controllers>json-rpc-middleware-stream>readable-stream>process-nextick-args": {
"packages": {
"browserify>process": true
}
},
"@metamask/snap-controllers>json-rpc-middleware-stream>readable-stream>safe-buffer": {
"packages": {
"browserify>buffer": true
}
},
"@metamask/snap-controllers>json-rpc-middleware-stream>readable-stream>string_decoder": {
"packages": {
"@metamask/snap-controllers>json-rpc-middleware-stream>readable-stream>safe-buffer": true
"json-rpc-engine>@metamask/safe-event-emitter": true,
"readable-stream": true
}
},
"@metamask/snap-controllers>nanoid": {
@ -2008,7 +2114,7 @@
"@babel/core>@babel/types": true,
"@metamask/snap-utils>@noble/hashes": true,
"@metamask/snap-utils>@scure/base": true,
"@metamask/snap-utils>ajv": true,
"@metamask/snap-utils>cron-parser": true,
"@metamask/snap-utils>rfdc": true,
"@metamask/snap-utils>superstruct": true,
"browserify": true,
@ -2033,6 +2139,12 @@
"TextEncoder": true
}
},
"@metamask/snap-utils>cron-parser": {
"packages": {
"browserify>browser-resolve": true,
"luxon": true
}
},
"@metamask/snap-utils>rfdc": {
"packages": {
"browserify>buffer": true

@ -1238,14 +1238,145 @@
"packages": {
"@ethersproject/bignumber": true,
"@ethersproject/bignumber>@ethersproject/bytes": true,
"@metamask/controllers": true,
"@metamask/controllers>@ethersproject/providers": true,
"@metamask/controllers>isomorphic-fetch": true,
"@metamask/smart-transactions-controller>@metamask/controllers": true,
"@metamask/smart-transactions-controller>bignumber.js": true,
"@metamask/smart-transactions-controller>fast-json-patch": true,
"lodash": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers": {
"globals": {
"Headers": true,
"URL": true,
"clearInterval": true,
"clearTimeout": true,
"console.error": true,
"console.log": true,
"fetch": true,
"setInterval": true,
"setTimeout": true
},
"packages": {
"@ethereumjs/common": true,
"@ethereumjs/tx": true,
"@metamask/contract-metadata": true,
"@metamask/controllers>@ethersproject/abi": true,
"@metamask/controllers>@ethersproject/contracts": true,
"@metamask/controllers>@ethersproject/providers": true,
"@metamask/controllers>abort-controller": true,
"@metamask/controllers>async-mutex": true,
"@metamask/controllers>eth-json-rpc-infura": true,
"@metamask/controllers>eth-phishing-detect": true,
"@metamask/controllers>isomorphic-fetch": true,
"@metamask/controllers>multiformats": true,
"@metamask/controllers>web3": true,
"@metamask/controllers>web3-provider-engine": true,
"@metamask/metamask-eth-abis": true,
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry": true,
"@metamask/smart-transactions-controller>@metamask/controllers>ethereumjs-wallet": true,
"@metamask/smart-transactions-controller>@metamask/controllers>nanoid": true,
"browserify>buffer": true,
"browserify>events": true,
"deep-freeze-strict": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
"eth-keyring-controller": true,
"eth-query": true,
"eth-rpc-errors": true,
"eth-sig-util": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true,
"immer": true,
"json-rpc-engine": true,
"jsonschema": true,
"punycode": true,
"single-call-balance-checker-abi": true,
"uuid": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry": {
"packages": {
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs": {
"globals": {
"clearInterval": true,
"setInterval": true
},
"packages": {
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>bn.js": true,
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>ethjs-abi": true,
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>ethjs-contract": true,
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>ethjs-query": true,
"browserify>buffer": true,
"ethjs>ethjs-filter": true,
"ethjs>ethjs-provider-http": true,
"ethjs>ethjs-unit": true,
"ethjs>ethjs-util": true,
"ethjs>js-sha3": true,
"ethjs>number-to-bn": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>ethjs-abi": {
"packages": {
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>bn.js": true,
"browserify>buffer": true,
"ethjs>js-sha3": true,
"ethjs>number-to-bn": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>ethjs-contract": {
"packages": {
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>ethjs-contract>ethjs-abi": true,
"ethjs-query>babel-runtime": true,
"ethjs>ethjs-filter": true,
"ethjs>ethjs-util": true,
"ethjs>js-sha3": true,
"promise-to-callback": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>ethjs-contract>ethjs-abi": {
"packages": {
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>bn.js": true,
"browserify>buffer": true,
"ethjs>js-sha3": true,
"ethjs>number-to-bn": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers>eth-method-registry>ethjs>ethjs-query": {
"globals": {
"console": true
},
"packages": {
"ethjs-query>babel-runtime": true,
"ethjs-query>ethjs-format": true,
"ethjs-query>ethjs-rpc": true,
"promise-to-callback": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers>ethereumjs-wallet": {
"packages": {
"@metamask/smart-transactions-controller>@metamask/controllers>ethereumjs-wallet>uuid": true,
"@truffle/codec>utf8": true,
"browserify>buffer": true,
"browserify>crypto-browserify": true,
"ethereumjs-util": true,
"ethereumjs-util>ethereum-cryptography": true,
"ethereumjs-wallet>aes-js": true,
"ethereumjs-wallet>bs58check": true,
"ethereumjs-wallet>randombytes": true,
"ethers>@ethersproject/json-wallets>scrypt-js": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers>ethereumjs-wallet>uuid": {
"globals": {
"crypto": true,
"msCrypto": true
}
},
"@metamask/smart-transactions-controller>@metamask/controllers>nanoid": {
"globals": {
"crypto.getRandomValues": true

@ -1187,6 +1187,16 @@
"typescript": true
}
},
"addons-linter>postcss>picocolors": {
"builtin": {
"tty.isatty": true
},
"globals": {
"process.argv.includes": true,
"process.env": true,
"process.platform": true
}
},
"babelify": {
"builtin": {
"path.extname": true,
@ -3206,106 +3216,58 @@
"fancy-log": true,
"gulp-autoprefixer>autoprefixer": true,
"gulp-autoprefixer>postcss": true,
"gulp-autoprefixer>through2": true,
"gulp-zip>plugin-error": true,
"through2": true,
"vinyl-sourcemaps-apply": true
}
},
"gulp-autoprefixer>autoprefixer": {
"globals": {
"process.cwd": true
"console": true,
"process.cwd": true,
"process.env.AUTOPREFIXER_GRID": true
},
"packages": {
"gulp-autoprefixer>autoprefixer>browserslist": true,
"gulp-autoprefixer>autoprefixer>postcss-value-parser": true,
"addons-linter>postcss>picocolors": true,
"gulp-autoprefixer>autoprefixer>fraction.js": true,
"gulp-autoprefixer>postcss": true,
"stylelint>autoprefixer>browserslist": true,
"stylelint>autoprefixer>caniuse-lite": true,
"stylelint>autoprefixer>normalize-range": true,
"stylelint>autoprefixer>num2fraction": true
"stylelint>postcss-value-parser": true
}
},
"gulp-autoprefixer>autoprefixer>browserslist": {
"builtin": {
"fs.existsSync": true,
"fs.readFileSync": true,
"fs.statSync": true,
"path.basename": true,
"path.dirname": true,
"path.join": true,
"path.resolve": true
},
"gulp-autoprefixer>autoprefixer>fraction.js": {
"globals": {
"console.warn": true,
"process.env.BROWSERSLIST": true,
"process.env.BROWSERSLIST_CONFIG": true,
"process.env.BROWSERSLIST_DISABLE_CACHE": true,
"process.env.BROWSERSLIST_ENV": true,
"process.env.BROWSERSLIST_STATS": true,
"process.env.NODE_ENV": true
},
"packages": {
"stylelint>autoprefixer>browserslist>electron-to-chromium": true,
"stylelint>autoprefixer>caniuse-lite": true
"define": true
}
},
"gulp-autoprefixer>postcss": {
"builtin": {
"fs": true,
"path": true
"fs.existsSync": true,
"fs.readFileSync": true,
"path.dirname": true,
"path.isAbsolute": true,
"path.join": true,
"path.relative": true,
"path.resolve": true,
"path.sep": true,
"url.fileURLToPath": true,
"url.pathToFileURL": true
},
"globals": {
"Buffer": true,
"URL": true,
"atob": true,
"btoa": true,
"console": true
},
"packages": {
"gulp-autoprefixer>postcss>chalk": true,
"gulp-autoprefixer>postcss>source-map": true,
"gulp-autoprefixer>postcss>supports-color": true
}
},
"gulp-autoprefixer>postcss>chalk": {
"globals": {
"process.env.TERM": true,
"process.platform": true
},
"packages": {
"gulp-autoprefixer>postcss>chalk>ansi-styles": true,
"gulp-autoprefixer>postcss>supports-color": true,
"mocha>escape-string-regexp": true
}
},
"gulp-autoprefixer>postcss>chalk>ansi-styles": {
"packages": {
"@metamask/jazzicon>color>color-convert": true
}
},
"gulp-autoprefixer>postcss>supports-color": {
"builtin": {
"os.release": true
},
"globals": {
"process.env": true,
"process.platform": true,
"process.stderr": true,
"process.stdout": true,
"process.versions.node.split": true
},
"packages": {
"mocha>supports-color>has-flag": true
}
},
"gulp-autoprefixer>through2": {
"builtin": {
"util.inherits": true
},
"globals": {
"process.nextTick": true
"console": true,
"process.env.LANG": true,
"process.env.NODE_ENV": true
},
"packages": {
"readable-stream": true,
"watchify>xtend": true
"addons-linter>postcss>picocolors": true,
"addons-linter>postcss>source-map-js": true,
"gulp-autoprefixer>postcss>nanoid": true
}
},
"gulp-dart-sass": {
@ -6453,9 +6415,9 @@
"stylelint>autoprefixer>caniuse-lite": true,
"stylelint>autoprefixer>normalize-range": true,
"stylelint>autoprefixer>num2fraction": true,
"stylelint>autoprefixer>picocolors": true,
"stylelint>postcss": true,
"stylelint>postcss-value-parser": true,
"stylelint>postcss>picocolors": true
"stylelint>postcss-value-parser": true
}
},
"stylelint>autoprefixer>browserslist": {
@ -6486,6 +6448,16 @@
"stylelint>autoprefixer>caniuse-lite": true
}
},
"stylelint>autoprefixer>picocolors": {
"builtin": {
"tty.isatty": true
},
"globals": {
"process.argv.includes": true,
"process.env": true,
"process.platform": true
}
},
"stylelint>chalk": {
"packages": {
"chalk>ansi-styles": true,

@ -84,7 +84,6 @@
},
"resolutions": {
"**/regenerator-runtime": "^0.13.7",
"**/caniuse-lite": "^1.0.30001312",
"**/cross-fetch": "^3.1.5",
"**/configstore/dot-prop": "^5.1.1",
"**/ethers/elliptic": "^6.5.4",
@ -112,7 +111,7 @@
"@keystonehq/metamask-airgapped-keyring": "^0.6.1",
"@material-ui/core": "^4.11.0",
"@metamask/contract-metadata": "^1.31.0",
"@metamask/controllers": "^32.0.2",
"@metamask/controllers": "^33.0.0",
"@metamask/design-tokens": "^1.9.0",
"@metamask/eth-json-rpc-infura": "^7.0.0",
"@metamask/eth-ledger-bridge-keyring": "^0.13.0",
@ -122,13 +121,13 @@
"@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/post-message-stream": "^6.0.0",
"@metamask/providers": "^10.0.0",
"@metamask/rpc-methods": "^0.22.2",
"@metamask/rpc-methods": "^0.23.0",
"@metamask/slip44": "^2.1.0",
"@metamask/smart-transactions-controller": "^3.0.0",
"@metamask/snap-controllers": "^0.22.2",
"@metamask/snap-utils": "^0.22.2",
"@metamask/snap-controllers": "^0.23.0",
"@metamask/snap-utils": "^0.23.0",
"@ngraveio/bc-ur": "^1.1.6",
"@popperjs/core": "^2.4.0",
"@reduxjs/toolkit": "^1.6.2",
@ -187,7 +186,7 @@
"localforage": "^1.9.0",
"lodash": "^4.17.21",
"loglevel": "^1.4.1",
"luxon": "^1.26.0",
"luxon": "^3.1.0",
"nanoid": "^2.1.6",
"nonce-tracker": "^1.0.0",
"obj-multiplex": "^1.0.0",
@ -325,11 +324,11 @@
"fast-glob": "^3.2.2",
"fs-extra": "^8.1.0",
"ganache": "^v7.0.4",
"geckodriver": "^1.21.0",
"geckodriver": "^3.2.0",
"gh-pages": "^3.2.3",
"globby": "^11.0.4",
"gulp": "^4.0.2",
"gulp-autoprefixer": "^5.0.0",
"gulp-autoprefixer": "^8.0.0",
"gulp-dart-sass": "^1.0.2",
"gulp-livereload": "4.0.0",
"gulp-rename": "^2.0.0",

@ -1,20 +1,22 @@
diff --git a/node_modules/luxon/build/cjs-browser/luxon.js b/node_modules/luxon/build/cjs-browser/luxon.js
index 206a47a..5556d1d 100644
index 9ab2b9f..14c2891 100644
--- a/node_modules/luxon/build/cjs-browser/luxon.js
+++ b/node_modules/luxon/build/cjs-browser/luxon.js
@@ -7243,13 +7243,13 @@ var DateTime = /*#__PURE__*/function () {
@@ -7373,7 +7373,7 @@ var DateTime = /*#__PURE__*/function () {
*/
;
- _proto.toLocaleString = function toLocaleString(opts) {
+ Reflect.defineProperty(_proto, 'toLocaleString', { value: function toLocaleString(opts) {
if (opts === void 0) {
opts = DATE_SHORT;
- _proto.toLocaleString = function toLocaleString(formatOpts, opts) {
+ Reflect.defineProperty(_proto, 'toLocaleString', { value: function toLocaleString(formatOpts, opts) {
if (formatOpts === void 0) {
formatOpts = DATE_SHORT;
}
@@ -7383,7 +7383,7 @@ var DateTime = /*#__PURE__*/function () {
}
return this.isValid ? Formatter.create(this.loc.clone(opts), opts).formatDateTime(this) : INVALID$2;
return this.isValid ? Formatter.create(this.loc.clone(opts), formatOpts).formatDateTime(this) : INVALID;
- }
+ }})
/**
* Returns an array of format "parts", meaning individual tokens along with metadata. This is allows callers to post-process individual sections of the formatted output.
* Defaults to the system's locale if no locale has been specified
* Defaults to the system's locale if no locale has been specified

@ -57,6 +57,13 @@ export const MESSAGE_TYPE = {
///: END:ONLY_INCLUDE_IN
} as const;
/**
* Custom messages to send and be received by the extension
*/
export const EXTENSION_MESSAGES = {
READY: 'METAMASK_EXTENSION_READY',
} as const;
/**
* The different kinds of subjects that MetaMask may interact with, including
* third parties and itself (e.g. when the background communicated with the UI).

@ -274,6 +274,7 @@ export const CURRENCY_SYMBOLS = {
USDC: 'USDC',
USDT: 'USDT',
WETH: 'WETH',
OPTIMISM: 'OP',
} as const;
/**
@ -531,6 +532,7 @@ export const NATIVE_CURRENCY_TOKEN_IMAGE_MAP = {
[CURRENCY_SYMBOLS.BNB]: BNB_TOKEN_IMAGE_URL,
[CURRENCY_SYMBOLS.MATIC]: MATIC_TOKEN_IMAGE_URL,
[CURRENCY_SYMBOLS.AVALANCHE]: AVAX_TOKEN_IMAGE_URL,
[CURRENCY_SYMBOLS.OPTIMISM]: OPTIMISM_TOKEN_IMAGE_URL,
} as const;
export const INFURA_BLOCKED_KEY = 'countryBlocked';

@ -24,6 +24,7 @@ export const EndowmentPermissions = Object.freeze({
'endowment:network-access': 'endowment:network-access',
'endowment:long-running': 'endowment:long-running',
'endowment:transaction-insight': 'endowment:transaction-insight',
'endowment:cronjob': 'endowment:cronjob',
} as const);
// Methods / permissions in external packages that we are temporarily excluding.

@ -1,4 +1,5 @@
import {
ETH_TOKEN_IMAGE_URL,
TEST_ETH_TOKEN_IMAGE_URL,
BNB_TOKEN_IMAGE_URL,
MATIC_TOKEN_IMAGE_URL,
@ -23,7 +24,7 @@ export const ETH_SWAPS_TOKEN_OBJECT = {
name: 'Ether',
address: DEFAULT_TOKEN_ADDRESS,
decimals: 18,
iconUrl: './images/black-eth-logo.svg',
iconUrl: ETH_TOKEN_IMAGE_URL,
};
export const BNB_SWAPS_TOKEN_OBJECT = {
@ -66,6 +67,10 @@ export const GOERLI_SWAPS_TOKEN_OBJECT = {
iconUrl: TEST_ETH_TOKEN_IMAGE_URL,
};
export const ARBITRUM_SWAPS_TOKEN_OBJECT = { ...ETH_SWAPS_TOKEN_OBJECT };
export const OPTIMISM_SWAPS_TOKEN_OBJECT = { ...ETH_SWAPS_TOKEN_OBJECT };
// A gas value for ERC20 approve calls that should be sufficient for all ERC20 approve implementations
export const DEFAULT_ERC20_APPROVE_GAS = '0x1d4c0';
@ -77,8 +82,9 @@ const BSC_CONTRACT_ADDRESS = '0x1a1ec25dc08e98e5e93f1104b5e5cdd298707d31';
// It's the same as we use for BSC.
const POLYGON_CONTRACT_ADDRESS = '0x1a1ec25dc08e98e5e93f1104b5e5cdd298707d31';
const AVALANCHE_CONTRACT_ADDRESS = '0x1a1ec25dc08e98e5e93f1104b5e5cdd298707d31';
const OPTIMISM_CONTRACT_ADDRESS = '0x9dDA6Ef3D919c9bC8885D5560999A3640431e8e6';
const ARBITRUM_CONTRACT_ADDRESS = '0x9dDA6Ef3D919c9bC8885D5560999A3640431e8e6';
export const WETH_CONTRACT_ADDRESS =
'0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
@ -91,6 +97,11 @@ export const WMATIC_CONTRACT_ADDRESS =
export const WAVAX_CONTRACT_ADDRESS =
'0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7';
export const WETH_OPTIMISM_CONTRACT_ADDRESS =
'0x4200000000000000000000000000000000000006';
export const WETH_ARBITRUM_CONTRACT_ADDRESS =
'0x82aF49447D8a07e3bd95BD0d56f35241523fBab1';
const SWAPS_TESTNET_CHAIN_ID = '0x539';
export const SWAPS_API_V2_BASE_URL = 'https://swap.metaswap.codefi.network';
@ -105,6 +116,8 @@ const MAINNET_DEFAULT_BLOCK_EXPLORER_URL = 'https://etherscan.io/';
const GOERLI_DEFAULT_BLOCK_EXPLORER_URL = 'https://goerli.etherscan.io/';
const POLYGON_DEFAULT_BLOCK_EXPLORER_URL = 'https://polygonscan.com/';
const AVALANCHE_DEFAULT_BLOCK_EXPLORER_URL = 'https://snowtrace.io/';
const OPTIMISM_DEFAULT_BLOCK_EXPLORER_URL = 'https://optimistic.etherscan.io/';
const ARBITRUM_DEFAULT_BLOCK_EXPLORER_URL = 'https://arbiscan.io/';
export const ALLOWED_PROD_SWAPS_CHAIN_IDS = [
CHAIN_IDS.MAINNET,
@ -112,6 +125,8 @@ export const ALLOWED_PROD_SWAPS_CHAIN_IDS = [
CHAIN_IDS.BSC,
CHAIN_IDS.POLYGON,
CHAIN_IDS.AVALANCHE,
CHAIN_IDS.OPTIMISM,
CHAIN_IDS.ARBITRUM,
];
export const ALLOWED_DEV_SWAPS_CHAIN_IDS = [
@ -131,6 +146,8 @@ export const SWAPS_CHAINID_CONTRACT_ADDRESS_MAP = {
[CHAIN_IDS.POLYGON]: POLYGON_CONTRACT_ADDRESS,
[CHAIN_IDS.GOERLI]: TESTNET_CONTRACT_ADDRESS,
[CHAIN_IDS.AVALANCHE]: AVALANCHE_CONTRACT_ADDRESS,
[CHAIN_IDS.OPTIMISM]: OPTIMISM_CONTRACT_ADDRESS,
[CHAIN_IDS.ARBITRUM]: ARBITRUM_CONTRACT_ADDRESS,
};
export const SWAPS_WRAPPED_TOKENS_ADDRESSES = {
@ -140,6 +157,8 @@ export const SWAPS_WRAPPED_TOKENS_ADDRESSES = {
[CHAIN_IDS.POLYGON]: WMATIC_CONTRACT_ADDRESS,
[CHAIN_IDS.GOERLI]: WETH_GOERLI_CONTRACT_ADDRESS,
[CHAIN_IDS.AVALANCHE]: WAVAX_CONTRACT_ADDRESS,
[CHAIN_IDS.OPTIMISM]: WETH_OPTIMISM_CONTRACT_ADDRESS,
[CHAIN_IDS.ARBITRUM]: WETH_ARBITRUM_CONTRACT_ADDRESS,
};
export const ALLOWED_CONTRACT_ADDRESSES = {
@ -167,6 +186,14 @@ export const ALLOWED_CONTRACT_ADDRESSES = {
SWAPS_CHAINID_CONTRACT_ADDRESS_MAP[CHAIN_IDS.AVALANCHE],
SWAPS_WRAPPED_TOKENS_ADDRESSES[CHAIN_IDS.AVALANCHE],
],
[CHAIN_IDS.OPTIMISM]: [
SWAPS_CHAINID_CONTRACT_ADDRESS_MAP[CHAIN_IDS.OPTIMISM],
SWAPS_WRAPPED_TOKENS_ADDRESSES[CHAIN_IDS.OPTIMISM],
],
[CHAIN_IDS.ARBITRUM]: [
SWAPS_CHAINID_CONTRACT_ADDRESS_MAP[CHAIN_IDS.ARBITRUM],
SWAPS_WRAPPED_TOKENS_ADDRESSES[CHAIN_IDS.ARBITRUM],
],
};
export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = {
@ -176,6 +203,8 @@ export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = {
[CHAIN_IDS.POLYGON]: MATIC_SWAPS_TOKEN_OBJECT,
[CHAIN_IDS.GOERLI]: GOERLI_SWAPS_TOKEN_OBJECT,
[CHAIN_IDS.AVALANCHE]: AVAX_SWAPS_TOKEN_OBJECT,
[CHAIN_IDS.OPTIMISM]: OPTIMISM_SWAPS_TOKEN_OBJECT,
[CHAIN_IDS.ARBITRUM]: ARBITRUM_SWAPS_TOKEN_OBJECT,
};
export const SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP = {
@ -184,6 +213,8 @@ export const SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP = {
[CHAIN_IDS.POLYGON]: POLYGON_DEFAULT_BLOCK_EXPLORER_URL,
[CHAIN_IDS.GOERLI]: GOERLI_DEFAULT_BLOCK_EXPLORER_URL,
[CHAIN_IDS.AVALANCHE]: AVALANCHE_DEFAULT_BLOCK_EXPLORER_URL,
[CHAIN_IDS.OPTIMISM]: OPTIMISM_DEFAULT_BLOCK_EXPLORER_URL,
[CHAIN_IDS.ARBITRUM]: ARBITRUM_DEFAULT_BLOCK_EXPLORER_URL,
};
export const ETHEREUM = 'ethereum';
@ -191,6 +222,8 @@ export const POLYGON = 'polygon';
export const BSC = 'bsc';
export const GOERLI = 'goerli';
export const AVALANCHE = 'avalanche';
export const OPTIMISM = 'optimism';
export const ARBITRUM = 'arbitrum';
export const SWAPS_CLIENT_ID = 'extension';

@ -12,11 +12,11 @@ export const LISTED_CONTRACT_ADDRESSES = Object.keys(contractMap).map(
/**
* @typedef {object} TokenDetails
* @property {string} address - The address of the selected 'TOKEN' or
* 'COLLECTIBLE' contract.
* 'NFT' contract.
* @property {string} [symbol] - The symbol of the token.
* @property {number} [decimals] - The number of decimals of the selected
* 'ERC20' asset.
* @property {number} [tokenId] - The id of the selected 'COLLECTIBLE' asset.
* @property {number} [tokenId] - The id of the selected 'NFT' asset.
* @property {TokenStandardStrings} [standard] - The standard of the selected
* asset.
* @property {boolean} [isERC721] - True when the asset is a ERC721 token.

@ -364,7 +364,7 @@ export const TRANSACTION_EVENTS = {
* @property {'NATIVE'} NATIVE - The native asset for the current network, such
* as ETH
* @property {'TOKEN'} TOKEN - An ERC20 token.
* @property {'COLLECTIBLE'} COLLECTIBLE - An ERC721 or ERC1155 token.
* @property {'NFT'} NFT - An ERC721 or ERC1155 token.
* @property {'UNKNOWN'} UNKNOWN - A transaction interacting with a contract
* that isn't a token method interaction will be marked as dealing with an
* unknown asset type.
@ -385,7 +385,7 @@ export const TRANSACTION_EVENTS = {
export const ASSET_TYPES = {
NATIVE: 'NATIVE',
TOKEN: 'TOKEN',
COLLECTIBLE: 'COLLECTIBLE',
NFT: 'NFT',
UNKNOWN: 'UNKNOWN',
};

@ -32,7 +32,7 @@ const getLocaleContext = (currentLocaleMessages, enLocaleMessages) => {
};
};
export async function getErrorHtml(supportLink, metamaskState) {
export async function getErrorHtml(errorKey, supportLink, metamaskState) {
let response, preferredLocale;
if (metamaskState?.currentLocale) {
preferredLocale = metamaskState.currentLocale;
@ -50,26 +50,40 @@ export async function getErrorHtml(supportLink, metamaskState) {
const { currentLocaleMessages, enLocaleMessages } = response;
const t = getLocaleContext(currentLocaleMessages, enLocaleMessages);
/**
* The pattern ${errorKey === 'troubleStarting' ? t('troubleStarting') : ''}
* is neccessary because we we need linter to see the string
* of the locale keys. If we use the variable directly, the linter will not
* see the string and will not be able to check if the locale key exists.
*/
return `
<div class="critical-error">
<div class="critical-error__alert">
<p class="critical-error__alert__message">
${t('troubleStarting')}
<div class="critical-error__icon">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.2325 9.78823L9.14559 1.96347C8.59641 0.910661 7.83651 0.333313 6.99998 0.333313C6.16345 0.333313 5.40354 0.910661 4.85437 1.96347L0.767492 9.78823C0.250247 10.7867 0.192775 11.7444 0.607848 12.4984C1.02292 13.2523 1.8403 13.6666 2.9131 13.6666H11.0869C12.1597 13.6666 12.977 13.2523 13.3921 12.4984C13.8072 11.7444 13.7497 10.7799 13.2325 9.78823ZM6.52105 5.08794C6.52105 4.80945 6.73816 4.57852 6.99998 4.57852C7.26179 4.57852 7.47891 4.80945 7.47891 5.08794V8.4841C7.47891 8.76259 7.26179 8.99353 6.99998 8.99353C6.73816 8.99353 6.52105 8.76259 6.52105 8.4841V5.08794ZM7.45337 11.0041C7.42144 11.0312 7.38951 11.0584 7.35758 11.0856C7.31927 11.1127 7.28095 11.1331 7.24264 11.1467C7.20432 11.1671 7.16601 11.1807 7.12131 11.1874C7.08299 11.1942 7.03829 11.201 6.99998 11.201C6.96166 11.201 6.91696 11.1942 6.87226 11.1874C6.83395 11.1807 6.79563 11.1671 6.75732 11.1467C6.71901 11.1331 6.68069 11.1127 6.64238 11.0856C6.61045 11.0584 6.57852 11.0312 6.54659 11.0041C6.43165 10.875 6.3614 10.6984 6.3614 10.5218C6.3614 10.3452 6.43165 10.1686 6.54659 10.0395C6.57852 10.0124 6.61045 9.98521 6.64238 9.95804C6.68069 9.93087 6.71901 9.91049 6.75732 9.8969C6.79563 9.87653 6.83395 9.86294 6.87226 9.85615C6.95528 9.83577 7.04468 9.83577 7.12131 9.85615C7.16601 9.86294 7.20432 9.87653 7.24264 9.8969C7.28095 9.91049 7.31927 9.93087 7.35758 9.95804C7.38951 9.98521 7.42144 10.0124 7.45337 10.0395C7.56831 10.1686 7.63855 10.3452 7.63855 10.5218C7.63855 10.6984 7.56831 10.875 7.45337 11.0041Z" fill="#F66A0A"/>
</svg>
</div>
<div class="critical-error__dscription">
<div class="critical-error__alert">
<p class="critical-error__alert__message">
${errorKey === 'troubleStarting' ? t('troubleStarting') : ''}
${errorKey === 'somethingIsWrong' ? t('somethingIsWrong') : ''}
</p>
<span id='critical-error-button' class="critical-error__alert__action-link">
${t('restartMetamask')}
</span>
</div>
<p class="critical-error__paragraph">
${t('stillGettingMessage')}
<a
href=${supportLink}
class="critical-error__paragraph__link"
target="_blank"
rel="noopener noreferrer">
${t('sendBugReport')}
</a>
</p>
<button id='critical-error-button' class="critical-error__alert__button">
${t('restartMetamask')}
</button>
</div>
<p class="critical-error__paragraph">
${t('stillGettingMessage')}
<a
href=${supportLink}
class="critical-error__paragraph__link"
target="_blank"
rel="noopener noreferrer">
${t('sendBugReport')}
</a>
</p>
</div>
`;
}

@ -33,7 +33,11 @@ describe('Error utils Tests', function () {
};
fetchLocale.mockReturnValue(mockStore.localeMessages.current);
const errorHtml = await getErrorHtml(SUPPORT_LINK, mockStore.metamask);
const errorHtml = await getErrorHtml(
'troubleStarting',
SUPPORT_LINK,
mockStore.metamask,
);
const currentLocale = mockStore.localeMessages.current;
const troubleStartingMessage = currentLocale.troubleStarting.message;
const restartMetamaskMessage = currentLocale.restartMetamask.message;

@ -146,7 +146,6 @@ export const getBaseApi = function (type, chainId = CHAIN_IDS.MAINNET) {
// eslint-disable-next-line no-param-reassign
chainId = TEST_CHAIN_IDS.includes(chainId) ? CHAIN_IDS.MAINNET : chainId;
const baseUrl = getBaseUrlForNewSwapsApi(type, chainId);
const chainIdDecimal = chainId && parseInt(chainId, 16);
if (!baseUrl) {
throw new Error(`Swaps API calls are disabled for chainId: ${chainId}`);
}
@ -164,8 +163,7 @@ export const getBaseApi = function (type, chainId = CHAIN_IDS.MAINNET) {
case 'gasPrices':
return `${baseUrl}/gasPrices`;
case 'network':
// Only use v2 for this endpoint.
return `${SWAPS_API_V2_BASE_URL}/networks/${chainIdDecimal}`;
return baseUrl;
default:
throw new Error('getBaseApi requires an api call type');
}

@ -0,0 +1,55 @@
/**
* Utility Functions to support browser.runtime JavaScript API
*/
import browser from 'webextension-polyfill';
import log from 'loglevel';
/**
* Returns an Error if extension.runtime.lastError is present
* this is a workaround for the non-standard error object that's used
*
* According to the docs, we are expected to check lastError in runtime API callbacks:
* "
* If you call an asynchronous function that may set lastError, you are expected to
* check for the error when you handle the result of the function. If lastError has been
* set and you don't check it within the callback function, then an error will be raised.
* "
*
* @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/lastError}
* @returns {Error|undefined}
*/
export function checkForLastError() {
const { lastError } = browser.runtime;
if (!lastError) {
return undefined;
}
// if it quacks like an Error, its an Error
if (lastError.stack && lastError.message) {
return lastError;
}
// repair incomplete error object (eg chromium v77)
return new Error(lastError.message);
}
/** @returns {Error|undefined} */
export function checkForLastErrorAndLog() {
const error = checkForLastError();
if (error) {
log.error(error);
}
return error;
}
/** @returns {Error|undefined} */
export function checkForLastErrorAndWarn() {
const error = checkForLastError();
if (error) {
console.warn(error);
}
return error;
}

@ -0,0 +1,54 @@
import sinon from 'sinon';
import browser from 'webextension-polyfill';
import log from 'loglevel';
import * as BrowserRuntimeUtil from './browser-runtime.utils';
const mockLastError = { message: 'error', stack: [] };
describe('Browser Runtime Utils', () => {
beforeAll(() => {
sinon.replace(browser, 'runtime', {
lastError: undefined,
});
});
describe('checkForLastError', () => {
it('should return undefined if no lastError found', () => {
expect(BrowserRuntimeUtil.checkForLastError()).toBeUndefined();
});
it('should return the lastError (Error object) if lastError is found', () => {
sinon.stub(browser.runtime, 'lastError').value(mockLastError);
expect(BrowserRuntimeUtil.checkForLastError()).toStrictEqual(
mockLastError,
);
});
it('should return an Error object if the lastError is found with no stack', () => {
sinon
.stub(browser.runtime, 'lastError')
.value({ message: mockLastError.message });
const result = BrowserRuntimeUtil.checkForLastError();
expect(result).toStrictEqual(expect.any(Error));
expect(result).toHaveProperty('stack');
expect(result.message).toBe(mockLastError.message);
});
});
describe('checkForLastErrorAndLog', () => {
it('should log and return error if error was found', () => {
sinon.stub(browser.runtime, 'lastError').value({ ...mockLastError });
sinon.stub(log, 'error');
const result = BrowserRuntimeUtil.checkForLastErrorAndLog();
expect(log.error.calledWith(result)).toBeTruthy();
expect(result).toStrictEqual(mockLastError);
log.error.restore();
});
});
});

@ -259,7 +259,7 @@ export async function determineTransactionAssetType(
assetType:
details.standard === TOKEN_STANDARDS.ERC20
? ASSET_TYPES.TOKEN
: ASSET_TYPES.COLLECTIBLE,
: ASSET_TYPES.NFT,
tokenStandard: details.standard,
};
}

@ -78,6 +78,10 @@ export const UI_NOTIFICATIONS = {
id: 15,
date: '2022-09-15',
},
16: {
id: 16,
date: null,
},
};
export const getTranslatedUINotifications = (t, locale) => {
@ -224,5 +228,16 @@ export const getTranslatedUINotifications = (t, locale) => {
)
: '',
},
16: {
...UI_NOTIFICATIONS[16],
title: t('notifications16Title'),
description: t('notifications16Description'),
actionText: t('notifications16ActionText'),
date: UI_NOTIFICATIONS[16].date
? new Intl.DateTimeFormat(formattedLocale).format(
new Date(UI_NOTIFICATIONS[16].date),
)
: '',
},
};
};

@ -12,13 +12,17 @@
"previousModalState": {
"name": null
}
}
},
"warning": null
},
"history": {
"mostRecentOverviewPage": "/"
"mostRecentOverviewPage": "/mostRecentOverviewPage"
},
"metamask": {
"usePhishDetect": true,
"participateInMetaMetrics": false,
"gasEstimateType": "fee-market",
"showBetaHeader": false,
"gasFeeEstimates": {
"low": {
"minWaitTimeEstimate": 180000,
@ -46,6 +50,7 @@
"priorityFeeTrend": "down",
"networkCongestion": 0.90625
},
"snaps": [{}],
"preferences": {
"hideZeroBalanceTokens": false,
"showFiatInTestnets": false,
@ -241,20 +246,6 @@
"unapprovedEncryptionPublicKeyMsgCount": 0,
"unapprovedTypedMessages": {},
"unapprovedTypedMessagesCount": 0,
"send": {
"gasLimit": "0x5208",
"gasPrice": "0xee6b2800",
"gasTotal": "0x4c65c6294000",
"tokenBalance": null,
"from": "0xc42edfcc21ed14dda456aa0756c153f7985d8813",
"to": "",
"amount": "1bc16d674ec80000",
"memo": "",
"errors": {},
"maxModeOn": false,
"editingTransactionId": null,
"toNickname": ""
},
"useTokenDetection": true,
"advancedGasFee": {
"maxBaseFee": "75",
@ -1284,5 +1275,24 @@
"origin": "tmashuang.github.io"
}
]
},
"send": {
"amountMode": "INPUT",
"currentTransactionUUID": null,
"draftTransactions": {},
"eip1559support": false,
"gasEstimateIsLoading": true,
"gasEstimatePollToken": null,
"gasIsSetInModal": false,
"gasPriceEstimate": "0x0",
"gasLimitMinimum": "0x5208",
"gasTotalForLayer1": "0x0",
"recipientMode": "CONTACT_LIST",
"recipientInput": "",
"selectedAccount": {
"address": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"balance": "0x0"
},
"stage": "INACTIVE"
}
}

@ -74,12 +74,12 @@ function defaultFixture() {
src: 'images/token-detection.svg',
width: '100%',
},
isShown: true,
isShown: false,
},
11: {
date: '2022-09-15',
id: 11,
isShown: true,
isShown: false,
},
12: {
date: '2022-05-18',
@ -98,11 +98,16 @@ function defaultFixture() {
14: {
date: '2022-09-15',
id: 14,
isShown: true,
isShown: false,
},
15: {
date: '2022-09-15',
id: 15,
isShown: false,
},
16: {
date: null,
id: 16,
isShown: true,
},
},
@ -225,7 +230,7 @@ function defaultFixture() {
selectedAddress: '0x5cfe73b6021e818b776b421b1c4db2474086a7e1',
theme: 'light',
useBlockie: false,
useCollectibleDetection: false,
useNftDetection: false,
useNonceField: false,
usePhishDetect: true,
useTokenDetection: false,
@ -338,7 +343,7 @@ function onboardingFixture() {
},
theme: 'light',
useBlockie: false,
useCollectibleDetection: false,
useNftDetection: false,
useNonceField: false,
usePhishDetect: true,
useTokenDetection: false,
@ -407,9 +412,9 @@ class FixtureBuilder {
withCollectiblesController(data) {
merge(
this.fixture.data.CollectiblesController
? this.fixture.data.CollectiblesController
: (this.fixture.data.CollectiblesController = {}),
this.fixture.data.NftController
? this.fixture.data.NftController
: (this.fixture.data.NftController = {}),
data,
);
return this;

@ -51,6 +51,8 @@ async function withFixtures(options, testSuite) {
const phishingPageServer = new PhishingWarningPageServer();
let webDriver;
let driver;
const errors = [];
let failed = false;
try {
await ganacheServer.start(ganacheOptions);
@ -110,8 +112,12 @@ async function withFixtures(options, testSuite) {
) {
await ensureXServerIsRunning();
}
const { driver } = await buildWebDriver(driverOptions);
webDriver = driver;
driver = (await buildWebDriver(driverOptions)).driver;
webDriver = driver.driver;
if (process.env.SELENIUM_BROWSER === 'chrome') {
await driver.checkBrowserForExceptions();
}
await testSuite({
driver,
@ -120,7 +126,7 @@ async function withFixtures(options, testSuite) {
});
if (process.env.SELENIUM_BROWSER === 'chrome') {
const errors = await driver.checkBrowserForConsoleErrors(driver);
errors.concat(await driver.checkBrowserForConsoleErrors(driver));
if (errors.length) {
const errorReports = errors.map((err) => err.message);
const errorMessage = `Errors found in browser console:\n${errorReports.join(
@ -137,10 +143,20 @@ async function withFixtures(options, testSuite) {
failed = true;
if (webDriver) {
try {
await webDriver.verboseReportOnFailure(title);
await driver.verboseReportOnFailure(title);
} catch (verboseReportError) {
console.error(verboseReportError);
}
if (
errors.length === 0 &&
driver.exceptions.length > 0 &&
failOnConsoleError
) {
const errorMessage = `Errors found in browser console:\n${driver.exceptions.join(
'\n',
)}`;
throw Error(errorMessage);
}
}
throw error;
} finally {
@ -151,7 +167,7 @@ async function withFixtures(options, testSuite) {
await secondaryGanacheServer.quit();
}
if (webDriver) {
await webDriver.quit();
await driver.quit();
}
if (dapp) {
for (let i = 0; i < numberOfDapps; i++) {

@ -458,10 +458,10 @@ describe('MetaMask', function () {
await driver.delay(veryLargeDelayMs);
await driver.clickElement({ text: 'Edit', tag: 'button' });
await driver.delay(veryLargeDelayMs);
await driver.clickElement(
{ text: 'Edit suggested gas fee', tag: 'button' },
10000,
);
await driver.clickElement({
text: 'Edit suggested gas fee',
tag: 'button',
});
await driver.delay(veryLargeDelayMs);
const inputs = await driver.findElements('input[type="number"]');
const gasLimitInput = inputs[0];
@ -576,10 +576,10 @@ describe('MetaMask', function () {
it('customizes gas', async function () {
await driver.clickElement('.confirm-approve-content__small-blue-text');
await driver.delay(regularDelayMs);
await driver.clickElement(
{ text: 'Edit suggested gas fee', tag: 'button' },
10000,
);
await driver.clickElement({
text: 'Edit suggested gas fee',
tag: 'button',
});
await driver.delay(regularDelayMs);
const [gasLimitInput, gasPriceInput] = await driver.findElements(

@ -42,7 +42,7 @@
},
"theme": "light",
"useBlockie": false,
"useCollectibleDetection": false,
"useNftDetection": false,
"useNonceField": false,
"usePhishDetect": true,
"useTokenDetection": false

@ -13,6 +13,14 @@ const getTestPathsForTestDir = async (testDir) => {
return testPaths;
};
function chunk(array, chunkSize) {
const result = [];
for (let i = 0; i < array.length; i += chunkSize) {
result.push(array.slice(i, i + chunkSize));
}
return result;
}
async function main() {
const { argv } = yargs(hideBin(process.argv))
.usage(
@ -66,7 +74,14 @@ async function main() {
args.push('--retries', retries);
}
for (const testPath of testPaths) {
// For running E2Es in parallel in CI
const currentChunkIndex = process.env.CIRCLE_NODE_INDEX ?? 0;
const totalChunks = process.env.CIRCLE_NODE_TOTAL ?? 1;
const chunkSize = Math.ceil(testPaths.length / totalChunks);
const chunks = chunk(testPaths, chunkSize);
const currentChunk = chunks[currentChunkIndex];
for (const testPath of currentChunk) {
await runInShell('node', [...args, testPath]);
}
}

@ -1,3 +1,3 @@
module.exports = {
TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/test-snaps/3.1.0',
TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/test-snaps/4.0.2/',
};

@ -15,10 +15,9 @@ describe('Test Snap bip-32', function () {
};
await withFixtures(
{
fixtures: new FixtureBuilder()
.withPermissionControllerConnectedToSnapDapp()
.build(),
fixtures: new FixtureBuilder().build(),
ganacheOptions,
failOnConsoleError: false,
title: this.test.title,
},
async ({ driver }) => {
@ -32,20 +31,38 @@ describe('Test Snap bip-32', function () {
await driver.driver.get(TEST_SNAPS_WEBSITE_URL);
await driver.delay(1000);
// find and scroll to the correct card and click first
const snapButton = await driver.findElement('#sendUpdateHello');
await driver.scrollToElement(snapButton);
await driver.delay(500);
await driver.fill('#snapId6', 'npm:@metamask/test-snap-bip32');
// find and scroll to the bip32 test and connect
const snapButton1 = await driver.findElement('#connectBip32');
await driver.scrollToElement(snapButton1);
await driver.delay(1000);
await driver.clickElement('#connectBip32');
// approve install of snap
// switch to metamask extension and click connect
await driver.waitUntilXWindowHandles(2, 5000, 10000);
let windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
);
await driver.clickElement(
{
text: 'Connect',
tag: 'button',
},
10000,
);
await driver.delay(2000);
// switch to metamask extension
await driver.waitUntilXWindowHandles(2, 5000, 10000);
windowHandles = await driver.getAllWindowHandles();
// approve install of snap
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
);
await driver.clickElement({
text: 'Approve & install',
tag: 'button',
@ -65,10 +82,46 @@ describe('Test Snap bip-32', function () {
windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle('Test Snaps', windowHandles);
// scroll to and click get public key
await driver.delay(1000);
const snapButton2 = await driver.findElement('#bip32GetPublic');
await driver.scrollToElement(snapButton2);
await driver.delay(1000);
await driver.clickElement('#bip32GetPublic');
// check for proper public key response
await driver.delay(1000);
const retrievePublicKeyResult1 = await driver.findElement(
'#bip32PublicKeyResult',
);
assert.equal(
await retrievePublicKeyResult1.getText(),
'"0x043e98d696ae15caef75fa8dd204a7c5c08d1272b2218ba3c20feeb4c691eec366606ece56791c361a2320e7fad8bcbb130f66d51c591fc39767ab2856e93f8dfb"',
);
// scroll to and click get compressed public key
await driver.delay(1000);
const snapButton3 = await driver.findElement(
'#bip32GetCompressedPublic',
);
await driver.scrollToElement(snapButton3);
await driver.delay(1000);
await driver.clickElement('#bip32GetCompressedPublic');
// check for proper public key response
await driver.delay(1000);
const retrievePublicKeyResult2 = await driver.findElement(
'#bip32PublicKeyResult',
);
assert.equal(
await retrievePublicKeyResult2.getText(),
'"0x033e98d696ae15caef75fa8dd204a7c5c08d1272b2218ba3c20feeb4c691eec366"',
);
// wait then run SECP256K1 test
await driver.delay(1000);
await driver.fill('#bip32SignMessage', 'foo bar');
await driver.clickElement('#sendBip32Secp256k1');
await driver.fill('#bip32Message-secp256k1', 'foo bar');
await driver.clickElement('#sendBip32-secp256k1');
// hit 'approve' on the custom confirm
await driver.waitUntilXWindowHandles(2, 5000, 10000);
@ -89,16 +142,23 @@ describe('Test Snap bip-32', function () {
// check result
await driver.delay(1000);
const secp256k1Result = await driver.findElement(
'#bip32Secp256k1Result',
'#bip32MessageResult-secp256k1',
);
assert.equal(
await secp256k1Result.getText(),
'Signature: "0xd30561eb9e3195e47d49198fb0bc66eda867a7dff4c5e8b60c2ec13851aa7d8cc3d485da177de63dad331f315d440cbb693a629efe228389c4693ea90465b101"',
'"0x3045022100b3ade2992ea3e5eb58c7550e9bddad356e9554233c8b099ebc3cb418e9301ae2022064746e15ae024808f0ba5d860e44dc4c97e65c8cba6f5ef9ea2e8c819930d2dc"',
);
// scroll further into messages section
await driver.delay(1000);
const snapButton4 = await driver.findElement('#bip32Message-ed25519');
await driver.scrollToElement(snapButton4);
await driver.delay(1000);
// wait then run ed25519 test
await driver.delay(1000);
await driver.clickElement('#sendBip32Ed25519');
await driver.fill('#bip32Message-ed25519', 'foo bar');
await driver.clickElement('#sendBip32-ed25519');
// hit 'approve' on the custom confirm
await driver.waitUntilXWindowHandles(2, 5000, 10000);
@ -118,38 +178,12 @@ describe('Test Snap bip-32', function () {
// check result
await driver.delay(1000);
const ed25519Result = await driver.findElement('#bip32Ed25519Result');
assert.equal(
await ed25519Result.getText(),
'Signature: "0xf3215b4d6c59aac7e01b4ceef530d1e2abf4857926b85a81aaae3894505699243768a887b7da4a8c2e0f25196196ba290b6531050db8dc15c252bdd508532a0a"',
);
const publicKeyButton = await driver.findElement('#sendBip32PublicKey');
await driver.scrollToElement(publicKeyButton);
// wait then run public key test
await driver.delay(1000);
await driver.clickElement('#sendBip32PublicKey');
// check result
await driver.delay(1000);
const publicKeyResult = await driver.findElement(
'#bip32PublicKeyResult',
);
assert.equal(
await publicKeyResult.getText(),
'Public key: "043e98d696ae15caef75fa8dd204a7c5c08d1272b2218ba3c20feeb4c691eec366606ece56791c361a2320e7fad8bcbb130f66d51c591fc39767ab2856e93f8dfb"',
);
// wait then run compressed public key test
await driver.delay(1000);
await driver.clickElement('#sendBip32CompressedPublicKey');
// check result
await driver.delay(1000);
const compressedPublicKeyResult = await driver.findElement(
'#bip32CompressedPublicKeyResult',
const ed25519Result = await driver.findElement(
'#bip32MessageResult-ed25519',
);
assert.equal(
await compressedPublicKeyResult.getText(),
'Public key: "033e98d696ae15caef75fa8dd204a7c5c08d1272b2218ba3c20feeb4c691eec366"',
await ed25519Result.getText(),
'"0xf3215b4d6c59aac7e01b4ceef530d1e2abf4857926b85a81aaae3894505699243768a887b7da4a8c2e0f25196196ba290b6531050db8dc15c252bdd508532a0a"',
);
},
);

@ -16,10 +16,9 @@ describe('Test Snap bip-44', function () {
};
await withFixtures(
{
fixtures: new FixtureBuilder()
.withPermissionControllerConnectedToSnapDapp()
.build(),
fixtures: new FixtureBuilder().build(),
ganacheOptions,
failOnConsoleError: false,
title: this.test.title,
},
async ({ driver }) => {
@ -31,19 +30,33 @@ describe('Test Snap bip-44', function () {
// navigate to test snaps page and connect
await driver.driver.get(TEST_SNAPS_WEBSITE_URL);
const snapButton1 = await driver.findElement('#connectBip44Snap');
await driver.scrollToElement(snapButton1);
await driver.delay(1000);
await driver.fill('#snapId3', 'npm:@metamask/test-snap-bip44');
await driver.clickElement('#connectBip44Snap');
const snapButton = await driver.findElement('#snapId3');
await driver.scrollToElement(snapButton);
await driver.delay(500);
// switch to metamask extension and click connect
await driver.waitUntilXWindowHandles(2, 5000, 10000);
let windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
);
await driver.clickElement(
{
text: 'Connect',
tag: 'button',
},
10000,
);
// connect the snap
await driver.clickElement('#connectBip44');
await driver.delay(2000);
// approve install of snap
// switch to metamask extension
await driver.waitUntilXWindowHandles(2, 5000, 10000);
let windowHandles = await driver.getAllWindowHandles();
windowHandles = await driver.getAllWindowHandles();
// approve install of snap
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
@ -66,14 +79,47 @@ describe('Test Snap bip-44', function () {
windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle('Test Snaps', windowHandles);
await driver.delay(1000);
await driver.clickElement('#sendBip44');
await driver.clickElement('#sendBip44Test');
// check the results of the public key test
await driver.delay(2000);
await driver.delay(1000);
const bip44Result = await driver.findElement('#bip44Result');
assert.equal(
await bip44Result.getText(),
'Public key: "0x86debb44fb3a984d93f326131d4c1db0bc39644f1a67b673b3ab45941a1cea6a385981755185ac4594b6521e4d1e08d1"',
'"0x86debb44fb3a984d93f326131d4c1db0bc39644f1a67b673b3ab45941a1cea6a385981755185ac4594b6521e4d1e08d1"',
);
// enter a message to sign
await driver.fill('#bip44Message', '1234');
await driver.delay(1000);
const snapButton3 = await driver.findElement('#signBip44Message');
await driver.scrollToElement(snapButton3);
await driver.delay(1000);
await driver.clickElement('#signBip44Message');
// Switch to approve signature message window and approve
await driver.waitUntilXWindowHandles(2, 5000, 10000);
windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
);
await driver.clickElement({
text: 'Approve',
tag: 'button',
});
// switch back to test-snaps page
await driver.waitUntilXWindowHandles(1, 5000, 10000);
windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle('Test Snaps', windowHandles);
await driver.delay(1000);
// check the results of the message signature
const bip44SignResult = await driver.findElement('#bip44SignResult');
assert.equal(
await bip44SignResult.getText(),
'"0xa41ab87ca50606eefd47525ad90294bbe44c883f6bc53655f1b8a55aa8e1e35df216f31be62e52c7a1faa519420e20810162e07dedb0fde2a4d997ff7180a78232ecd8ce2d6f4ba42ccacad33c5e9e54a8c4d41506bdffb2bb4c368581d8b086"',
);
},
);

@ -16,10 +16,9 @@ describe('Test Snap Confirm', function () {
};
await withFixtures(
{
fixtures: new FixtureBuilder()
.withPermissionControllerConnectedToSnapDapp()
.build(),
fixtures: new FixtureBuilder().build(),
ganacheOptions,
failOnConsoleError: false,
title: this.test.title,
},
async ({ driver }) => {
@ -31,29 +30,48 @@ describe('Test Snap Confirm', function () {
// navigate to test snaps page and connect
await driver.driver.get(TEST_SNAPS_WEBSITE_URL);
await driver.fill('#snapId1', 'npm:@metamask/test-snap-confirm');
await driver.clickElement('#connectHello');
const snapButton1 = await driver.findElement('#connectConfirmSnap');
await driver.scrollToElement(snapButton1);
await driver.delay(1000);
await driver.clickElement('#connectConfirmSnap');
// approve install of snap
// switch to metamask extension and click connect
await driver.waitUntilXWindowHandles(2, 5000, 10000);
let windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
);
await driver.clickElement(
{
text: 'Connect',
tag: 'button',
},
10000,
);
await driver.delay(2000);
// approve install of snap
await driver.waitUntilXWindowHandles(2, 5000, 10000);
windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
);
await driver.clickElement({
text: 'Approve & install',
tag: 'button',
});
// click send inputs on test snap page
// switch back to test snaps page
await driver.waitUntilXWindowHandles(1, 5000, 10000);
windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle('Test Snaps', windowHandles);
const snapButton = await driver.findElement('#sendConfirmButton');
await driver.scrollToElement(snapButton);
// click send inputs on test snap page
const snapButton2 = await driver.findElement('#sendConfirmButton');
await driver.scrollToElement(snapButton2);
await driver.delay(1000);
await driver.clickElement('#sendConfirmButton');

@ -1,6 +1,5 @@
const { strict: assert } = require('assert');
const { withFixtures } = require('../helpers');
const { PAGES } = require('../webdriver/driver');
const FixtureBuilder = require('../fixture-builder');
const { TEST_SNAPS_WEBSITE_URL } = require('./enums');
@ -17,10 +16,9 @@ describe('Test Snap Error', function () {
};
await withFixtures(
{
fixtures: new FixtureBuilder()
.withPermissionControllerConnectedToSnapDapp()
.build(),
fixtures: new FixtureBuilder().build(),
ganacheOptions,
failOnConsoleError: false,
title: this.test.title,
},
async ({ driver }) => {
@ -31,17 +29,31 @@ describe('Test Snap Error', function () {
await driver.press('#password', driver.Key.ENTER);
// navigate to test snaps page and connect
await driver.driver.get(TEST_SNAPS_WEBSITE_URL);
await driver.fill('#snapId2', 'npm:@metamask/test-snap-error');
const snapButton = await driver.findElement('#connectError');
await driver.openNewPage(TEST_SNAPS_WEBSITE_URL);
const snapButton = await driver.findElement('#connectErrorSnap');
await driver.scrollToElement(snapButton);
await driver.delay(500);
await driver.delay(1000);
await driver.clickElement('#connectErrorSnap');
// switch to metamask extension and click connect
await driver.waitUntilXWindowHandles(3, 5000, 10000);
let windowHandles = await driver.getAllWindowHandles();
const extensionPage = windowHandles[0];
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
);
await driver.clickElement(
{
text: 'Connect',
tag: 'button',
},
10000,
);
await driver.clickElement('#connectError');
await driver.delay(2000);
// approve install of snap
let windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
@ -52,14 +64,19 @@ describe('Test Snap Error', function () {
});
// click send inputs on test snap page
await driver.waitUntilXWindowHandles(1, 5000, 10000);
await driver.waitUntilXWindowHandles(2, 5000, 10000);
windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle('Test Snaps', windowHandles);
await driver.delay(1000);
// find and click on send error
await driver.clickElement('#sendError');
await driver.navigate(PAGES.HOME);
// switch back to the extension page
await driver.switchToWindow(extensionPage);
await driver.delay(1000);
// look for the actual error and check if it is correct
const error = await driver.findElement(
'.home-notification__content-container',
);
@ -70,6 +87,12 @@ describe('Test Snap Error', function () {
),
true,
);
// try to click on the dismiss button and pass test if it works
await driver.clickElement({
text: 'Dismiss',
tag: 'button',
});
},
);
});

@ -3,8 +3,8 @@ const { withFixtures } = require('../helpers');
const FixtureBuilder = require('../fixture-builder');
const { TEST_SNAPS_WEBSITE_URL } = require('./enums');
describe('Test Snap Confirm', function () {
it('can pop up a snap confirm and get its result', async function () {
describe('Test Snap Installed', function () {
it('can tell if a snap is installed', async function () {
const ganacheOptions = {
accounts: [
{
@ -16,10 +16,9 @@ describe('Test Snap Confirm', function () {
};
await withFixtures(
{
fixtures: new FixtureBuilder()
.withPermissionControllerConnectedToSnapDapp()
.build(),
fixtures: new FixtureBuilder().build(),
ganacheOptions,
failOnConsoleError: false,
title: this.test.title,
},
async ({ driver }) => {
@ -30,35 +29,69 @@ describe('Test Snap Confirm', function () {
await driver.press('#password', driver.Key.ENTER);
// navigate to test snaps page and connect
await driver.driver.get(TEST_SNAPS_WEBSITE_URL);
await driver.fill('#snapId1', 'npm:@metamask/test-snap-confirm');
await driver.clickElement('#connectHello');
await driver.openNewPage(TEST_SNAPS_WEBSITE_URL);
await driver.delay(1000);
const confirmButton = await driver.findElement('#connectConfirmSnap');
await driver.scrollToElement(confirmButton);
await driver.clickElement('#connectConfirmSnap');
// approve install of snap
await driver.waitUntilXWindowHandles(2, 5000, 10000);
// switch to metamask extension and click connect
await driver.waitUntilXWindowHandles(3, 5000, 10000);
let windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
);
await driver.clickElement(
{
text: 'Connect',
tag: 'button',
},
10000,
);
await driver.delay(2000);
// approve install of snap
await driver.waitUntilXWindowHandles(3, 5000, 10000);
windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
);
await driver.clickElement({
text: 'Approve & install',
tag: 'button',
});
// click send inputs on test snap page
await driver.waitUntilXWindowHandles(1, 5000, 10000);
await driver.waitUntilXWindowHandles(2, 5000, 10000);
windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle('Test Snaps', windowHandles);
const errorButton = await driver.findElement('#connectError');
const errorButton = await driver.findElement('#connectErrorSnap');
await driver.scrollToElement(errorButton);
await driver.delay(1000);
await driver.fill('#snapId2', 'npm:@metamask/test-snap-error');
await driver.clickElement('#connectError');
await driver.clickElement('#connectErrorSnap');
// switch to metamask extension and click connect
await driver.waitUntilXWindowHandles(3, 5000, 10000);
windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
);
await driver.clickElement(
{
text: 'Connect',
tag: 'button',
},
10000,
);
await driver.delay(2000);
// approve install of snap
await driver.waitUntilXWindowHandles(2, 5000, 10000);
windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle(
'MetaMask Notification',
@ -69,19 +102,13 @@ describe('Test Snap Confirm', function () {
tag: 'button',
});
await driver.waitUntilXWindowHandles(1, 5000, 10000);
await driver.waitUntilXWindowHandles(2, 5000, 10000);
windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle('Test Snaps', windowHandles);
const getInstalledSnapsButton = await driver.findElement(
'#getInstalledSnapsButton',
);
await driver.scrollToElement(getInstalledSnapsButton);
await driver.delay(1000);
await driver.clickElement('#getInstalledSnapsButton');
const result = await driver.findElement('#installedSnapsResult');
await driver.scrollToElement(result);
await driver.delay(1000);
const result = await driver.findElement('#getInstalledSnapsResult');
assert.equal(
await result.getText(),
'npm:@metamask/test-snap-confirm, npm:@metamask/test-snap-error',

@ -17,10 +17,9 @@ describe('Test Snap manageState', function () {
await withFixtures(
{
fixtures: new FixtureBuilder()
.withPermissionControllerConnectedToSnapDapp()
.build(),
fixtures: new FixtureBuilder().build(),
ganacheOptions,
failOnConsoleError: false,
title: this.test.title,
},
async ({ driver }) => {
@ -31,32 +30,45 @@ describe('Test Snap manageState', function () {
await driver.press('#password', driver.Key.ENTER);
// navigate to test snaps page, then fill in the snapId
await driver.driver.get(TEST_SNAPS_WEBSITE_URL);
await driver.openNewPage(TEST_SNAPS_WEBSITE_URL);
await driver.delay(1000);
await driver.fill('#snapId4', 'npm:@metamask/test-snap-managestate');
// find and scroll to the rest of the card
const snapButton = await driver.findElement('#snapId4');
await driver.scrollToElement(snapButton);
await driver.delay(500);
// connect the snap
// find and scroll to the connect button and click it
const snapButton1 = await driver.findElement('#connectManageState');
await driver.scrollToElement(snapButton1);
await driver.delay(1000);
await driver.clickElement('#connectManageState');
// approve install of snap
await driver.waitUntilXWindowHandles(2, 5000, 10000);
// switch to metamask extension and click connect
await driver.waitUntilXWindowHandles(3, 5000, 10000);
let windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
);
await driver.clickElement(
{
text: 'Connect',
tag: 'button',
},
10000,
);
await driver.delay(2000);
// approve install of snap
await driver.waitUntilXWindowHandles(3, 5000, 10000);
windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
);
await driver.clickElement({
text: 'Approve & install',
tag: 'button',
});
// fill and click send inputs on test snap page
await driver.waitUntilXWindowHandles(1, 5000, 10000);
await driver.waitUntilXWindowHandles(2, 5000, 10000);
windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle('Test Snaps', windowHandles);
await driver.fill('#dataManageState', '23');
@ -64,46 +76,40 @@ describe('Test Snap manageState', function () {
await driver.clickElement('#sendManageState');
// check the results of the public key test
await driver.delay(500);
await driver.delay(1000);
const manageStateResult = await driver.findElement(
'#sendManageStateResult',
);
assert.equal(await manageStateResult.getText(), 'true');
// click get results
await driver.clickElement('#retrieveManageState');
// check the results
await driver.delay(500);
await driver.delay(1000);
const retrieveManageStateResult = await driver.findElement(
'#retrieveManageStateResult',
);
assert.equal(
await retrieveManageStateResult.getText(),
'{"testState":["23"]}',
'{ "testState": [ "23" ] }',
);
// click clear results
await driver.clickElement('#clearManageState');
// check if true
await driver.delay(500);
await driver.delay(1000);
const clearManageStateResult = await driver.findElement(
'#clearManageStateResult',
);
assert.equal(await clearManageStateResult.getText(), 'true');
// click get results again
await driver.clickElement('#retrieveManageState');
// check result array is empty
await driver.delay(500);
await driver.delay(1000);
const retrieveManageStateResult2 = await driver.findElement(
'#retrieveManageStateResult',
);
assert.equal(
await retrieveManageStateResult2.getText(),
'{"testState":[]}',
'{ "testState": [] }',
);
},
);

@ -1,6 +1,5 @@
const { strict: assert } = require('assert');
const { withFixtures } = require('../helpers');
const { PAGES } = require('../webdriver/driver');
const FixtureBuilder = require('../fixture-builder');
const { TEST_SNAPS_WEBSITE_URL } = require('./enums');
@ -17,10 +16,9 @@ describe('Test Snap Notification', function () {
};
await withFixtures(
{
fixtures: new FixtureBuilder()
.withPermissionControllerConnectedToSnapDapp()
.build(),
fixtures: new FixtureBuilder().build(),
ganacheOptions,
failOnConsoleError: false,
title: this.test.title,
},
async ({ driver }) => {
@ -31,21 +29,35 @@ describe('Test Snap Notification', function () {
await driver.press('#password', driver.Key.ENTER);
// navigate to test snaps page
await driver.driver.get(TEST_SNAPS_WEBSITE_URL);
await driver.openNewPage(TEST_SNAPS_WEBSITE_URL);
await driver.delay(1000);
// find and scroll down to snapId5
const snapButton = await driver.findElement('#snapId5');
// find and scroll down to snapId5 and connect
const snapButton = await driver.findElement('#connectNotification');
await driver.scrollToElement(snapButton);
await driver.delay(500);
await driver.fill('#snapId5', 'npm:@metamask/test-snap-notification');
// connect the snap
await driver.clickElement('#connectNotification');
// approve install of snap
await driver.waitUntilXWindowHandles(2, 5000, 10000);
// switch to metamask extension and click connect
await driver.waitUntilXWindowHandles(3, 5000, 10000);
let windowHandles = await driver.getAllWindowHandles();
const extensionPage = windowHandles[0];
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
);
await driver.clickElement(
{
text: 'Connect',
tag: 'button',
},
10000,
);
await driver.delay(2000);
// approve install of snap
await driver.waitUntilXWindowHandles(3, 5000, 10000);
windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
@ -56,14 +68,14 @@ describe('Test Snap Notification', function () {
});
// click send inputs on test snap page
await driver.waitUntilXWindowHandles(1, 5000, 10000);
await driver.waitUntilXWindowHandles(2, 5000, 10000);
windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle('Test Snaps', windowHandles);
await driver.delay(1000);
await driver.clickElement('#sendInAppNotification');
// try to go to the MM pages
await driver.navigate(PAGES.HOME);
// switch back to the extension page
await driver.switchToWindow(extensionPage);
await driver.delay(1500);
// check to see that there is one notification
@ -74,14 +86,14 @@ describe('Test Snap Notification', function () {
// try to click on the account menu icon (via xpath)
await driver.clickElement('.account-menu__icon');
await driver.delay(500);
await driver.delay(1000);
// try to click on the notification item (via xpath)
await driver.clickElement({
text: 'Notifications',
tag: 'div',
});
await driver.delay(500);
await driver.delay(1000);
// look for the correct text in notifications (via xpath)
const notificationResultMessage = await driver.findElement(

@ -16,10 +16,9 @@ describe('Test Snap update', function () {
};
await withFixtures(
{
fixtures: new FixtureBuilder()
.withPermissionControllerConnectedToSnapDapp()
.build(),
fixtures: new FixtureBuilder().build(),
ganacheOptions,
failOnConsoleError: false,
title: this.test.title,
},
async ({ driver }) => {
@ -33,15 +32,32 @@ describe('Test Snap update', function () {
await driver.openNewPage(TEST_SNAPS_WEBSITE_URL);
// find and scroll to the correct card and click first
const snapButton = await driver.findElement('#sendUpdateHello');
const snapButton = await driver.findElement('#connectUpdateNew');
await driver.scrollToElement(snapButton);
await driver.delay(500);
await driver.fill('#snapId7', 'npm:@metamask/test-snap-confirm');
await driver.clickElement('#connectUpdateOld');
await driver.delay(1000);
await driver.clickElement('#connectUpdate');
// approve install of snap
await driver.delay(2000);
// switch to metamask extension and click connect
await driver.waitUntilXWindowHandles(3, 5000, 10000);
let windowHandles = await driver.getAllWindowHandles();
const extensionPage = windowHandles[0];
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
);
await driver.clickElement(
{
text: 'Connect',
tag: 'button',
},
10000,
);
await driver.delay(2000);
// approve install of snap
await driver.waitUntilXWindowHandles(3, 5000, 10000);
windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
@ -58,9 +74,9 @@ describe('Test Snap update', function () {
await driver.delay(1000);
// find and scroll to the correct card and click first
const snapButton2 = await driver.findElement('#snapId7');
const snapButton2 = await driver.findElement('#connectUpdateNew');
await driver.scrollToElement(snapButton2);
await driver.delay(500);
await driver.delay(1000);
await driver.clickElement('#connectUpdateNew');
// switch to metamask extension and click connect
@ -78,33 +94,15 @@ describe('Test Snap update', function () {
tag: 'button',
});
// switch to the original MM tab
await driver.switchToWindow(extensionPage);
await driver.delay(500);
// click on the account menu icon
await driver.clickElement('.account-menu__icon');
await driver.delay(500);
// try to click on the notification item
await driver.clickElement({
text: 'Settings',
tag: 'div',
});
await driver.delay(500);
// try to click on the snaps item
await driver.clickElement({
text: 'Snaps',
tag: 'div',
});
await driver.delay(500);
// navigate to test snap page
await driver.waitUntilXWindowHandles(2, 5000, 10000);
windowHandles = await driver.getAllWindowHandles();
await driver.switchToWindowWithTitle('Test Snaps', windowHandles);
// look for the correct version text
const versionResult = await driver.findElement(
'.snap-settings-card__version',
);
assert.equal(await versionResult.getText(), 'v2.0.0');
const versionResult = await driver.findElement('#updateSnapVersion');
await driver.delay(1000);
assert.equal(await versionResult.getText(), '"2.0.0"');
},
);
});

@ -221,10 +221,10 @@ describe.skip('Create token, approve token and approve token without gas', funct
await driver.clickElement(
'.confirm-approve-content__small-blue-text',
);
await driver.clickElement(
{ text: 'Edit suggested gas fee', tag: 'button' },
10000,
);
await driver.clickElement({
text: 'Edit suggested gas fee',
tag: 'button',
});
const [gasLimitInput, gasPriceInput] = await driver.findElements(
'input[type="number"]',
);

@ -135,7 +135,7 @@ describe('Send ETH non-contract address with data that matches ERC20 transfer da
await driver.clickElement({ text: 'Next', tag: 'button' });
await driver.clickElement({ text: '0xc42...cd28' });
await driver.clickElement({ text: 'New contract' });
const recipientAddress = await driver.findElements({
text: '0xc427D562164062a23a5cFf596A4a3208e72Acd28',
@ -239,23 +239,6 @@ describe('Send ETH from dapp using advanced gas controls', function () {
await driver.fill('#password', 'correct horse battery staple');
await driver.press('#password', driver.Key.ENTER);
// goes to the settings screen
await driver.clickElement('.account-menu__icon');
await driver.clickElement({ text: 'Settings', tag: 'div' });
await driver.clickElement({ text: 'Advanced', tag: 'div' });
await driver.clickElement(
'[data-testid="advanced-setting-show-testnet-conversion"] .settings-page__content-item-col > label > div',
);
const advancedGasTitle = await driver.findElement({
text: 'Advanced gas controls',
tag: 'span',
});
await driver.scrollToElement(advancedGasTitle);
await driver.clickElement(
'[data-testid="advanced-setting-advanced-gas-inline"] .settings-page__content-item-col > label > div',
);
await driver.clickElement('.app-header__logo-container');
// initiates a send from the dapp
await driver.openNewPage('http://127.0.0.1:8080/');
await driver.clickElement({ text: 'Send', tag: 'button' });
@ -272,10 +255,10 @@ describe('Send ETH from dapp using advanced gas controls', function () {
css: '.transaction-total-banner',
text: '0.00021 ETH',
});
await driver.clickElement(
{ text: 'Edit suggested gas fee', tag: 'button' },
10000,
);
await driver.clickElement({
text: 'Edit suggested gas fee',
tag: 'button',
});
await driver.waitForSelector({
css: '.transaction-total-banner',
text: '0.00021 ETH',

@ -57,7 +57,7 @@ describe('Send ETH to a 40 character hexadecimal address', function () {
);
await sendTransactionListItem.click();
await driver.clickElement({ text: 'Activity log', tag: 'summary' });
await driver.clickElement('.sender-to-recipient__name:nth-of-type(2)');
await driver.clickElement('[data-testid="sender-to-recipient__name"]');
// Verify address in activity log
const publicAddress = await driver.findElement(
@ -108,7 +108,7 @@ describe('Send ETH to a 40 character hexadecimal address', function () {
);
await sendTransactionListItem.click();
await driver.clickElement({ text: 'Activity log', tag: 'summary' });
await driver.clickElement('.sender-to-recipient__name:nth-of-type(2)');
await driver.clickElement('[data-testid="sender-to-recipient__name"]');
// Verify address in activity log
const publicAddress = await driver.findElement(
@ -212,7 +212,7 @@ describe('Send ERC20 to a 40 character hexadecimal address', function () {
);
await sendTransactionListItem.click();
await driver.clickElement({ text: 'Activity log', tag: 'summary' });
await driver.clickElement('.sender-to-recipient__name:nth-of-type(2)');
await driver.clickElement('[data-testid="sender-to-recipient__name"]');
// Verify address in activity log
const publicAddress = await driver.findElement(
@ -302,7 +302,7 @@ describe('Send ERC20 to a 40 character hexadecimal address', function () {
);
await sendTransactionListItem.click();
await driver.clickElement({ text: 'Activity log', tag: 'summary' });
await driver.clickElement('.sender-to-recipient__name:nth-of-type(2)');
await driver.clickElement('[data-testid="sender-to-recipient__name"]');
// Verify address in activity log
const publicAddress = await driver.findElement(

@ -56,7 +56,7 @@ describe('Sign Typed Data V4 Signature Request', function () {
const origin = content[0];
const address = content[1];
const message = await driver.findElement(
'.signature-request-message--node-value',
'.signature-request-data__node__value',
);
assert.equal(await title.getText(), 'Signature request');
assert.equal(await name.getText(), 'Ether Mail');
@ -140,7 +140,7 @@ describe('Sign Typed Data V3 Signature Request', function () {
const origin = content[0];
const address = content[1];
const messages = await driver.findElements(
'.signature-request-message--node-value',
'.signature-request-data__node__value',
);
assert.equal(await title.getText(), 'Signature request');
assert.equal(await name.getText(), 'Ether Mail');
@ -154,6 +154,10 @@ describe('Sign Typed Data V3 Signature Request', function () {
assert.equal(await messages[4].getText(), 'Hello, Bob!');
// Approve signing typed data
await driver.clickElement(
'[data-testid="signature-request-scroll-button"]',
);
await driver.delay(regularDelayMs);
await driver.clickElement({ text: 'Sign', tag: 'button' });
await driver.waitUntilXWindowHandles(2);
windowHandles = await driver.getAllWindowHandles();

@ -49,6 +49,7 @@ class Driver {
this.browser = browser;
this.extensionUrl = extensionUrl;
this.timeout = timeout;
this.exceptions = [];
// The following values are found in
// https://github.com/SeleniumHQ/selenium/blob/trunk/javascript/node/selenium-webdriver/lib/input.js#L50-L110
// These should be replaced with string constants 'Enter' etc for playwright.
@ -414,7 +415,9 @@ class Driver {
const htmlSource = await this.driver.getPageSource();
await fs.writeFile(`${filepathBase}-dom.html`, htmlSource);
const uiState = await this.driver.executeScript(
() => window.getCleanAppState && window.getCleanAppState(),
() =>
window.stateHooks.getCleanAppState &&
window.stateHooks.getCleanAppState(),
);
await fs.writeFile(
`${filepathBase}-state.json`,
@ -436,6 +439,15 @@ class Driver {
return browserLogs;
}
async checkBrowserForExceptions() {
const { exceptions } = this;
const cdpConnection = await this.driver.createCDPConnection('page');
await this.driver.onLogException(cdpConnection, function (exception) {
const { description } = exception.exceptionDetails.exception;
exceptions.push(description);
});
}
async checkBrowserForConsoleErrors() {
const ignoredLogTypes = ['WARNING'];
const ignoredErrorMessages = [

@ -1,3 +1,5 @@
/* eslint-disable-next-line */
import { TextEncoder, TextDecoder } from 'util';
import nock from 'nock';
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
@ -102,6 +104,10 @@ if (!window.crypto.getRandomValues) {
window.crypto.getRandomValues = require('polyfill-crypto.getrandomvalues');
}
// TextEncoder/TextDecoder
window.TextEncoder = TextEncoder;
window.TextDecoder = TextDecoder;
// Used to test `clearClipboard` function
if (!window.navigator.clipboard) {
window.navigator.clipboard = {};

@ -103,6 +103,9 @@ const createGetSmartTransactionFeesApiResponse = () => {
export const createSwapsMockStore = () => {
return {
confirmTransaction: {
txData: {},
},
swaps: {
customGas: {
limit: '0x0',
@ -144,6 +147,76 @@ export const createSwapsMockStore = () => {
showFiatInTestnets: true,
},
currentCurrency: 'ETH',
currentNetworkTxList: [
{
id: 6571648590592143,
time: 1667403993369,
status: 'confirmed',
metamaskNetworkId: '5',
originalGasEstimate: '0x7548',
userEditedGasLimit: false,
chainId: '0x5',
loadingDefaults: false,
dappSuggestedGasFees: null,
sendFlowHistory: null,
txParams: {
from: '0x806627172af48bd5b0765d3449a7def80d6576ff',
to: '0x881d40237659c251811cec9c364ef91dc08d300c',
nonce: '0x30',
value: '0x5af3107a4000',
gas: '0x7548',
maxFeePerGas: '0x19286f704d',
maxPriorityFeePerGas: '0x77359400',
},
origin: 'metamask',
actionId: 1667403993358.877,
type: 'swap',
userFeeLevel: 'medium',
defaultGasEstimates: {
estimateType: 'medium',
gas: '0x7548',
maxFeePerGas: '0x19286f704d',
maxPriorityFeePerGas: '0x77359400',
},
sourceTokenSymbol: 'ETH',
destinationTokenSymbol: 'USDC',
destinationTokenDecimals: 6,
destinationTokenAddress: '0xdac17f958d2ee523a2206206994597c13d831ec7',
swapMetaData: {
token_from: 'ETH',
token_from_amount: '0.0001',
token_to: 'USDC',
token_to_amount: '0.15471500',
slippage: 2,
custom_slippage: false,
best_quote_source: 'pmm',
other_quote_selected: false,
other_quote_selected_source: '',
gas_fees: '3.016697',
estimated_gas: '30024',
used_gas_price: '0',
is_hardware_wallet: false,
stx_enabled: false,
current_stx_enabled: false,
stx_user_opt_in: false,
reg_tx_fee_in_usd: 3.02,
reg_tx_fee_in_eth: 0.00193,
reg_tx_max_fee_in_usd: 5.06,
reg_tx_max_fee_in_eth: 0.00324,
max_fee_per_gas: '19286f704d',
max_priority_fee_per_gas: '77359400',
base_and_priority_fee_per_gas: 'efd93d95a',
},
swapTokenValue: '0.0001',
estimatedBaseFee: 'e865e455a',
hash: '0x8216e3696e7deb7ca794703015f17d5114a09362ae98f6a1611203e4c9509243',
submittedTime: 1667403996143,
firstRetryBlockNumber: '0x7838fe',
baseFeePerGas: '0xe0ef7d207',
blockTimestamp: '636290e8',
postTxBalance: '19a61aaaf06e4bd1',
},
],
conversionRate: 1,
contractExchangeRates: {
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': 2,

@ -1,6 +1,7 @@
import React, { useMemo } from 'react';
import React, { useMemo, useState } from 'react';
import { Provider } from 'react-redux';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { mount, shallow } from 'enzyme';
import { Router, MemoryRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
@ -122,3 +123,17 @@ export function renderWithLocalization(component) {
return render(component, { wrapper: Wrapper });
}
export function renderControlledInput(InputComponent, props) {
const ControlledWrapper = () => {
const [value, setValue] = useState('');
return (
<InputComponent
value={value}
onChange={(e) => setValue(e.target.value)}
{...props}
/>
);
};
return { user: userEvent.setup(), ...render(<ControlledWrapper />) };
}

@ -9,6 +9,7 @@
@import 'alerts/alerts';
@import 'app-header/index';
@import 'asset-list-item/asset-list-item';
@import 'beta-header/index';
@import 'cancel-speedup-popover/index';
@import 'confirm-page-container/index';
@import 'confirm-page-container/enableEIP1559V2-notice';

@ -7,6 +7,10 @@ import { DEFAULT_ROUTE } from '../../../helpers/constants/routes';
import { EVENT, EVENT_NAMES } from '../../../../shared/constants/metametrics';
import NetworkDisplay from '../network-display';
///: BEGIN:ONLY_INCLUDE_IN(beta)
import BetaHeader from '../beta-header';
///: END:ONLY_INCLUDE_IN(beta)
export default class AppHeader extends PureComponent {
static propTypes = {
history: PropTypes.object,
@ -23,6 +27,9 @@ export default class AppHeader extends PureComponent {
///: BEGIN:ONLY_INCLUDE_IN(flask)
unreadNotificationsCount: PropTypes.number,
///: END:ONLY_INCLUDE_IN
///: BEGIN:ONLY_INCLUDE_IN(beta)
showBetaHeader: PropTypes.bool,
///: END:ONLY_INCLUDE_IN
onClick: PropTypes.func,
};
@ -112,33 +119,44 @@ export default class AppHeader extends PureComponent {
disableNetworkIndicator,
disabled,
onClick,
///: BEGIN:ONLY_INCLUDE_IN(beta)
showBetaHeader,
///: END:ONLY_INCLUDE_IN(beta)
} = this.props;
return (
<div className="app-header">
<div className="app-header__contents">
<MetaFoxLogo
unsetIconHeight
onClick={async () => {
if (onClick) {
await onClick();
}
history.push(DEFAULT_ROUTE);
}}
/>
<div className="app-header__account-menu-container">
{!hideNetworkIndicator && (
<div className="app-header__network-component-wrapper">
<NetworkDisplay
onClick={(event) => this.handleNetworkIndicatorClick(event)}
disabled={disabled || disableNetworkIndicator}
/>
</div>
)}
{this.renderAccountMenu()}
<>
{
///: BEGIN:ONLY_INCLUDE_IN(beta)
showBetaHeader ? <BetaHeader /> : null
///: END:ONLY_INCLUDE_IN(beta)
}
<div className="app-header">
<div className="app-header__contents">
<MetaFoxLogo
unsetIconHeight
onClick={async () => {
if (onClick) {
await onClick();
}
history.push(DEFAULT_ROUTE);
}}
/>
<div className="app-header__account-menu-container">
{!hideNetworkIndicator && (
<div className="app-header__network-component-wrapper">
<NetworkDisplay
onClick={(event) => this.handleNetworkIndicatorClick(event)}
disabled={disabled || disableNetworkIndicator}
/>
</div>
)}
{this.renderAccountMenu()}
</div>
</div>
</div>
</div>
</>
);
}
}

@ -1,9 +1,14 @@
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { compose } from 'redux';
///: BEGIN:ONLY_INCLUDE_IN(flask)
import { getUnreadNotificationsCount } from '../../../selectors';
///: END:ONLY_INCLUDE_IN
import {
///: BEGIN:ONLY_INCLUDE_IN(flask)
getUnreadNotificationsCount,
///: END:ONLY_INCLUDE_IN
///: BEGIN:ONLY_INCLUDE_IN(beta)
getShowBetaHeader,
///: END:ONLY_INCLUDE_IN
} from '../../../selectors';
import * as actions from '../../../store/actions';
import AppHeader from './app-header.component';
@ -17,6 +22,10 @@ const mapStateToProps = (state) => {
const unreadNotificationsCount = getUnreadNotificationsCount(state);
///: END:ONLY_INCLUDE_IN
///: BEGIN:ONLY_INCLUDE_IN(beta)
const showBetaHeader = getShowBetaHeader(state);
///: END:ONLY_INCLUDE_IN
return {
networkDropdownOpen,
selectedAddress,
@ -25,6 +34,9 @@ const mapStateToProps = (state) => {
///: BEGIN:ONLY_INCLUDE_IN(flask)
unreadNotificationsCount,
///: END:ONLY_INCLUDE_IN
///: BEGIN:ONLY_INCLUDE_IN(beta)
showBetaHeader,
///: END:ONLY_INCLUDE_IN
};
};

@ -217,25 +217,88 @@ export default function ApproveContentCard({
}
ApproveContentCard.propTypes = {
/**
* Whether to show header including icon, transaction fee text and edit button
*/
showHeader: PropTypes.bool,
/**
* Symbol icon
*/
symbol: PropTypes.node,
/**
* Title to be included in the header
*/
title: PropTypes.string,
/**
* Whether to show edit button or not
*/
showEdit: PropTypes.bool,
/**
* Whether to show advanced gas fee options or not
*/
showAdvanceGasFeeOptions: PropTypes.bool,
/**
* Should open customize gas modal when edit button is clicked
*/
onEditClick: PropTypes.func,
/**
* Footer to be shown
*/
footer: PropTypes.node,
/**
* Whether to include border-bottom or not
*/
noBorder: PropTypes.bool,
/**
* Is enhanced gas fee enabled or not
*/
supportsEIP1559V2: PropTypes.bool,
/**
* Whether to render transaction details content or not
*/
renderTransactionDetailsContent: PropTypes.bool,
/**
* Whether to render data content or not
*/
renderDataContent: PropTypes.bool,
/**
* Is multi-layer fee network or not
*/
isMultiLayerFeeNetwork: PropTypes.bool,
/**
* Total sum of the transaction in native currency
*/
ethTransactionTotal: PropTypes.string,
/**
* Current native currency
*/
nativeCurrency: PropTypes.string,
/**
* Current transaction
*/
fullTxData: PropTypes.object,
/**
* Total sum of the transaction converted to hex value
*/
hexTransactionTotal: PropTypes.string,
/**
* Total sum of the transaction in fiat currency
*/
fiatTransactionTotal: PropTypes.string,
/**
* Current fiat currency
*/
currentCurrency: PropTypes.string,
/**
* Is set approve for all or not
*/
isSetApproveForAll: PropTypes.bool,
/**
* Whether a current set approval for all transaction will approve or revoke access
*/
isApprovalOrRejection: PropTypes.bool,
/**
* Current transaction data
*/
data: PropTypes.string,
};

@ -0,0 +1,196 @@
import React from 'react';
import ApproveContentCard from './approve-content-card';
export default {
title: 'Components/App/ApproveContentCard',
id: __filename,
argTypes: {
showHeader: {
control: 'boolean',
},
symbol: {
control: 'array',
},
title: {
control: 'text',
},
showEdit: {
control: 'boolean',
},
showAdvanceGasFeeOptions: {
control: 'boolean',
},
footer: {
control: 'array',
},
noBorder: {
control: 'boolean',
},
supportsEIP1559V2: {
control: 'boolean',
},
renderTransactionDetailsContent: {
control: 'boolean',
},
renderDataContent: {
control: 'boolean',
},
isMultiLayerFeeNetwork: {
control: 'boolean',
},
ethTransactionTotal: {
control: 'text',
},
nativeCurrency: {
control: 'text',
},
fullTxData: {
control: 'object',
},
hexTransactionTotal: {
control: 'text',
},
fiatTransactionTotal: {
control: 'text',
},
currentCurrency: {
control: 'text',
},
isSetApproveForAll: {
control: 'boolean',
},
isApprovalOrRejection: {
control: 'boolean',
},
data: {
control: 'text',
},
onEditClick: {
control: 'onEditClick',
},
},
args: {
showHeader: true,
symbol: <i className="fa fa-tag" />,
title: 'Transaction fee',
showEdit: true,
showAdvanceGasFeeOptions: true,
noBorder: true,
supportsEIP1559V2: false,
renderTransactionDetailsContent: true,
renderDataContent: false,
isMultiLayerFeeNetwork: false,
ethTransactionTotal: '0.0012',
nativeCurrency: 'GoerliETH',
hexTransactionTotal: '0x44364c5bb0000',
fiatTransactionTotal: '1.54',
currentCurrency: 'usd',
isSetApproveForAll: false,
isApprovalOrRejection: false,
data: '',
fullTxData: {
id: 3049568294499567,
time: 1664449552289,
status: 'unapproved',
metamaskNetworkId: '3',
originalGasEstimate: '0xea60',
userEditedGasLimit: false,
chainId: '0x3',
loadingDefaults: false,
dappSuggestedGasFees: {
gasPrice: '0x4a817c800',
gas: '0xea60',
},
sendFlowHistory: [],
txParams: {
from: '0xdd34b35ca1de17dfcdc07f79ff1f8f94868c40a1',
to: '0x55797717b9947b31306f4aac7ad1365c6e3923bd',
value: '0x0',
data: '0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170',
gas: '0xea60',
maxFeePerGas: '0x4a817c800',
maxPriorityFeePerGas: '0x4a817c800',
},
origin: 'https://metamask.github.io',
type: 'approve',
history: [
{
id: 3049568294499567,
time: 1664449552289,
status: 'unapproved',
metamaskNetworkId: '3',
originalGasEstimate: '0xea60',
userEditedGasLimit: false,
chainId: '0x3',
loadingDefaults: true,
dappSuggestedGasFees: {
gasPrice: '0x4a817c800',
gas: '0xea60',
},
sendFlowHistory: [],
txParams: {
from: '0xdd34b35ca1de17dfcdc07f79ff1f8f94868c40a1',
to: '0x55797717b9947b31306f4aac7ad1365c6e3923bd',
value: '0x0',
data: '0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170',
gas: '0xea60',
gasPrice: '0x4a817c800',
},
origin: 'https://metamask.github.io',
type: 'approve',
},
[
{
op: 'remove',
path: '/txParams/gasPrice',
note: 'Added new unapproved transaction.',
timestamp: 1664449553939,
},
{
op: 'add',
path: '/txParams/maxFeePerGas',
value: '0x4a817c800',
},
{
op: 'add',
path: '/txParams/maxPriorityFeePerGas',
value: '0x4a817c800',
},
{
op: 'replace',
path: '/loadingDefaults',
value: false,
},
{
op: 'add',
path: '/userFeeLevel',
value: 'custom',
},
{
op: 'add',
path: '/defaultGasEstimates',
value: {
estimateType: 'custom',
gas: '0xea60',
maxFeePerGas: '0x4a817c800',
maxPriorityFeePerGas: '0x4a817c800',
},
},
],
],
userFeeLevel: 'custom',
defaultGasEstimates: {
estimateType: 'custom',
gas: '0xea60',
maxFeePerGas: '0x4a817c800',
maxPriorityFeePerGas: '0x4a817c800',
},
},
},
};
export const DefaultStory = (args) => {
return <ApproveContentCard {...args} />;
};
DefaultStory.storyName = 'Default';

@ -0,0 +1,36 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Beta Header should match snapshot 1`] = `
<div>
<div
class="box beta-header box--padding-2 box--display-flex box--flex-direction-row box--width-full box--background-color-warning-default"
>
<h6
class="box box--flex-direction-row typography beta-header__message typography--h7 typography--weight-normal typography--style-normal typography--color-warning-inverse"
>
<span>
This is a BETA version. Please report bugs
<a
href="https://metamask.zendesk.com/hc/en-us"
rel="noreferrer noopener"
target="_blank"
>
here
</a>
</span>
</h6>
<button
aria-label="Close"
class="beta-header__button"
data-testid="beta-header-close"
>
<i
class="fa fa-times"
/>
</button>
</div>
</div>
`;

@ -0,0 +1,24 @@
import React from 'react';
import { Provider } from 'react-redux';
import testData from '../../../../.storybook/test-data';
import configureStore from '../../../store/store';
import BetaHeader from '.';
const store = configureStore({
...testData,
metamask: { ...testData.metamask, isUnlocked: true, showBetaHeader: true },
});
export default {
title: 'Components/App/BetaHeader',
decorators: [(story) => <Provider store={store}>{story()}</Provider>],
id: __filename,
};
export const DefaultStory = () => (
<>
<BetaHeader />
</>
);
DefaultStory.storyName = 'Default';

@ -0,0 +1,45 @@
import React from 'react';
import { fireEvent } from '@testing-library/react';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import mockState from '../../../../test/data/mock-state.json';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import BetaHeader from '.';
const mockHideBetaHeader = jest.fn();
jest.mock('../../../store/actions', () => {
return {
hideBetaHeader: () => {
mockHideBetaHeader();
},
};
});
describe('Beta Header', () => {
let store;
beforeEach(() => {
store = configureMockStore([thunk])(mockState);
});
afterEach(() => {
mockHideBetaHeader.mockClear();
});
it('should match snapshot', () => {
const { container } = renderWithProvider(<BetaHeader />, store);
expect(container).toMatchSnapshot();
});
describe('Beta Header', () => {
it('gets hidden when close button is clicked', () => {
const { queryByTestId } = renderWithProvider(<BetaHeader />, store);
const closeButton = queryByTestId('beta-header-close');
fireEvent.click(closeButton);
expect(mockHideBetaHeader).toHaveBeenCalledTimes(1);
});
});
});

@ -0,0 +1,59 @@
import React from 'react';
import { useI18nContext } from '../../../hooks/useI18nContext';
import Box from '../../ui/box/box';
import Typography from '../../ui/typography/typography';
import {
TYPOGRAPHY,
COLORS,
BLOCK_SIZES,
DISPLAY,
} from '../../../helpers/constants/design-system';
import { BETA_BUGS_URL } from '../../../helpers/constants/beta';
import { hideBetaHeader } from '../../../store/actions';
const BetaHeader = () => {
const t = useI18nContext();
return (
<Box
display={DISPLAY.FLEX}
width={BLOCK_SIZES.FULL}
backgroundColor={COLORS.WARNING_DEFAULT}
padding={2}
className="beta-header"
>
<Typography
variant={TYPOGRAPHY.H7}
marginTop={0}
marginBottom={0}
className="beta-header__message"
color={COLORS.WARNING_INVERSE}
>
{t('betaHeaderText', [
<a
href={BETA_BUGS_URL}
key="link"
target="_blank"
rel="noreferrer noopener"
>
{t('here')}
</a>,
])}
</Typography>
<button
className="beta-header__button"
data-testid="beta-header-close"
onClick={() => {
hideBetaHeader();
}}
aria-label={t('close')}
>
<i className="fa fa-times" />
</button>
</Box>
);
};
export default BetaHeader;

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

Loading…
Cancel
Save