diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 000000000..b604b35eb --- /dev/null +++ b/.browserslistrc @@ -0,0 +1 @@ +chrome >= 66, firefox >= 68 \ No newline at end of file diff --git a/.circleci/config.yml b/.circleci/config.yml index 7d1068879..f9decf829 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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 diff --git a/.circleci/scripts/firefox-install.sh b/.circleci/scripts/firefox-install.sh index f2f9f284d..fb574f13e 100755 --- a/.circleci/scripts/firefox-install.sh +++ b/.circleci/scripts/firefox-install.sh @@ -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' diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 6df4c6e65..8f4c7ba9e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -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 - - -## Pre-Merge Checklist +## Pre-merge author checklist -- [ ] PR template is filled out -- [ ] **IF** this PR fixes a bug, a test that _would have_ caught the bug has been added +- [ ] 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 + +- [ ] 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. diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 93758bae9..0f7977000 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -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 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 6f004d274..f945eb7b7 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -23,7 +23,7 @@ on: jobs: analyze: name: Analyze - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest permissions: actions: read contents: read diff --git a/.github/workflows/crowdin_action.yml b/.github/workflows/crowdin_action.yml index f476277ed..a77592321 100644 --- a/.github/workflows/crowdin_action.yml +++ b/.github/workflows/crowdin_action.yml @@ -13,7 +13,7 @@ on: jobs: synchronize-with-crowdin: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: diff --git a/.storybook/initial-states/transactions.js b/.storybook/initial-states/transactions.js index fefafefc4..70885e4a6 100644 --- a/.storybook/initial-states/transactions.js +++ b/.storybook/initial-states/transactions.js @@ -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, }, { diff --git a/README.md b/README.md index 21a67235a..302c32ca6 100644 --- a/README.md +++ b/README.md @@ -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'. diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 9bb059502..42aa54bfd 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -1853,7 +1853,7 @@ "message": "Richtungspfeil" }, "lightTheme": { - "message": "Leicht" + "message": "Hell" }, "likeToImportTokens": { "message": "Möchtest du diese Token hinzufügen?" diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 71eaf9cd9..078fda863 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -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." }, diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index a4ac39f5c..19e8a579b 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -2,6 +2,14 @@ "about": { "message": "關於" }, + "acceptTermsOfUse": { + "message": "我已閱讀並同意$1", + "description": "$1 is the `terms` message" + }, + "accessAndSpendNotice": { + "message": "$1 可能會存取並最多花費以下額度", + "description": "$1 is the url of the site requesting ability to spend" + }, "accessingYourCamera": { "message": "正在存取您的攝影鏡頭..." }, @@ -18,7 +26,10 @@ "message": "帳戶" }, "accountSelectionRequired": { - "message": "必須先選擇一個帳戶!" + "message": "您必須先選擇一個帳戶!" + }, + "active": { + "message": "啟用" }, "activity": { "message": "交易紀錄" @@ -32,6 +43,32 @@ "addAlias": { "message": "新增化名" }, + "addContact": { + "message": "新增合約" + }, + "addCustomToken": { + "message": "Add Custom Token" + }, + "addEthereumChainConfirmationDescription": { + "message": "這會允許在 MetaMask 內使用這個網路。" + }, + "addEthereumChainConfirmationRisks": { + "message": "MetaMask 不會對自訂的網路做驗證。" + }, + "addEthereumChainConfirmationRisksLearnMore": { + "message": "了解更多關於$1的事。", + "description": "$1 is a link with text that is provided by the 'addEthereumChainConfirmationRisksLearnMoreLink' key" + }, + "addEthereumChainConfirmationRisksLearnMoreLink": { + "message": "詐騙與網路安全風險", + "description": "Link text for the 'addEthereumChainConfirmationRisksLearnMore' translation key" + }, + "addEthereumChainConfirmationTitle": { + "message": "允許這個網站新增一個網路?" + }, + "addFriendsAndAddresses": { + "message": "新增朋友和您信任的位址" + }, "addNetwork": { "message": "新增網路" }, @@ -47,6 +84,37 @@ "advancedOptions": { "message": "進階選項" }, + "affirmAgree": { + "message": "我同意" + }, + "alertDisableTooltip": { + "message": "這可以在「設定 > 提醒」裡變更" + }, + "alertSettingsUnconnectedAccount": { + "message": "選擇尚未連結的帳戶瀏覽一個網站時" + }, + "alertSettingsUnconnectedAccountDescription": { + "message": "當您瀏覽一個使用 web3 的網站,但目前選擇的帳戶沒有連結時,這個提醒會顯示在一個彈跳視窗。" + }, + "alertSettingsWeb3ShimUsage": { + "message": "當一個網站試著使用已經移除的 window.web3 API" + }, + "alertSettingsWeb3ShimUsageDescription": { + "message": "當您瀏覽一個嘗試使用已經移除的 window.web3 API 的網站,可能會因此故障時,這個提醒會顯示在一個彈跳視窗。" + }, + "alerts": { + "message": "提醒" + }, + "allowExternalExtensionTo": { + "message": "允許這個外部擴充功能:" + }, + "allowThisSiteTo": { + "message": "允許這個網站:" + }, + "allowWithdrawAndSpend": { + "message": "允許 $1 提款或最多花費以下額度:", + "description": "The url of the site that requested permission to 'withdraw and spend'" + }, "amount": { "message": "數量" }, @@ -54,42 +122,43 @@ "message": "以太坊瀏覽器擴充插件", "description": "The description of the application" }, - "appName": { - "message": "MetaMask", - "description": "The name of the application" - }, - "appNameBeta": { - "message": "MetaMask Beta", - "description": "The name of the application (Beta)" - }, - "appNameFlask": { - "message": "MetaMask Flask", - "description": "The name of the application (Flask)" - }, "approve": { + "message": "批准花費上限" + }, + "approveButtonText": { "message": "批准" }, + "approveSpendLimit": { + "message": "批准 $1 花費限額", + "description": "The token symbol that is being approved" + }, "approved": { - "message": "已批准" + "message": "已許可" }, "asset": { "message": "資產" }, + "assetOptions": { + "message": "資產選項" + }, "assets": { "message": "資產" }, "attemptToCancel": { - "message": "嘗試取消?" + "message": "嘗試取消?" }, "attemptToCancelDescription": { - "message": "送出取消並不保證您的交易一定會被取消。若取消成功,將會收取上方顯示交易費用" + "message": "送出取消請求並不保證您原本的交易一定會被取消。若取消成功,將會收取上方顯示交易費用。" }, "attemptingConnect": { - "message": "正在嘗試連接區塊鏈。" + "message": "正在嘗試連結區塊鏈。" }, "attributions": { "message": "來源" }, + "authorizedPermissions": { + "message": "您已經授予以下權限" + }, "autoLockTimeLimit": { "message": "自動登出計時器(分)" }, @@ -103,13 +172,13 @@ "message": "上一頁" }, "backToAll": { - "message": "回到所有" + "message": "回到全部" }, "backupApprovalInfo": { "message": "在裝置遺失、忘記密碼、需要重新安裝 MetaMask、或是想在另一裝置開啟錢包的情形下,你需要此秘密代碼來復原錢包。" }, "backupApprovalNotice": { - "message": "備份你的秘密還原代碼,安全防護錢包與資金。" + "message": "備份你的助憶詞,保護你的錢包與資金。" }, "backupNow": { "message": "現在備份" @@ -126,8 +195,11 @@ "blockExplorerUrl": { "message": "區塊鏈瀏覽器" }, + "blockExplorerUrlDefinition": { + "message": "這個網路的區塊鏈瀏覽器網址。" + }, "blockExplorerView": { - "message": "在 $1 觀看帳號 ", + "message": "在 $1 檢視帳戶", "description": "$1 replaced by URL for custom block explorer" }, "browserNotSupported": { @@ -140,14 +212,23 @@ "message": "用 Wyre 購買 $1" }, "buyWithWyreDescription": { - "message": "Wyre 讓你使用信用卡在 MetaMask 帳號中直接存入 $1 。" + "message": "Wyre 讓你使用信用卡在 MetaMask 帳戶中直接存入 $1 。" }, "bytes": { "message": "位元組" }, + "canToggleInSettings": { + "message": "您可以在「設定 -> 提醒」中重新啟用這個通知。" + }, "cancel": { "message": "取消" }, + "cancelEdit": { + "message": "Cancel Edit" + }, + "cancelSpeedUp": { + "message": "cancel or speed up a tranaction." + }, "cancellationGasFee": { "message": "需要的手續費" }, @@ -157,8 +238,11 @@ "chainId": { "message": "鏈 ID" }, + "chainIdDefinition": { + "message": "鏈 ID 用來簽署這個網路上的交易。" + }, "chromeRequiredForHardwareWallets": { - "message": "您需要在 Google Chrome 瀏覽器使用 MetaMask 連結您的硬體錢包" + "message": "您需要在 Google Chrome 瀏覽器使用 MetaMask 連結您的硬體錢包。" }, "clickToRevealSeed": { "message": "點選顯示助憶詞" @@ -178,26 +262,103 @@ "confirmed": { "message": "已確認" }, + "confusableUnicode": { + "message": "'$1' 和 '$2' 相像。" + }, + "confusableZeroWidthUnicode": { + "message": "發現零寬度字元。" + }, + "confusingEnsDomain": { + "message": "我們在 ENS 名稱中偵測到一些容易混淆的字元。請再次確認 ENS 名稱來避免被詐騙的可能。" + }, "congratulations": { "message": "恭喜" }, "connect": { "message": "連線" }, + "connectAccountOrCreate": { + "message": "連結帳戶或建立新的" + }, "connectHardwareWallet": { "message": "連線硬體錢包" }, + "connectManually": { + "message": "手動連結到目前的網站" + }, + "connectTo": { + "message": "連結到 $1", + "description": "$1 is the name/origin of a web3 site/application that the user can connect to metamask" + }, + "connectToAll": { + "message": "連結到您的全部$1", + "description": "$1 will be replaced by the translation of connectToAllAccounts" + }, + "connectToAllAccounts": { + "message": "帳戶", + "description": "will replace $1 in connectToAll, completing the sentence 'connect to all of your accounts', will be text that shows list of accounts on hover" + }, + "connectToMultiple": { + "message": "連結到 $1", + "description": "$1 will be replaced by the translation of connectToMultipleNumberOfAccounts" + }, + "connectToMultipleNumberOfAccounts": { + "message": "$1 個帳戶", + "description": "$1 is the number of accounts to which the web3 site/application is asking to connect; this will substitute $1 in connectToMultiple" + }, + "connectWithMetaMask": { + "message": "連結到 MetaMask" + }, + "connectedAccountsDescriptionPlural": { + "message": "您有 $1 個帳戶已連結到這個網站。", + "description": "$1 is the number of accounts" + }, + "connectedAccountsDescriptionSingular": { + "message": "您有 1 個帳戶已連結到這個網站。" + }, + "connectedAccountsEmptyDescription": { + "message": "MetaMask 尚未連結到這個網站。要連結到一個 web3 的網站,在該網站上尋找「連線」的按鈕。" + }, + "connectedSites": { + "message": "已連結的網站" + }, + "connectedSitesDescription": { + "message": "$1 已經連結到這些網站。他們可以檢視您的帳戶位址。", + "description": "$1 is the account name" + }, + "connectedSitesEmptyDescription": { + "message": "$1 尚未連結到任何網站。", + "description": "$1 is the account name" + }, + "connecting": { + "message": "連線中..." + }, "connectingTo": { - "message": "連線到$1" + "message": "連線到 $1" }, "connectingToGoerli": { - "message": "連接至 Goerli 測試網路" + "message": "連線到 Goerli 測試網路" }, "connectingToMainnet": { - "message": "連線到主 Ethereum 網路" + "message": "連線到 Ethereum 主網路" + }, + "connectingToSepolia": { + "message": "連線到 Sepolia 測試網路" + }, + "contactUs": { + "message": "聯絡我們" + }, + "contacts": { + "message": "聯絡人" + }, + "continue": { + "message": "繼續" }, "continueToWyre": { - "message": "繼續至 Wyre" + "message": "繼續前往 Wyre" + }, + "contractAddressError": { + "message": "您正在將代幣傳送到代幣合約的位址。這可能會導致這些代幣遺失。" }, "contractDeployment": { "message": "部署合約" @@ -235,23 +396,61 @@ "currencyConversion": { "message": "轉換匯率" }, + "currencySymbol": { + "message": "貨幣代碼" + }, + "currencySymbolDefinition": { + "message": "用來代表這個網路的貨幣的代碼。" + }, + "currentAccountNotConnected": { + "message": "您目前的帳戶並未連結" + }, + "currentExtension": { + "message": "目前擴充功能頁面" + }, "currentLanguage": { - "message": "當前語言" + "message": "目前語言" + }, + "customSpendLimit": { + "message": "自訂花費限制" + }, + "customSpendingCap": { + "message": "自訂花費上限" }, "customToken": { "message": "自訂代幣" }, "decimal": { - "message": "小數點精度" + "message": "小數點位數" }, "decimalsMustZerotoTen": { - "message": "小數點後位數至少為0, 最多為36." + "message": "小數點位數至少為 0,至多為 36。" + }, + "decrypt": { + "message": "解密" + }, + "decryptCopy": { + "message": "複製已加密的訊息" + }, + "decryptInlineError": { + "message": "這個訊息無法被解密,因為以下錯誤:$1", + "description": "$1 is error message" + }, + "decryptMessageNotice": { + "message": "$1 想要讀取這個訊息來完成您的動作", + "description": "$1 is the web3 site name" + }, + "decryptMetamask": { + "message": "解密訊息" + }, + "decryptRequest": { + "message": "解密請求" }, "delete": { "message": "刪除" }, "deleteAccount": { - "message": "刪除帳號" + "message": "刪除帳戶" }, "deleteNetwork": { "message": "刪除網路?" @@ -259,12 +458,52 @@ "deleteNetworkDescription": { "message": "你確定要刪除網路嗎?" }, + "depositCrypto": { + "message": "存入 $1", + "description": "$1 represents the crypto symbol to be purchased" + }, "details": { "message": "詳情" }, + "directDepositCrypto": { + "message": "直接存入 $1" + }, + "directDepositCryptoExplainer": { + "message": "如果您已經擁有一些 $1,直接存入功能是讓新錢包最快取得的方式。" + }, + "disconnect": { + "message": "中斷連結" + }, + "disconnectAllAccounts": { + "message": "中斷所有帳戶的連結" + }, + "disconnectAllAccountsConfirmationDescription": { + "message": "您確定要中斷連結?您可能會失去網站的一些功能。" + }, + "disconnectPrompt": { + "message": "中斷 $1" + }, + "disconnectThisAccount": { + "message": "中斷這個帳戶的連結" + }, + "dismiss": { + "message": "忽略" + }, + "dismissReminderDescriptionField": { + "message": "把這個選項打開來忽略備份助憶詞的提醒。我們強烈建議您備份助憶詞,避免資金遺失" + }, + "dismissReminderField": { + "message": "忽略備份助憶詞的提醒" + }, + "domain": { + "message": "網域" + }, "done": { "message": "完成" }, + "dontShowThisAgain": { + "message": "不要再顯示" + }, "downloadGoogleChrome": { "message": "下載 Google Chrome 瀏覽器" }, @@ -283,8 +522,27 @@ "editContact": { "message": "編輯聯絡資訊" }, + "editNonceField": { + "message": "編輯 nonce" + }, + "editNonceMessage": { + "message": "這是一個進階功能,請小心使用。" + }, + "editPermission": { + "message": "編輯權限" + }, + "enableAutoDetect": { + "message": " Enable Autodetect" + }, + "encryptionPublicKeyNotice": { + "message": "$1 想要取得您的加密公鑰。同意之後,這個網站就可以向您發送加密訊息。", + "description": "$1 is the web3 site name" + }, + "encryptionPublicKeyRequest": { + "message": "請求加密公鑰" + }, "endOfFlowMessage1": { - "message": "你通過測試了—安全存放助記詞,這是你的責任!" + "message": "您通過測試了—安全存放助憶詞,這是您的責任!" }, "endOfFlowMessage10": { "message": "都完成了" @@ -296,32 +554,84 @@ "message": "在多處儲存備份。" }, "endOfFlowMessage4": { - "message": "不要分享此助記祠給任何人" + "message": "不要分享此助憶詞給任何人。" }, "endOfFlowMessage5": { - "message": "小心網路釣魚!MetaMask 永遠不會主動詢問你的助記詞。" + "message": "小心網路釣魚!MetaMask 永遠不會主動詢問你的助憶詞。" }, "endOfFlowMessage6": { - "message": "如你需要再次備份助記詞,可至設定 -> 安全。" + "message": "如你需要再次備份助憶詞,可至「設定 -> 安全」。" + }, + "endOfFlowMessage7": { + "message": "如果您有任何疑問或看到哪裡怪怪的,從$1聯絡我們尋求支援。", + "description": "$1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." }, "endOfFlowMessage8": { - "message": "MetaMask 無法還原你的助記詞。暸解更多" + "message": "MetaMask 無法還原你的助憶詞。" }, "endOfFlowMessage9": { "message": "深入瞭解。" }, + "endpointReturnedDifferentChainId": { + "message": "這個 RPC 端點回傳了一個不同的鏈 ID:$1", + "description": "$1 is the return value of eth_chainId from an RPC endpoint" + }, + "ensIllegalCharacter": { + "message": "Illegal Character for ENS." + }, "ensNotFoundOnCurrentNetwork": { "message": "現有網路中找不到 ENS 名稱。嘗試轉換到主要以太坊網路。" }, "ensRegistrationError": { "message": "ENS 名稱註冊錯誤" }, + "ensUnknownError": { + "message": "ENS Lookup failed." + }, + "enterMaxSpendLimit": { + "message": "輸入最大花費限制" + }, "enterPassword": { "message": "請輸入密碼" }, "enterPasswordContinue": { "message": "請輸入密碼" }, + "errorCode": { + "message": "代碼:$1", + "description": "Displayed error code for debugging purposes. $1 is the error code" + }, + "errorDetails": { + "message": "錯誤詳細資訊", + "description": "Title for collapsible section that displays error details for debugging purposes" + }, + "errorMessage": { + "message": "訊息:$1", + "description": "Displayed error message for debugging purposes. $1 is the error message" + }, + "errorName": { + "message": "代碼:$1", + "description": "Displayed error name for debugging purposes. $1 is the error name" + }, + "errorPageMessage": { + "message": "重新整理頁面然後再試一次,或從$1聯絡我們尋求支援。", + "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." + }, + "errorPagePopupMessage": { + "message": "重新開啟彈跳視窗然後再試一次,或從$1聯絡我們尋求支援。", + "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." + }, + "errorPageTitle": { + "message": "MetaMask 遭遇錯誤", + "description": "Title of generic error page" + }, + "errorStack": { + "message": "堆疊資訊:", + "description": "Title for error stack, which is displayed for debugging purposes" + }, + "ethGasPriceFetchWarning": { + "message": "顯示備用 gas 價格,因為目前主要的 gas 估計服務無法使用。" + }, "ethereumPublicAddress": { "message": "以太坊公開位址" }, @@ -332,14 +642,26 @@ "message": "展開畫面" }, "exportPrivateKey": { - "message": "導出私鑰" + "message": "匯出私鑰" + }, + "externalExtension": { + "message": "外部擴充功能" }, "failed": { - "message": "交易失败" + "message": "交易失敗" + }, + "failedToFetchChainId": { + "message": "無法取得鏈 ID。您的 RPC URL 正確嗎?" + }, + "failureMessage": { + "message": "有東西出錯了,所以無法完成這個動作" }, "fast": { "message": "快" }, + "feeAssociatedRequest": { + "message": "這個請求會附帶一筆手續費。" + }, "fiat": { "message": "法定貨幣", "description": "Exchange type" @@ -348,14 +670,24 @@ "message": "檔案匯入失敗?點擊這裡!", "description": "Helps user import their account from a JSON file" }, + "forbiddenIpfsGateway": { + "message": "禁止使用的 IPFS Gateway:請指定一個 CID gateway" + }, "forgetDevice": { - "message": "移除此裝置" + "message": "忘記此裝置" }, "from": { "message": "來源帳戶" }, + "fromAddress": { + "message": "來自:$1", + "description": "$1 is the address to include in the From label. It is typically shortened first using shortenAddress" + }, + "functionApprove": { + "message": "函式:Approve" + }, "functionType": { - "message": "功能類型" + "message": "函式型別" }, "gasLimit": { "message": "Gas 上限" @@ -366,18 +698,35 @@ "gasLimitTooLow": { "message": "Gas 上限至少為 21000" }, + "gasLimitTooLowWithDynamicFee": { + "message": "Gas 上限至少為 $1", + "description": "$1 is the custom gas limit, in decimal." + }, "gasPrice": { "message": "Gas 價格 (GWEI)" }, + "gasPriceExcessive": { + "message": "您的 gas 費用設定得太高了。考慮降低一些。" + }, + "gasPriceExcessiveInput": { + "message": "Gas 價格極高" + }, "gasPriceExtremelyLow": { "message": "Gas 價格極低" }, + "gasPriceFetchFailed": { + "message": "由於網路錯誤,gas 價格估計失敗。" + }, "gasPriceInfoTooltipContent": { - "message": "Gas 價格代表願意為交易手續費付出的單位價格,價格越高交易處理速度越快" + "message": "Gas 價格代表願意為交易手續費付出的單位價格,價格越高交易處理速度越快。" }, "gasUsed": { "message": "Gas 用量" }, + "gdprMessagePrivacyPolicy": { + "message": "Privacy Policy here", + "description": "this translation is intended to be exclusively used as the replacement for the $1 in the gdprMessage translation" + }, "general": { "message": "一般" }, @@ -385,12 +734,15 @@ "message": "取得以太幣" }, "getEtherFromFaucet": { - "message": "從水管取得 $1 以太幣。", + "message": "從水龍頭取得 $1 上的以太幣", "description": "Displays network name for Ether faucet" }, "getStarted": { "message": "開始使用" }, + "goBack": { + "message": "Go Back" + }, "goerli": { "message": "Goerli 測試網路" }, @@ -403,6 +755,13 @@ "hardwareWalletConnected": { "message": "硬體錢包已連線" }, + "hardwareWalletLegacyDescription": { + "message": "(舊版)", + "description": "Text representing the MEW path" + }, + "hardwareWalletSupportLinkConversion": { + "message": "點選這裡" + }, "hardwareWallets": { "message": "連線硬體錢包" }, @@ -414,7 +773,7 @@ "description": "as in -click here- for more information (goes with troubleTokenBalances)" }, "hexData": { - "message": "16進位資料" + "message": "十六進位資料" }, "hide": { "message": "隱藏" @@ -422,6 +781,13 @@ "hideTokenPrompt": { "message": "隱藏代幣?" }, + "hideTokenSymbol": { + "message": "隱藏 $1", + "description": "$1 is the symbol for a token (e.g. 'DAI')" + }, + "hideZeroBalanceTokens": { + "message": "隱藏零餘額的代幣" + }, "history": { "message": "紀錄" }, @@ -433,29 +799,42 @@ "message": "匯入帳戶" }, "importAccountMsg": { - "message": " 匯入的帳戶與您原有 MetaMask 帳戶的助憶詞並無關聯。請查看與匯入帳戶相關的資料 " + "message": " 匯入的帳戶並不會與您原有 MetaMask 帳戶助憶詞建立關聯。請用以下連結瞭解匯入帳戶相關的資料: " }, "importAccountSeedPhrase": { "message": "利用助憶詞還原" }, + "importMyWallet": { + "message": "Import My Wallet" + }, + "importTokensCamelCase": { + "message": "Import Tokens" + }, "importWallet": { "message": "匯入錢包" }, + "importYourExisting": { + "message": "使用助憶詞匯入您既有的錢包" + }, "imported": { "message": "已匯入私鑰", "description": "status showing that an account has been fully loaded into the keyring" }, + "infuraBlockedNotification": { + "message": "MetaMask 無法連線到區塊鏈主機。在$1查看可能的原因。", + "description": "$1 is a clickable link with with text defined by the 'here' key" + }, "initialTransactionConfirmed": { "message": "交易已確認" }, "insufficientBalance": { - "message": "餘額不足" + "message": "餘額不足。" }, "insufficientFunds": { - "message": "資金不足" + "message": "資金不足。" }, "insufficientTokens": { - "message": "代幣不足" + "message": "代幣不足。" }, "invalidAddress": { "message": "錯誤的位址" @@ -464,17 +843,52 @@ "message": "接收位址錯誤" }, "invalidAddressRecipientNotEthNetwork": { - "message": "非 ETH 網路,設定至小寫" + "message": "非 ETH 網路,請設定至小寫" }, "invalidBlockExplorerURL": { "message": "無效的區塊鏈瀏覽器 URL" }, + "invalidChainIdTooBig": { + "message": "無效的鏈 ID。鏈 ID 數值過大。" + }, + "invalidCustomNetworkAlertContent1": { + "message": "必須重新輸入自訂網路 '$1' 的鏈 ID。", + "description": "$1 is the name/identifier of the network." + }, + "invalidCustomNetworkAlertContent2": { + "message": "為了避免您被惡意或錯誤設定的網路提供者侵害,自訂網路現在一律需要提供鏈 ID。" + }, + "invalidCustomNetworkAlertContent3": { + "message": "前往「設定 > 網路」然後輸入鏈 ID。您可以在 $1 找到大部分熱門網路的鏈 ID。", + "description": "$1 is a link to https://chainid.network" + }, + "invalidCustomNetworkAlertTitle": { + "message": "無效的自訂網路" + }, + "invalidHexNumber": { + "message": "無效的十六進位數值。" + }, + "invalidHexNumberLeadingZeros": { + "message": "無效的十六進位數值。請去掉開頭的零。" + }, + "invalidIpfsGateway": { + "message": "無效的 IPFS Gateway:值必須要是一個合法的 URL" + }, + "invalidNumber": { + "message": "無效的數值。輸入一個小數或 0x開頭的十六進位數值。" + }, + "invalidNumberLeadingZeros": { + "message": "無效的數值。請去掉開頭的零。" + }, "invalidRPC": { - "message": "無效的 RPC URI" + "message": "無效的 RPC URL" }, "invalidSeedPhrase": { "message": "無效的助憶詞" }, + "ipfsGatewayDescription": { + "message": "輸入用於解析 ENS 內容的 IPFS CID gateway URL。" + }, "jsonFile": { "message": "JSON 格式檔案", "description": "format for importing an account" @@ -482,11 +896,23 @@ "knownAddressRecipient": { "message": "已知合約位址" }, + "knownTokenWarning": { + "message": "This action will edit tokens that are already listed in your wallet, which can be used to phish you. Only approve if you are certain that you mean to change what these tokens represent." + }, + "lastConnected": { + "message": "最近連結" + }, "learnMore": { "message": "了解更多" }, "ledgerAccountRestriction": { - "message": "您必須使用最後的帳戶才能產生新帳戶" + "message": "您必須使用最後一個的帳戶才能產生新帳戶" + }, + "ledgerLocked": { + "message": "無法連上 Ledger 裝置。請確定您的裝置已解鎖,而且已開啟 Ethereum 應用程式。" + }, + "ledgerTimeout": { + "message": "Ledger Live 花了太多時間回應,或連線逾時。請確定您的 Ledger Live 應用程式已開啟而且裝置已解鎖。" }, "letsGoSetUp": { "message": "好,我們開始吧!" @@ -509,9 +935,15 @@ "lock": { "message": "鎖定" }, + "lockTimeTooGreat": { + "message": "鎖定時間過長" + }, "mainnet": { "message": "以太坊 主網路" }, + "makeAnotherSwap": { + "message": "建立新的 swap" + }, "max": { "message": "最大值" }, @@ -524,28 +956,76 @@ "message": { "message": "訊息" }, + "metaMaskConnectStatusParagraphOne": { + "message": "您現在能夠在 MetaMask 裡更仔細地控制您帳戶的連結。" + }, + "metaMaskConnectStatusParagraphThree": { + "message": "按這裡管理您已連結的帳戶。" + }, + "metaMaskConnectStatusParagraphTwo": { + "message": "連線狀態按鈕會顯示您正在造訪的網站是否已經連結到您目前選擇的帳戶。" + }, "metamaskDescription": { "message": "MetaMask 是以太坊安全身份識別金庫" }, + "metamaskSwapsOfflineDescription": { + "message": "MetaMask Swaps 正在維護。請晚點再來看看。" + }, "metamaskVersion": { "message": "MetaMask 版本" }, + "mismatchedChainLinkText": { + "message": "驗證網路的詳細資料", + "description": "Serves as link text for the 'mismatchedChain' key. This text will be embedded inside the translation for that key." + }, "mustSelectOne": { - "message": "必須選擇至少 1 代幣" + "message": "必須選擇至少 1 代幣。" }, "myAccounts": { "message": "我的帳戶" }, + "name": { + "message": "名稱" + }, + "needCryptoInWallet": { + "message": "要使用 MetaMask 存取去中心化應用程式時,您的錢包中需要有 $1。", + "description": "$1 represents the cypto symbol to be purchased" + }, + "needHelp": { + "message": "需要幫助?聯繫$1", + "description": "$1 represents `needHelpLinkText`, the text which goes in the help link" + }, + "needHelpLinkText": { + "message": "MetaMask 支援" + }, "needImportFile": { - "message": "您必須選擇一個檔案來匯入", + "message": "您必須選擇一個檔案來匯入。", "description": "User is important an account and needs to add a file to continue" }, "negativeETH": { - "message": "不能送出負值的以太幣" + "message": "不能送出負值的以太幣。" + }, + "networkDetails": { + "message": "網路詳細資料" }, "networkName": { "message": "網路名稱" }, + "networkNameDefinition": { + "message": "對應這個網路的名稱。" + }, + "networkNameTestnet": { + "message": "測試網路" + }, + "networkSettingsChainIdDescription": { + "message": "鏈 ID 用來簽署交易。它必須和該網路回傳的鏈 ID 一致。您可以輸入一個十進位數值或 0x 開頭的十六進位數值,但我們會顯示成十進位。" + }, + "networkURL": { + "message": "網路 URL" + }, + "networkURLDefinition": { + "message": "用來存取這個網路的 URL。" + }, "networks": { "message": "網路" }, @@ -577,15 +1057,28 @@ "next": { "message": "下一頁" }, + "nextNonceWarning": { + "message": "Nonce 比建議的 $1 高", + "description": "The next nonce according to MetaMask's internal logic" + }, + "nftTokenIdPlaceholder": { + "message": "Enter the collectible ID" + }, + "noAccountsFound": { + "message": "指定的搜尋條件找不到帳戶" + }, "noAddressForName": { "message": "此 ENS 尚未指定位址。" }, "noAlreadyHaveSeed": { - "message": "不,我已經有助記詞" + "message": "不,我已經有助憶詞" }, "noConversionRateAvailable": { "message": "尚未有匯率比較值" }, + "noThanks": { + "message": "不了" + }, "noTransactions": { "message": "尚未有交易" }, @@ -595,15 +1088,49 @@ "noWebcamFoundTitle": { "message": "找不到攝影鏡頭" }, + "nonceField": { + "message": "自訂交易 nonce" + }, + "nonceFieldDescription": { + "message": "打開此選項來在交易確認視窗中更改 nonce (交易編號)。這是一個進階功能,請小心使用。" + }, + "nonceFieldHeading": { + "message": "自訂 nonce" + }, + "notCurrentAccount": { + "message": "這是正確的帳戶嗎?它與您的錢包目前所選擇的帳戶不同" + }, "notEnoughGas": { "message": "Gas 不夠" }, + "ofTextNofM": { + "message": "之" + }, "off": { - "message": "關" + "message": "關閉" + }, + "offlineForMaintenance": { + "message": "離線維護中" + }, + "ok": { + "message": "確認" }, "on": { "message": "開啟" }, + "onboardingPinExtensionBillboardAccess": { + "message": "Full Access" + }, + "onboardingReturnNotice": { + "message": "\"$1\" 將會關閉這個分頁並且導回到 $2", + "description": "Return the user to the site that initiated onboarding" + }, + "onlyAddTrustedNetworks": { + "message": "惡意的網路提供者可以欺騙您區塊鏈上的狀態或記錄您的網路活動。請只新增您信任的自訂網路。" + }, + "onlyConnectTrust": { + "message": "記住,只連線到您信任的網站。" + }, "origin": { "message": "來源" }, @@ -626,20 +1153,30 @@ "message": "您所輸入的密碼不一致" }, "pastePrivateKey": { - "message": "請貼上您的私鑰字串:", + "message": "請貼上您的私鑰字串:", "description": "For importing an account from a private key" }, "pending": { "message": "等待處理" }, + "permissions": { + "message": "權限" + }, "personalAddressDetected": { - "message": "偵測為個人位址,請輸入代幣合約位址" + "message": "偵測到這是個人位址,請輸入代幣合約位址" + }, + "plusXMore": { + "message": "+ $1 更多", + "description": "$1 is a number of additional but unshown items in a list- this message will be shown in place of those items" + }, + "prev": { + "message": "前一頁" }, "primaryCurrencySetting": { - "message": "主要顯示" + "message": "主要貨幣" }, "primaryCurrencySettingDescription": { - "message": "選擇使用以太幣(ETH)或是法定貨幣(例如:美金)為主要錢幣顯示單位" + "message": "選擇原生來優先使用鏈上原生貨幣 (例如 ETH) 顯示金額。選擇法定貨幣來優先使用您選擇的法定貨幣顯示金額。" }, "privacyMsg": { "message": "隱私政策" @@ -649,22 +1186,37 @@ "description": "select this type of file to use to import an account" }, "privateKeyWarning": { - "message": "注意:永遠不要公開這個私鑰。任何取得這把私鑰的人都可以竊取這個帳戶中的所有資產。" + "message": "警告:永遠不要公開這個私鑰。任何取得這把私鑰的人都可以竊取這個帳戶中的所有資產。" }, "privateNetwork": { "message": "私有網路" }, + "proposedApprovalLimit": { + "message": "提議的允許量限制" + }, + "provide": { + "message": "提供" + }, + "publicAddress": { + "message": "公開位址" + }, "queue": { "message": "佇列" }, + "queued": { + "message": "已排入佇列" + }, "readdToken": { - "message": "未來可以隨時重新加入此代幣" + "message": "您未來可以在帳戶選項中的「新增代幣」重新加入此代幣。" + }, + "receive": { + "message": "接收" }, "recents": { "message": "最近" }, "recipientAddressPlaceholder": { - "message": "搜尋,公開地址 (0x),或 ENS" + "message": "搜尋、公開位址 (0x)、或 ENS" }, "reject": { "message": "拒絕" @@ -673,7 +1225,7 @@ "message": "全部拒絕" }, "rejectTxsDescription": { - "message": "您將批次拒絕 $1 筆交易." + "message": "您將批次拒絕 $1 筆交易。" }, "rejectTxsN": { "message": "拒絕 $1 筆交易" @@ -691,7 +1243,7 @@ "message": "移除帳戶" }, "removeAccountDescription": { - "message": "此帳戶將由錢包中移除。在繼續進行之前請確認您已經匯出並妥善備份助憶詞或私鑰。" + "message": "此帳戶將由錢包中移除。在繼續進行之前請確認您已經匯出並妥善備份助憶詞或私鑰。您可以從帳戶下拉選單中匯入或再次建立帳戶。" }, "requestsAwaitingAcknowledgement": { "message": "請求正在等待確認" @@ -706,7 +1258,16 @@ "message": "重置帳戶" }, "resetAccountDescription": { - "message": "重置帳戶將清除您的交易紀錄" + "message": "重置帳戶將清除您的交易紀錄。這將不會改變您的帳戶餘額或要求您重新輸入助憶詞。" + }, + "restore": { + "message": "還原" + }, + "retryTransaction": { + "message": "重試交易" + }, + "reusedTokenNameWarning": { + "message": "這裡的其中一個代幣重複使用了您關注的另一個代幣的符號,這可能會造成混淆甚至讓您被誤導。" }, "revealSeedWords": { "message": "顯示助憶詞" @@ -718,7 +1279,7 @@ "message": "絕對不要在公共場合輸入助憶詞!這可被用來竊取您的帳戶。" }, "revealSeedWordsWarningTitle": { - "message": "請勿將助憶詞洩漏予他人" + "message": "請勿將助憶詞洩漏予他人!" }, "rpcUrl": { "message": "新的 RPC URL" @@ -727,7 +1288,7 @@ "message": "儲存" }, "saveAsCsvFile": { - "message": "儲存為CSV格式檔案" + "message": "儲存為 CSV 格式檔案" }, "scanInstructions": { "message": "請將 QR code 放在攝影鏡頭前面" @@ -735,9 +1296,15 @@ "scanQrCode": { "message": "掃描 QR Code" }, + "scrollDown": { + "message": "向下捲動" + }, "search": { "message": "搜尋" }, + "searchAccounts": { + "message": "搜尋帳戶" + }, "searchResults": { "message": "搜尋結果" }, @@ -748,29 +1315,90 @@ "message": "助憶詞將可協助您用更簡單的方式備份帳戶資訊。" }, "secretBackupPhraseWarning": { - "message": "警告: 絕對不要洩漏您的助憶詞。任何人只要得知助憶詞代表他可以竊取您所有的以太幣和代幣。" + "message": "警告:絕對不要洩漏您的助憶詞。任何人只要得知助憶詞就能竊取您所有的以太幣和代幣。" + }, + "secretPhrase": { + "message": "在輸入您的助憶詞來還原您的金庫。" + }, + "secureWallet": { + "message": "Secure Wallet" }, "securityAndPrivacy": { "message": "安全&隱私" }, + "seedPhraseIntroSidebarBulletFour": { + "message": "寫下來並且存放在不同的秘密地點。" + }, + "seedPhraseIntroSidebarBulletOne": { + "message": "儲存在密碼管理器裡" + }, + "seedPhraseIntroSidebarBulletThree": { + "message": "存在安全保管箱裡。" + }, + "seedPhraseIntroSidebarBulletTwo": { + "message": "存在銀行保險箱。" + }, + "seedPhraseIntroSidebarCopyOne": { + "message": "您的助憶詞就是您的錢包與資金的「總鑰匙」。" + }, + "seedPhraseIntroSidebarCopyThree": { + "message": "如果有人向您詢問您的助憶詞,他們十之八九是在詐騙您。" + }, + "seedPhraseIntroSidebarCopyTwo": { + "message": "永遠永遠不要洩漏您的助憶詞,即使是 MetaMask 官方也一樣!" + }, + "seedPhraseIntroSidebarTitleOne": { + "message": "什麼是助憶詞?" + }, + "seedPhraseIntroSidebarTitleThree": { + "message": "我應該分享我的助憶詞嗎?" + }, + "seedPhraseIntroSidebarTitleTwo": { + "message": "我該如何存放我的助憶詞?" + }, + "seedPhraseIntroTitle": { + "message": "保護您的錢包" + }, + "seedPhraseIntroTitleCopy": { + "message": "在開始之前,觀賞這段短片來了解何謂助憶詞,以及如何保持您錢包的安全。" + }, "seedPhraseReq": { - "message": "助憶詞為 12 個詞語" + "message": "助憶詞為 12, 15, 18, 21 或 24 個單字" + }, + "selectAccounts": { + "message": "選擇一個或多個帳戶" + }, + "selectAll": { + "message": "選擇全部" }, "selectAnAccount": { "message": "選擇帳戶" }, + "selectAnAccountAlreadyConnected": { + "message": "這個帳戶已經連結到 MetaMask 了" + }, "selectEachPhrase": { "message": "請依照正確順序點選助憶詞" }, + "selectHdPath": { + "message": "選擇 HD Path" + }, "selectPathHelp": { "message": "若看不到您已經擁有的 Ledger 帳戶,請嘗試切換路徑為 \"Legacy (MEW / MyCrypto)\"" }, "selectType": { "message": "選擇類型" }, + "selectingAllWillAllow": { + "message": "選擇全部會讓這個網站能瀏覽您目前的全部帳戶。請再次確認您信任這個網站。" + }, "send": { "message": "發送" }, + "sendSpecifiedTokens": { + "message": "發送 $1", + "description": "Symbol of the specified token" + }, "sendTokens": { "message": "發送代幣" }, @@ -778,10 +1406,10 @@ "message": "設定" }, "showAdvancedGasInline": { - "message": "顯示進階 Gas 控制選項" + "message": "顯示進階 gas 控制選項" }, "showAdvancedGasInlineDescription": { - "message": "在交易時顯示可微調 Gas 價格以及 Gas 上限的功能" + "message": "選擇此項會在傳送或確認畫面顯示可微調 gas 價格以及 gas 上限的功能" }, "showFiatConversionInTestnets": { "message": "在測試網上顯示匯率" @@ -790,10 +1418,19 @@ "message": "選擇此來在測試網上顯示法定貨幣匯率" }, "showHexData": { - "message": "顯示16進位資料" + "message": "顯示十六進位資料" }, "showHexDataDescription": { - "message": "在交易時顯示16進位資料" + "message": "在交易時顯示十六進位資料" + }, + "showIncomingTransactions": { + "message": "顯示傳入的交易" + }, + "showIncomingTransactionsDescription": { + "message": "選擇此項來利用 Etherscan 在交易列表裡顯示傳入的交易" + }, + "showPermissions": { + "message": "顯示權限" }, "showPrivateKeys": { "message": "顯示私鑰" @@ -805,14 +1442,20 @@ "message": "簽署" }, "signNotice": { - "message": "簽署此訊息可能會產生危險地副作用。 \n只從您完全信任的網站上簽署。這種危險的方法,將在未來的版本中被移除。" + "message": "簽署此訊息可能會產生危險的副作用。\n請只從您完全信任的網站上簽署。\n這種危險的方法將在未來的版本中被移除。" }, "signatureRequest": { "message": "請求簽署" }, + "signatureRequest1": { + "message": "訊息" + }, "signed": { "message": "已簽署" }, + "skipAccountSecurity": { + "message": "Skip Account Security?" + }, "somethingWentWrong": { "message": "糟糕!出了點問題。" }, @@ -820,13 +1463,35 @@ "message": "加速" }, "speedUpCancellation": { - "message": "加速取消" + "message": "加速這個取消請求" }, "speedUpTransaction": { "message": "加速這筆交易" }, + "spendLimitAmount": { + "message": "花費額度上限" + }, + "spendLimitInsufficient": { + "message": "花費額度上限不足" + }, + "spendLimitInvalid": { + "message": "花費額度上限無效;必須是正值" + }, + "spendLimitPermission": { + "message": "花費額度上限的權限" + }, + "spendLimitRequestedBy": { + "message": "$1 請求的花費額度上限", + "description": "Origin of the site requesting the spend limit" + }, + "spendLimitTooLarge": { + "message": "花費額度上限太大" + }, "stateLogError": { - "message": "在取得狀態紀錄時發生錯誤." + "message": "在取得狀態紀錄時發生錯誤。" + }, + "stateLogFileName": { + "message": "MetaMask 狀態紀錄" }, "stateLogs": { "message": "狀態紀錄" @@ -834,53 +1499,103 @@ "stateLogsDescription": { "message": "狀態紀錄包含您的公開帳戶位址和已傳送的交易資訊" }, + "statusConnected": { + "message": "已連結" + }, + "statusNotConnected": { + "message": "未連結" + }, + "step1LedgerWallet": { + "message": "下載 Ledger 應用程式" + }, + "step1LedgerWalletMsg": { + "message": "下載、安裝、然後輸入密碼來解鎖 $1。", + "description": "$1 represents the `ledgerLiveApp` localization value" + }, + "step1TrezorWallet": { + "message": "插入 Trezor 錢包" + }, + "step1TrezorWalletMsg": { + "message": "直接將錢包連接到電腦上。關於更多使用您的硬體錢包的詳細資訊,$1", + "description": "$1 represents the `hardwareWalletSupportLinkConversion` localization key" + }, + "step2LedgerWallet": { + "message": "插入 Ledger 錢包" + }, "storePhrase": { "message": "您可以用密碼管理系統例如 1Password 等軟體儲存助憶詞。" }, + "submit": { + "message": "傳送" + }, "submitted": { - "message": "已送出" + "message": "已傳送" + }, + "support": { + "message": "支援" }, "supportCenter": { "message": "造訪我們的協助中心" }, + "swapSearchNameOrAddress": { + "message": "用名稱搜尋或貼上地址" + }, + "switchEthereumChainConfirmationDescription": { + "message": "這將在 MetaMask 中將目前選擇的網路切換到剛才新增的網路:" + }, + "switchEthereumChainConfirmationTitle": { + "message": "允許這個網站切換網路?" + }, + "switchNetwork": { + "message": "切換網路" + }, "switchNetworks": { "message": "切換網路" }, + "switchToThisAccount": { + "message": "切換到這個帳戶" + }, + "switchingNetworksCancelsPendingConfirmations": { + "message": "切換網路將會取消所有等待確認的請求" + }, "symbol": { "message": "符號" }, "symbolBetweenZeroTwelve": { - "message": "符號不得超過11個字符。" + "message": "符號不得超過 11 個字元。" }, "syncWithMobile": { - "message": "和移動裝置同步" + "message": "和行動裝置同步" }, "syncWithMobileBeCareful": { "message": "掃描代碼時確保沒有其他人在看你的螢幕" }, "syncWithMobileComplete": { - "message": "你的資料已成功同步。開始活用 MetaMask 移動 app!" + "message": "你的資料已成功同步。開始活用 MetaMask 行動應用程式!" }, "syncWithMobileDesc": { - "message": "你可以用移動裝置同步帳號與資訊。開啟 MetaMask 移動 app,至「設定」並點擊「自瀏覽器擴充功能同步」" + "message": "你可以用行動裝置同步帳戶與資訊。開啟 MetaMask 行動應用程式,至「設定」並點擊「自瀏覽器擴充功能同步」" }, "syncWithMobileDescNewUsers": { - "message": "如你是第一次開啟 MetaMask 移動 app,只要跟著手機上的指示操作即可。" + "message": "如果您是第一次開啟 MetaMask 行動應用程式,只要跟著手機上的指示操作即可。" }, "syncWithMobileScanThisCode": { - "message": "用你的 MetaMask 移動 app 掃描此代碼" + "message": "用您的 MetaMask 行動應用程式 掃描此條碼" }, "syncWithMobileTitle": { - "message": "和移動裝置同步" + "message": "和行動裝置同步" }, "terms": { "message": "使用條款" }, + "termsOfService": { + "message": "服務條款" + }, "testFaucet": { - "message": "測試水管" + "message": "測試水龍頭" }, "thisWillCreate": { - "message": "這將創建新的錢包與助記詞" + "message": "這將創建新的錢包與助憶詞" }, "tips": { "message": "提示" @@ -888,6 +1603,10 @@ "to": { "message": "目的帳戶" }, + "toAddress": { + "message": "傳送到:$1", + "description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress" + }, "token": { "message": "代碼" }, @@ -895,11 +1614,17 @@ "message": "已加入過此代幣。" }, "tokenContractAddress": { - "message": "代幣合同位址" + "message": "代幣合約位址" + }, + "tokenDecimalFetchFailed": { + "message": "需要填入代幣的小數位數。" }, "tokenSymbol": { "message": "代幣代號" }, + "tooltipApproveButton": { + "message": "我了解了" + }, "total": { "message": "總量" }, @@ -907,54 +1632,61 @@ "message": "交易" }, "transactionCancelAttempted": { - "message": "交易取消請求 手續費 $1 時間 $2" + "message": "交易取消請求,手續費 $1 @ $2" }, "transactionCancelSuccess": { - "message": "交易取消成功 $2" + "message": "交易成功取消 @ $2" }, "transactionConfirmed": { - "message": "交易確認 時間 $2." + "message": "交易確認 @ $2。" }, "transactionCreated": { - "message": "交易產生 數量 $1 時間 $2" + "message": "交易建立,數量 $1 @ $2。" }, "transactionDropped": { - "message": "交易廢除 時間 $2" + "message": "交易被捨棄 @ $2。" }, "transactionError": { - "message": "交易失敗。合約代碼拋出錯誤資訊" + "message": "交易失敗。合約程式碼拋出錯誤。" }, "transactionErrorNoContract": { - "message": "合約位址錯誤" + "message": "嘗試在非合約位址呼叫合約函式。" }, "transactionErrored": { - "message": "交易錯誤" + "message": "交易遇到錯誤。" }, "transactionFee": { - "message": "交易費" + "message": "交易手續費" }, "transactionResubmitted": { - "message": "交易重新送出 手續費提高至 $1 時間 $2" + "message": "交易重新送出,手續費提高至 $1 @ $2" }, "transactionSubmitted": { - "message": "交易送出 手續費 $1 時間 $2" + "message": "交易送出,手續費 $1 @ $2。" }, "transactionUpdated": { - "message": "交易狀態更新 時間 $2" + "message": "交易狀態更新於 $2。" }, "transfer": { "message": "交易" }, "transferBetweenAccounts": { - "message": "在我的帳號間轉帳" + "message": "在我的帳戶間轉帳" }, "transferFrom": { "message": "交易來源" }, + "troubleConnectingToWallet": { + "message": "我們在連線到您的 $1 的時候遇到問題,試著檢查 $2 然後再試一次。", + "description": "$1 is the wallet device name; $2 is a link to wallet connection guide" + }, "troubleTokenBalances": { - "message": "無法取得代幣餘額。您k可以到這裡查看 ", + "message": "無法取得代幣餘額。您可以到這裡查看 ", "description": "Followed by a link (here) to view token balances" }, + "trustSiteApprovePermission": { + "message": "您信任這個網站嗎?當您授予這個權限,$1 就能提領您的 $2 並且代替您自動發送交易。" + }, "tryAgain": { "message": "再試一次" }, @@ -982,17 +1714,33 @@ "unknownQrCode": { "message": "錯誤:無法辨識 QR code" }, + "unlimited": { + "message": "無限" + }, "unlock": { "message": "解鎖" }, "unlockMessage": { "message": "去中心化網路世界等待著您" }, + "unrecognizedChain": { + "message": "無法辨識這個自訂網路。我們建議您先$1再繼續。", + "description": "$1 is a clickable link with text defined by the 'unrecognizedChanLinkText' key. The link will open to instructions for users to validate custom network details." + }, "updatedWithDate": { "message": "更新時間 $1" }, "urlErrorMsg": { - "message": "URIs 需要加入適當的 HTTP/HTTPS 前綴字" + "message": "URL 需要以適當的 HTTP/HTTPS 作為開頭" + }, + "urlExistsErrorMsg": { + "message": "URL 已經在既有的網路列表裡了" + }, + "usePhishingDetection": { + "message": "使用網路釣魚偵測" + }, + "usePhishingDetectionDescription": { + "message": "遇到針對以太坊使用者的釣魚網域名稱的時候顯示警告" }, "usedByClients": { "message": "可用於各種不同的客戶端" @@ -1000,24 +1748,68 @@ "userName": { "message": "使用者名稱" }, + "verifyThisTokenDecimalOn": { + "message": "代幣的小數位數可以在 $1 找到", + "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" + }, + "verifyThisTokenOn": { + "message": "在 $1 驗證這個代幣的資訊", + "description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\"" + }, "viewAccount": { "message": "查看帳戶" }, + "viewAllDetails": { + "message": "查看所有詳情" + }, "viewContact": { "message": "觀看聯絡資訊" }, + "viewMore": { + "message": "檢視更多" + }, + "viewOnCustomBlockExplorer": { + "message": "在 $1 瀏覽", + "description": "$1 is the action type. e.g (Account, Transaction, Swap) and $2 is the Custom Block Exporer URL" + }, + "viewOnEtherscan": { + "message": "在 Etherscan 上瀏覽", + "description": "$1 is the action type. e.g (Account, Transaction, Swap)" + }, + "viewinExplorer": { + "message": "在 Explorer 上瀏覽", + "description": "$1 is the action type. e.g (Account, Transaction, Swap)" + }, "visitWebSite": { "message": "造訪我們的網站" }, + "walletConnectionGuide": { + "message": "我們的硬體錢包連線指南" + }, + "web3ShimUsageNotification": { + "message": "我們注意到目前的網站試著要使用已經移除的 window.web3 API。如果網站看起來壞了,請$1了解更多資訊。", + "description": "$1 is a clickable link." + }, "welcome": { "message": "歡迎來到 MetaMask" }, "welcomeBack": { "message": "歡迎回來!" }, + "whatsThis": { + "message": "這是什麼?" + }, "writePhrase": { "message": "將助憶詞寫在紙上,並保存在安全的場所。若想要更安全,將助憶詞分別寫在不同紙張上並存放在不同的地方。" }, + "xOfY": { + "message": "$1 之 $2", + "description": "$1 and $2 are intended to be two numbers, where $2 is a total, and $1 is a count towards that total" + }, + "xOfYPending": { + "message": "$1 之 $2 等待中", + "description": "$1 and $2 are intended to be two numbers, where $2 is a total number of pending confirmations, and $1 is a count towards that total" + }, "yesLetsTry": { "message": "了解,試試看" }, diff --git a/app/build-types/beta/images/beta-mascot.json b/app/build-types/beta/images/beta-mascot.json deleted file mode 100644 index 4db57f543..000000000 --- a/app/build-types/beta/images/beta-mascot.json +++ /dev/null @@ -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] - ] -} diff --git a/app/build-types/beta/images/icon-128.png b/app/build-types/beta/images/icon-128.png index 97762ff99..95b2ec2c7 100644 Binary files a/app/build-types/beta/images/icon-128.png and b/app/build-types/beta/images/icon-128.png differ diff --git a/app/build-types/beta/images/icon-16.png b/app/build-types/beta/images/icon-16.png index 216b4ad06..e1c32722f 100644 Binary files a/app/build-types/beta/images/icon-16.png and b/app/build-types/beta/images/icon-16.png differ diff --git a/app/build-types/beta/images/icon-19.png b/app/build-types/beta/images/icon-19.png index f7da09c5e..5073efb79 100644 Binary files a/app/build-types/beta/images/icon-19.png and b/app/build-types/beta/images/icon-19.png differ diff --git a/app/build-types/beta/images/icon-32.png b/app/build-types/beta/images/icon-32.png index fb2a55a57..f0ac3fb28 100644 Binary files a/app/build-types/beta/images/icon-32.png and b/app/build-types/beta/images/icon-32.png differ diff --git a/app/build-types/beta/images/icon-34.png b/app/build-types/beta/images/icon-34.png new file mode 100644 index 000000000..bd70156f3 Binary files /dev/null and b/app/build-types/beta/images/icon-34.png differ diff --git a/app/build-types/beta/images/icon-38.png b/app/build-types/beta/images/icon-38.png index e9449d4d0..ae868f268 100644 Binary files a/app/build-types/beta/images/icon-38.png and b/app/build-types/beta/images/icon-38.png differ diff --git a/app/build-types/beta/images/icon-48.png b/app/build-types/beta/images/icon-48.png index 0fdfeb25c..306a7bce0 100644 Binary files a/app/build-types/beta/images/icon-48.png and b/app/build-types/beta/images/icon-48.png differ diff --git a/app/build-types/beta/images/icon-512.png b/app/build-types/beta/images/icon-512.png index 09690ab1b..0b9ccdc08 100644 Binary files a/app/build-types/beta/images/icon-512.png and b/app/build-types/beta/images/icon-512.png differ diff --git a/app/build-types/beta/images/icon-64.png b/app/build-types/beta/images/icon-64.png index b60b7d5d5..2bcda034c 100644 Binary files a/app/build-types/beta/images/icon-64.png and b/app/build-types/beta/images/icon-64.png differ diff --git a/app/build-types/beta/images/info-logo.png b/app/build-types/beta/images/info-logo.png index 97762ff99..a855a15b1 100644 Binary files a/app/build-types/beta/images/info-logo.png and b/app/build-types/beta/images/info-logo.png differ diff --git a/app/build-types/beta/images/logo/metamask-fox.svg b/app/build-types/beta/images/logo/metamask-fox.svg index 53698aefd..b6516605d 100644 --- a/app/build-types/beta/images/logo/metamask-fox.svg +++ b/app/build-types/beta/images/logo/metamask-fox.svg @@ -1,130 +1,36 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/scripts/background.js b/app/scripts/background.js index f3ed3dc3a..ec2aff120 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -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 "" 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: '', + 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 { diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index dcaa6c59b..c2bcbad5e 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -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; +}; + +/** + * 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(); - setupExtensionStreams(); - 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(); diff --git a/app/scripts/controllers/app-state.js b/app/scripts/controllers/app-state.js index 0cf5fc24b..9b62cf3e2 100644 --- a/app/scripts/controllers/app-state.js +++ b/app/scripts/controllers/app-state.js @@ -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 * diff --git a/app/scripts/controllers/metametrics.js b/app/scripts/controllers/metametrics.js index 212b9a908..1c87c6181 100644 --- a/app/scripts/controllers/metametrics.js +++ b/app/scripts/controllers/metametrics.js @@ -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} [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); + } } diff --git a/app/scripts/controllers/metametrics.test.js b/app/scripts/controllers/metametrics.test.js index 1925da7a9..297a21912 100644 --- a/app/scripts/controllers/metametrics.test.js +++ b/app/scripts/controllers/metametrics.test.js @@ -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(); }); }); diff --git a/app/scripts/controllers/permissions/specifications.test.js b/app/scripts/controllers/permissions/specifications.test.js index 4d313b12c..f199e9122 100644 --- a/app/scripts/controllers/permissions/specifications.test.js +++ b/app/scripts/controllers/permissions/specifications.test.js @@ -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', () => { diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index e2bb9045c..569d09750 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -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 * diff --git a/app/scripts/controllers/preferences.test.js b/app/scripts/controllers/preferences.test.js index ee19e216f..488e03010 100644 --- a/app/scripts/controllers/preferences.test.js +++ b/app/scripts/controllers/preferences.test.js @@ -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, ); }); diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js index 024ed6c24..7e57a8ee9 100644 --- a/app/scripts/lib/account-tracker.js +++ b/app/scripts/lib/account-tracker.js @@ -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(); + const networkId = this.getNetworkIdentifier(); + const rpcUrl = 'http://127.0.0.1:8545'; - switch (chainId) { - case CHAIN_IDS.MAINNET: - await this._updateAccountsViaBalanceChecker( - addresses, - SINGLE_CALL_BALANCES_ADDRESS, - ); - break; + 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.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.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.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.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.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.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.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; + case CHAIN_IDS.ARBITRUM: + await this._updateAccountsViaBalanceChecker( + addresses, + SINGLE_CALL_BALANCES_ADDRESS_ARBITRUM, + ); + break; - default: - await Promise.all(addresses.map(this._updateAccount.bind(this))); + default: + await Promise.all(addresses.map(this._updateAccount.bind(this))); + } } } diff --git a/app/scripts/lib/segment/analytics.js b/app/scripts/lib/segment/analytics.js index c93275f94..8966c8eeb 100644 --- a/app/scripts/lib/segment/analytics.js +++ b/app/scripts/lib/segment/analytics.js @@ -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 ( diff --git a/app/scripts/lib/stream-utils.js b/app/scripts/lib/stream-utils.js index 6fa4d7a6c..cac30c066 100644 --- a/app/scripts/lib/stream-utils.js +++ b/app/scripts/lib/stream-utils.js @@ -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); diff --git a/app/scripts/lib/util.js b/app/scripts/lib/util.js index 6539e1f23..58c0b3c89 100644 --- a/app/scripts/lib/util.js +++ b/app/scripts/lib/util.js @@ -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); +}; diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 05c698635..b56b17f2e 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -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), + + removeAndIgnoreNft: nftController.removeAndIgnoreNft.bind(nftController), + + removeNft: nftController.removeNft.bind(nftController), + + checkAndUpdateAllNftsOwnershipStatus: + nftController.checkAndUpdateAllNftsOwnershipStatus.bind(nftController), + + checkAndUpdateSingleNftOwnershipStatus: + nftController.checkAndUpdateSingleNftOwnershipStatus.bind( + nftController, ), - removeAndIgnoreCollectible: - collectiblesController.removeAndIgnoreCollectible.bind( - collectiblesController, - ), - - removeCollectible: collectiblesController.removeCollectible.bind( - collectiblesController, - ), - - checkAndUpdateAllCollectiblesOwnershipStatus: - collectiblesController.checkAndUpdateAllCollectiblesOwnershipStatus.bind( - collectiblesController, - ), - - checkAndUpdateSingleCollectibleOwnershipStatus: - collectiblesController.checkAndUpdateSingleCollectibleOwnershipStatus.bind( - collectiblesController, - ), - - 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; } diff --git a/app/scripts/migrations/076.js b/app/scripts/migrations/076.js new file mode 100644 index 000000000..98ca9e29f --- /dev/null +++ b/app/scripts/migrations/076.js @@ -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; +} diff --git a/app/scripts/migrations/076.test.js b/app/scripts/migrations/076.test.js new file mode 100644 index 000000000..c25c04e6c --- /dev/null +++ b/app/scripts/migrations/076.test.js @@ -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' }, + }, + }); + }); +}); diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index ab7c6b3d5..f3c65061b 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -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; diff --git a/app/scripts/sentry-install.js b/app/scripts/sentry-install.js index 1f0b87bd5..fc654371b 100644 --- a/app/scripts/sentry-install.js +++ b/app/scripts/sentry-install.js @@ -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?.() || {}, }); diff --git a/app/scripts/ui.js b/app/scripts/ui.js index d9569f043..13516becd 100644 --- a/app/scripts/ui.js +++ b/app/scripts/ui.js @@ -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; diff --git a/development/build/index.js b/development/build/index.js index 628cbbbac..a1a6874e6 100755 --- a/development/build/index.js +++ b/development/build/index.js @@ -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); diff --git a/development/build/styles.js b/development/build/styles.js index 47c5e5c66..8b5ccf29e 100644 --- a/development/build/styles.js +++ b/development/build/styles.js @@ -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); } } } diff --git a/development/generate-icon-names.js b/development/generate-icon-names.js index 60637ae86..a89887de7 100644 --- a/development/generate-icon-names.js +++ b/development/generate-icon-names.js @@ -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'); diff --git a/development/metamaskbot-build-announce.js b/development/metamaskbot-build-announce.js index 8ce98ce67..6e929969c 100755 --- a/development/metamaskbot-build-announce.js +++ b/development/metamaskbot-build-announce.js @@ -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) => `
  • ${key}: ${bundles[key].join(', ')}
  • `) .join('')}`; + 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 = `Report`; @@ -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 = ``; + + 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 = `
    ${sizeDiffExposedContent}${sizeDiffHiddenContent}
    \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) { diff --git a/development/ts-migration-dashboard/files-to-convert.json b/development/ts-migration-dashboard/files-to-convert.json index 6cce4cb55..bc94b3e44 100644 --- a/development/ts-migration-dashboard/files-to-convert.json +++ b/development/ts-migration-dashboard/files-to-convert.json @@ -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", diff --git a/docs/QA_Guide.md b/docs/QA_Guide.md index dc5b94c40..a615d5f60 100644 --- a/docs/QA_Guide.md +++ b/docs/QA_Guide.md @@ -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. diff --git a/docs/generating-fixture-data.md b/docs/generating-fixture-data.md index 77b90f3c2..274945765 100644 --- a/docs/generating-fixture-data.md +++ b/docs/generating-fixture-data.md @@ -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()) ``` diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index d134a6a9f..ac2f28d58 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -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 diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 5853f5d74..f2fa87fe8 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -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 diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index d134a6a9f..ac2f28d58 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -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 diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json index b50930cc5..de3161d63 100644 --- a/lavamoat/build-system/policy.json +++ b/lavamoat/build-system/policy.json @@ -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 + "console": true, + "process.env.LANG": true, + "process.env.NODE_ENV": 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 - }, - "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, diff --git a/package.json b/package.json index a52646d89..90f546837 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/patches/luxon+1.26.0.patch b/patches/luxon+3.1.0.patch similarity index 61% rename from patches/luxon+1.26.0.patch rename to patches/luxon+3.1.0.patch index 6c4482216..0cb7a6e65 100644 --- a/patches/luxon+1.26.0.patch +++ b/patches/luxon+3.1.0.patch @@ -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 \ No newline at end of file + * Defaults to the system's locale if no locale has been specified diff --git a/shared/constants/app.ts b/shared/constants/app.ts index 496d48434..8ebf53cba 100644 --- a/shared/constants/app.ts +++ b/shared/constants/app.ts @@ -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). diff --git a/shared/constants/network.ts b/shared/constants/network.ts index 89dbe4ec2..a94617b86 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -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'; diff --git a/shared/constants/permissions.ts b/shared/constants/permissions.ts index 49c281f13..dbd39b51e 100644 --- a/shared/constants/permissions.ts +++ b/shared/constants/permissions.ts @@ -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. diff --git a/shared/constants/swaps.js b/shared/constants/swaps.js index 22629bc1a..37f9e30d2 100644 --- a/shared/constants/swaps.js +++ b/shared/constants/swaps.js @@ -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'; diff --git a/shared/constants/tokens.js b/shared/constants/tokens.js index c846fff18..ef9e7f76a 100644 --- a/shared/constants/tokens.js +++ b/shared/constants/tokens.js @@ -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. diff --git a/shared/constants/transaction.js b/shared/constants/transaction.js index 9564f066c..37c2e45e3 100644 --- a/shared/constants/transaction.js +++ b/shared/constants/transaction.js @@ -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', }; diff --git a/shared/lib/error-utils.js b/shared/lib/error-utils.js index c0baad2c1..0dc11e91c 100644 --- a/shared/lib/error-utils.js +++ b/shared/lib/error-utils.js @@ -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 `
    -
    -

    - ${t('troubleStarting')} -

    - +
    + + + +
    +
    +
    +

    + ${errorKey === 'troubleStarting' ? t('troubleStarting') : ''} + ${errorKey === 'somethingIsWrong' ? t('somethingIsWrong') : ''} +

    + + ${t('restartMetamask')} + +
    +

    + ${t('stillGettingMessage')} + + ${t('sendBugReport')} + +

    -

    - ${t('stillGettingMessage')} - - ${t('sendBugReport')} - -

    `; } diff --git a/shared/lib/error-utils.test.js b/shared/lib/error-utils.test.js index e1a121d7f..de846aa8b 100644 --- a/shared/lib/error-utils.test.js +++ b/shared/lib/error-utils.test.js @@ -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; diff --git a/shared/lib/swaps-utils.js b/shared/lib/swaps-utils.js index 6981ef255..c1393abe3 100644 --- a/shared/lib/swaps-utils.js +++ b/shared/lib/swaps-utils.js @@ -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'); } diff --git a/shared/modules/browser-runtime.utils.js b/shared/modules/browser-runtime.utils.js new file mode 100644 index 000000000..8f7b2afe7 --- /dev/null +++ b/shared/modules/browser-runtime.utils.js @@ -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; +} diff --git a/shared/modules/browser-runtime.utils.test.js b/shared/modules/browser-runtime.utils.test.js new file mode 100644 index 000000000..b116f2d7c --- /dev/null +++ b/shared/modules/browser-runtime.utils.test.js @@ -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(); + }); + }); +}); diff --git a/shared/modules/transaction.utils.js b/shared/modules/transaction.utils.js index 1cf471ed7..d7fd45801 100644 --- a/shared/modules/transaction.utils.js +++ b/shared/modules/transaction.utils.js @@ -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, }; } diff --git a/shared/notifications/index.js b/shared/notifications/index.js index 9a3cb0351..099350364 100644 --- a/shared/notifications/index.js +++ b/shared/notifications/index.js @@ -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), + ) + : '', + }, }; }; diff --git a/test/data/mock-state.json b/test/data/mock-state.json index a376803df..a83d6bb87 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -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" } } diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index 0eca41d41..7717a8a9a 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -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; diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index b3f74418a..d950f0c05 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -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++) { diff --git a/test/e2e/metamask-ui.spec.js b/test/e2e/metamask-ui.spec.js index 2a2f16dfb..347609a9f 100644 --- a/test/e2e/metamask-ui.spec.js +++ b/test/e2e/metamask-ui.spec.js @@ -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( diff --git a/test/e2e/restore/MetaMaskUserData.json b/test/e2e/restore/MetaMaskUserData.json index abea11e14..cb9dffefb 100644 --- a/test/e2e/restore/MetaMaskUserData.json +++ b/test/e2e/restore/MetaMaskUserData.json @@ -42,7 +42,7 @@ }, "theme": "light", "useBlockie": false, - "useCollectibleDetection": false, + "useNftDetection": false, "useNonceField": false, "usePhishDetect": true, "useTokenDetection": false diff --git a/test/e2e/run-all.js b/test/e2e/run-all.js index 2623f9ba9..27b754750 100644 --- a/test/e2e/run-all.js +++ b/test/e2e/run-all.js @@ -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]); } } diff --git a/test/e2e/snaps/enums.js b/test/e2e/snaps/enums.js index d18f41755..a940e21b9 100644 --- a/test/e2e/snaps/enums.js +++ b/test/e2e/snaps/enums.js @@ -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/', }; diff --git a/test/e2e/snaps/test-snap-bip-32.spec.js b/test/e2e/snaps/test-snap-bip-32.spec.js index b179461e7..f82a5f96c 100644 --- a/test/e2e/snaps/test-snap-bip-32.spec.js +++ b/test/e2e/snaps/test-snap-bip-32.spec.js @@ -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'); + const ed25519Result = await driver.findElement( + '#bip32MessageResult-ed25519', + ); 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', - ); - assert.equal( - await compressedPublicKeyResult.getText(), - 'Public key: "033e98d696ae15caef75fa8dd204a7c5c08d1272b2218ba3c20feeb4c691eec366"', + '"0xf3215b4d6c59aac7e01b4ceef530d1e2abf4857926b85a81aaae3894505699243768a887b7da4a8c2e0f25196196ba290b6531050db8dc15c252bdd508532a0a"', ); }, ); diff --git a/test/e2e/snaps/test-snap-bip-44.spec.js b/test/e2e/snaps/test-snap-bip-44.spec.js index ce617a350..32a48130a 100644 --- a/test/e2e/snaps/test-snap-bip-44.spec.js +++ b/test/e2e/snaps/test-snap-bip-44.spec.js @@ -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,23 +30,37 @@ 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); - - // connect the snap - await driver.clickElement('#connectBip44'); - - // 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', @@ -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"', ); }, ); diff --git a/test/e2e/snaps/test-snap-confirm.spec.js b/test/e2e/snaps/test-snap-confirm.spec.js index 9d85977df..00112ebdc 100644 --- a/test/e2e/snaps/test-snap-confirm.spec.js +++ b/test/e2e/snaps/test-snap-confirm.spec.js @@ -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,12 +30,31 @@ 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'); + + // 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); - let windowHandles = await driver.getAllWindowHandles(); + windowHandles = await driver.getAllWindowHandles(); await driver.switchToWindowWithTitle( 'MetaMask Notification', windowHandles, @@ -46,14 +64,14 @@ describe('Test Snap Confirm', function () { 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'); diff --git a/test/e2e/snaps/test-snap-error.spec.js b/test/e2e/snaps/test-snap-error.spec.js index 337b932df..3ac9a47b4 100644 --- a/test/e2e/snaps/test-snap-error.spec.js +++ b/test/e2e/snaps/test-snap-error.spec.js @@ -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'); - await driver.clickElement('#connectError'); + // 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 - 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', + }); }, ); }); diff --git a/test/e2e/snaps/test-snap-get-snaps.spec.js b/test/e2e/snaps/test-snap-installed.spec.js similarity index 58% rename from test/e2e/snaps/test-snap-get-snaps.spec.js rename to test/e2e/snaps/test-snap-installed.spec.js index 8de5cadbb..ccf0692c7 100644 --- a/test/e2e/snaps/test-snap-get-snaps.spec.js +++ b/test/e2e/snaps/test-snap-installed.spec.js @@ -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,13 +29,32 @@ 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'); + + // 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(2, 5000, 10000); - let windowHandles = await driver.getAllWindowHandles(); + await driver.waitUntilXWindowHandles(3, 5000, 10000); + windowHandles = await driver.getAllWindowHandles(); await driver.switchToWindowWithTitle( 'MetaMask Notification', windowHandles, @@ -47,18 +65,33 @@ describe('Test Snap Confirm', 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); - 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); + const result = await driver.findElement('#installedSnapsResult'); + await driver.scrollToElement(result); await driver.delay(1000); - await driver.clickElement('#getInstalledSnapsButton'); - 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', diff --git a/test/e2e/snaps/test-snap-managestate.spec.js b/test/e2e/snaps/test-snap-managestate.spec.js index 58fc36862..86cb4e47a 100644 --- a/test/e2e/snaps/test-snap-managestate.spec.js +++ b/test/e2e/snaps/test-snap-managestate.spec.js @@ -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": [] }', ); }, ); diff --git a/test/e2e/snaps/test-snap-notification.spec.js b/test/e2e/snaps/test-snap-notification.spec.js index ff2002881..a52f77005 100644 --- a/test/e2e/snaps/test-snap-notification.spec.js +++ b/test/e2e/snaps/test-snap-notification.spec.js @@ -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( diff --git a/test/e2e/snaps/test-snap-update.spec.js b/test/e2e/snaps/test-snap-update.spec.js index 862e95045..ebbf74d4f 100644 --- a/test/e2e/snaps/test-snap-update.spec.js +++ b/test/e2e/snaps/test-snap-update.spec.js @@ -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'); + + await driver.delay(2000); + + // 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 - let windowHandles = await driver.getAllWindowHandles(); - const extensionPage = windowHandles[0]; + 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"'); }, ); }); diff --git a/test/e2e/tests/custom-token-add-approve.spec.js b/test/e2e/tests/custom-token-add-approve.spec.js index 67e045325..2815f49a4 100644 --- a/test/e2e/tests/custom-token-add-approve.spec.js +++ b/test/e2e/tests/custom-token-add-approve.spec.js @@ -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"]', ); diff --git a/test/e2e/tests/send-eth.spec.js b/test/e2e/tests/send-eth.spec.js index 0833a1f85..b9500e22d 100644 --- a/test/e2e/tests/send-eth.spec.js +++ b/test/e2e/tests/send-eth.spec.js @@ -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', diff --git a/test/e2e/tests/send-hex-address.spec.js b/test/e2e/tests/send-hex-address.spec.js index ade462eb3..315b5565d 100644 --- a/test/e2e/tests/send-hex-address.spec.js +++ b/test/e2e/tests/send-hex-address.spec.js @@ -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( diff --git a/test/e2e/tests/signature-request.spec.js b/test/e2e/tests/signature-request.spec.js index 38141527b..9fa2a08d4 100644 --- a/test/e2e/tests/signature-request.spec.js +++ b/test/e2e/tests/signature-request.spec.js @@ -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(); diff --git a/test/e2e/webdriver/driver.js b/test/e2e/webdriver/driver.js index 378a0e873..28a9ac115 100644 --- a/test/e2e/webdriver/driver.js +++ b/test/e2e/webdriver/driver.js @@ -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 = [ diff --git a/test/helpers/setup-helper.js b/test/helpers/setup-helper.js index cb627c805..4aac6a02d 100644 --- a/test/helpers/setup-helper.js +++ b/test/helpers/setup-helper.js @@ -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 = {}; diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js index 334d3fe85..5d4055628 100644 --- a/test/jest/mock-store.js +++ b/test/jest/mock-store.js @@ -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, diff --git a/test/lib/render-helpers.js b/test/lib/render-helpers.js index 0291df0f8..27ac3f80e 100644 --- a/test/lib/render-helpers.js +++ b/test/lib/render-helpers.js @@ -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 ( + setValue(e.target.value)} + {...props} + /> + ); + }; + return { user: userEvent.setup(), ...render() }; +} diff --git a/ui/components/app/app-components.scss b/ui/components/app/app-components.scss index ed008d5f0..0b457c66c 100644 --- a/ui/components/app/app-components.scss +++ b/ui/components/app/app-components.scss @@ -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'; diff --git a/ui/components/app/app-header/app-header.component.js b/ui/components/app/app-header/app-header.component.js index 7f1bbc253..f5e7d57c5 100644 --- a/ui/components/app/app-header/app-header.component.js +++ b/ui/components/app/app-header/app-header.component.js @@ -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 ( -
    -
    - { - if (onClick) { - await onClick(); - } - history.push(DEFAULT_ROUTE); - }} - /> -
    - {!hideNetworkIndicator && ( -
    - this.handleNetworkIndicatorClick(event)} - disabled={disabled || disableNetworkIndicator} - /> -
    - )} - {this.renderAccountMenu()} + <> + { + ///: BEGIN:ONLY_INCLUDE_IN(beta) + showBetaHeader ? : null + ///: END:ONLY_INCLUDE_IN(beta) + } + +
    +
    + { + if (onClick) { + await onClick(); + } + history.push(DEFAULT_ROUTE); + }} + /> +
    + {!hideNetworkIndicator && ( +
    + this.handleNetworkIndicatorClick(event)} + disabled={disabled || disableNetworkIndicator} + /> +
    + )} + {this.renderAccountMenu()} +
    -
    + ); } } diff --git a/ui/components/app/app-header/app-header.container.js b/ui/components/app/app-header/app-header.container.js index e30049bc3..04b9c47c4 100644 --- a/ui/components/app/app-header/app-header.container.js +++ b/ui/components/app/app-header/app-header.container.js @@ -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 }; }; diff --git a/ui/components/app/approve-content-card/approve-content-card.js b/ui/components/app/approve-content-card/approve-content-card.js index 34b7e3ee9..2e821eaa0 100644 --- a/ui/components/app/approve-content-card/approve-content-card.js +++ b/ui/components/app/approve-content-card/approve-content-card.js @@ -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, }; diff --git a/ui/components/app/approve-content-card/approve-content-card.stories.js b/ui/components/app/approve-content-card/approve-content-card.stories.js new file mode 100644 index 000000000..113b35384 --- /dev/null +++ b/ui/components/app/approve-content-card/approve-content-card.stories.js @@ -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: , + 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 ; +}; + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/app/beta-header/__snapshots__/beta-header.test.js.snap b/ui/components/app/beta-header/__snapshots__/beta-header.test.js.snap new file mode 100644 index 000000000..24c327ba4 --- /dev/null +++ b/ui/components/app/beta-header/__snapshots__/beta-header.test.js.snap @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Beta Header should match snapshot 1`] = ` +
    +
    +
    + + + This is a BETA version. Please report bugs + + here + + + + +
    + +
    +
    +`; diff --git a/ui/components/app/beta-header/beta-header.stories.js b/ui/components/app/beta-header/beta-header.stories.js new file mode 100644 index 000000000..4a4480090 --- /dev/null +++ b/ui/components/app/beta-header/beta-header.stories.js @@ -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) => {story()}], + id: __filename, +}; + +export const DefaultStory = () => ( + <> + + +); + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/app/beta-header/beta-header.test.js b/ui/components/app/beta-header/beta-header.test.js new file mode 100644 index 000000000..2036b23e0 --- /dev/null +++ b/ui/components/app/beta-header/beta-header.test.js @@ -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(, store); + expect(container).toMatchSnapshot(); + }); + + describe('Beta Header', () => { + it('gets hidden when close button is clicked', () => { + const { queryByTestId } = renderWithProvider(, store); + + const closeButton = queryByTestId('beta-header-close'); + fireEvent.click(closeButton); + + expect(mockHideBetaHeader).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/ui/components/app/beta-header/index.js b/ui/components/app/beta-header/index.js new file mode 100644 index 000000000..dd1884339 --- /dev/null +++ b/ui/components/app/beta-header/index.js @@ -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 ( + + + {t('betaHeaderText', [ + + {t('here')} + , + ])} + + + + ); +}; + +export default BetaHeader; diff --git a/ui/components/app/beta-header/index.scss b/ui/components/app/beta-header/index.scss new file mode 100644 index 000000000..6d143af97 --- /dev/null +++ b/ui/components/app/beta-header/index.scss @@ -0,0 +1,16 @@ +.beta-header { + &__message { + text-align: center; + flex-grow: 1; + } + + &__button { + background: transparent; + padding: 0 6px; + margin: 0; + + i { + color: var(--color-warning-inverse); + } + } +} diff --git a/ui/components/app/collectible-details/collectible-details.js b/ui/components/app/collectible-details/collectible-details.js index d2e6a9100..213eeb353 100644 --- a/ui/components/app/collectible-details/collectible-details.js +++ b/ui/components/app/collectible-details/collectible-details.js @@ -30,8 +30,8 @@ import Copy from '../../ui/icon/copy-icon.component'; import { getCollectibleContracts } from '../../../ducks/metamask/metamask'; import { DEFAULT_ROUTE, SEND_ROUTE } from '../../../helpers/constants/routes'; import { - checkAndUpdateSingleCollectibleOwnershipStatus, - removeAndIgnoreCollectible, + checkAndUpdateSingleNftOwnershipStatus, + removeAndIgnoreNft, } from '../../../store/actions'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import { getEnvironmentType } from '../../../../app/scripts/lib/util'; @@ -79,14 +79,14 @@ export default function CollectibleDetails({ collectible }) { ); const onRemove = () => { - dispatch(removeAndIgnoreCollectible(address, tokenId)); + dispatch(removeAndIgnoreNft(address, tokenId)); history.push(DEFAULT_ROUTE); }; const prevCollectible = usePrevious(collectible); useEffect(() => { if (!isEqual(prevCollectible, collectible)) { - checkAndUpdateSingleCollectibleOwnershipStatus(collectible); + checkAndUpdateSingleNftOwnershipStatus(collectible); } }, [collectible, prevCollectible]); @@ -111,7 +111,7 @@ export default function CollectibleDetails({ collectible }) { const onSend = async () => { await dispatch( startNewDraftTransaction({ - type: ASSET_TYPES.COLLECTIBLE, + type: ASSET_TYPES.NFT, details: collectible, }), ); diff --git a/ui/components/app/collectibles-tab/collectibles-tab.js b/ui/components/app/collectibles-tab/collectibles-tab.js index 6b7c74843..3f228f0ab 100644 --- a/ui/components/app/collectibles-tab/collectibles-tab.js +++ b/ui/components/app/collectibles-tab/collectibles-tab.js @@ -18,17 +18,17 @@ import { } from '../../../helpers/constants/design-system'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { getCollectiblesDetectionNoticeDismissed } from '../../../ducks/metamask/metamask'; -import { getIsMainnet, getUseCollectibleDetection } from '../../../selectors'; +import { getIsMainnet, getUseNftDetection } from '../../../selectors'; import { EXPERIMENTAL_ROUTE } from '../../../helpers/constants/routes'; import { - checkAndUpdateAllCollectiblesOwnershipStatus, - detectCollectibles, + checkAndUpdateAllNftsOwnershipStatus, + detectNfts, } from '../../../store/actions'; import { useCollectiblesCollections } from '../../../hooks/useCollectiblesCollections'; import ZENDESK_URLS from '../../../helpers/constants/zendesk-url'; export default function CollectiblesTab({ onAddNFT }) { - const useCollectibleDetection = useSelector(getUseCollectibleDetection); + const useNftDetection = useSelector(getUseNftDetection); const isMainnet = useSelector(getIsMainnet); const collectibleDetectionNoticeDismissed = useSelector( getCollectiblesDetectionNoticeDismissed, @@ -46,9 +46,9 @@ export default function CollectiblesTab({ onAddNFT }) { const onRefresh = () => { if (isMainnet) { - dispatch(detectCollectibles()); + dispatch(detectNfts()); } - checkAndUpdateAllCollectiblesOwnershipStatus(); + checkAndUpdateAllNftsOwnershipStatus(); }; if (collectiblesLoading) { @@ -66,7 +66,7 @@ export default function CollectiblesTab({ onAddNFT }) { ) : ( <> {isMainnet && - !useCollectibleDetection && + !useNftDetection && !collectibleDetectionNoticeDismissed ? ( ) : null} @@ -123,7 +123,7 @@ export default function CollectiblesTab({ onAddNFT }) { className="collectibles-tab__link" justifyContent={JUSTIFY_CONTENT.FLEX_END} > - {isMainnet && !useCollectibleDetection ? ( + {isMainnet && !useNftDetection ? ( diff --git a/ui/components/app/collectibles-tab/collectibles-tab.test.js b/ui/components/app/collectibles-tab/collectibles-tab.test.js index 2e1b6b692..e9d10278d 100644 --- a/ui/components/app/collectibles-tab/collectibles-tab.test.js +++ b/ui/components/app/collectibles-tab/collectibles-tab.test.js @@ -150,17 +150,17 @@ const render = ({ selectedAddress, chainId = '0x1', collectiblesDetectionNoticeDismissed = false, - useCollectibleDetection, + useNftDetection, onAddNFT = jest.fn(), }) => { const store = configureStore({ metamask: { - allCollectibles: { + allNfts: { [ACCOUNT_1]: { [chainId]: collectibles, }, }, - allCollectibleContracts: { + allNftContracts: { [ACCOUNT_1]: { [chainId]: collectibleContracts, }, @@ -168,7 +168,7 @@ const render = ({ provider: { chainId }, selectedAddress, collectiblesDetectionNoticeDismissed, - useCollectibleDetection, + useNftDetection, collectiblesDropdownState, }, }); @@ -184,9 +184,9 @@ describe('Collectible Items', () => { setBackgroundConnection({ setCollectiblesDetectionNoticeDismissed: setCollectiblesDetectionNoticeDismissedStub, - detectCollectibles: detectCollectiblesStub, + detectNfts: detectCollectiblesStub, getState: getStateStub, - checkAndUpdateAllCollectiblesOwnershipStatus: + checkAndUpdateAllNftsOwnershipStatus: checkAndUpdateAllCollectiblesOwnershipStatusStub, updateCollectibleDropDownState: updateCollectibleDropDownStateStub, }); @@ -231,7 +231,7 @@ describe('Collectible Items', () => { render({ selectedAddress: ACCOUNT_1, collectibles: COLLECTIBLES, - useCollectibleDetection: true, + useNftDetection: true, }); expect(screen.queryByText('New! NFT detection')).not.toBeInTheDocument(); }); @@ -284,7 +284,7 @@ describe('Collectible Items', () => { render({ selectedAddress: ACCOUNT_1, collectibles: COLLECTIBLES, - useCollectibleDetection: true, + useNftDetection: true, }); expect(detectCollectiblesStub).not.toHaveBeenCalled(); expect( @@ -302,7 +302,7 @@ describe('Collectible Items', () => { chainId: '0x5', selectedAddress: ACCOUNT_1, collectibles: COLLECTIBLES, - useCollectibleDetection: true, + useNftDetection: true, }); expect( checkAndUpdateAllCollectiblesOwnershipStatusStub, diff --git a/ui/components/app/confirm-page-container/confirm-page-container-container.test.js b/ui/components/app/confirm-page-container/confirm-page-container-container.test.js index 2256f6e9c..7964f0645 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-container.test.js +++ b/ui/components/app/confirm-page-container/confirm-page-container-container.test.js @@ -92,7 +92,7 @@ describe('Confirm Page Container Container Test', () => { expect(senderRecipient).toBeInTheDocument(); }); it('should render recipient as address', () => { - const recipientName = screen.queryByText(shortenAddress(props.toAddress)); + const recipientName = screen.queryByText('New contract'); expect(recipientName).toBeInTheDocument(); }); @@ -118,7 +118,7 @@ describe('Confirm Page Container Container Test', () => { describe('Contact/AddressBook name should appear in recipient header', () => { it('should not show add to address dialog if recipient is in contact list and should display contact name', () => { - const addressBookName = 'test save name'; + const addressBookName = 'New contract'; const addressBook = { '0x5': { diff --git a/ui/components/app/confirm-page-container/confirm-page-container.component.js b/ui/components/app/confirm-page-container/confirm-page-container.component.js index fef74137b..c43233232 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container.component.js @@ -65,6 +65,7 @@ export default class ConfirmPageContainer extends Component { fromName: PropTypes.string, toAddress: PropTypes.string, toName: PropTypes.string, + toMetadataName: PropTypes.string, toEns: PropTypes.string, toNickname: PropTypes.string, // Content @@ -119,6 +120,7 @@ export default class ConfirmPageContainer extends Component { fromName, fromAddress, toName, + toMetadataName, toEns, toNickname, toAddress, @@ -233,6 +235,7 @@ export default class ConfirmPageContainer extends Component { senderName={fromName} senderAddress={fromAddress} recipientName={toName} + recipientMetadataName={toMetadataName} recipientAddress={toAddress} recipientEns={toEns} recipientNickname={toNickname} diff --git a/ui/components/app/confirm-page-container/confirm-page-container.container.js b/ui/components/app/confirm-page-container/confirm-page-container.container.js index 265ca7ea3..0ce2790fc 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container.container.js +++ b/ui/components/app/confirm-page-container/confirm-page-container.container.js @@ -5,6 +5,9 @@ import { getIsBuyableChain, getNetworkIdentifier, getSwapsDefaultToken, + getMetadataContractName, + getAccountName, + getMetaMaskIdentities, } from '../../../selectors'; import ConfirmPageContainer from './confirm-page-container.component'; @@ -15,11 +18,15 @@ function mapStateToProps(state, ownProps) { const networkIdentifier = getNetworkIdentifier(state); const defaultToken = getSwapsDefaultToken(state); const accountBalance = defaultToken.string; + const identities = getMetaMaskIdentities(state); + const toName = getAccountName(identities, to); + const toMetadataName = getMetadataContractName(state, to); return { isBuyableChain, contact, - toName: contact?.name || ownProps.toName, + toName, + toMetadataName, isOwnedAccount: getAccountsWithLabels(state) .map((accountWithLabel) => accountWithLabel.address) .includes(to), diff --git a/ui/components/app/confirm-page-container/flask/snap-insight.js b/ui/components/app/confirm-page-container/flask/snap-insight.js index 6446d4943..30c21776d 100644 --- a/ui/components/app/confirm-page-container/flask/snap-insight.js +++ b/ui/components/app/confirm-page-container/flask/snap-insight.js @@ -15,10 +15,15 @@ import { useI18nContext } from '../../../../hooks/useI18nContext'; import { useTransactionInsightSnap } from '../../../../hooks/flask/useTransactionInsightSnap'; import SnapContentFooter from '../../flask/snap-content-footer/snap-content-footer'; import Box from '../../../ui/box/box'; +import ActionableMessage from '../../../ui/actionable-message/actionable-message'; export const SnapInsight = ({ transaction, chainId, selectedSnap }) => { const t = useI18nContext(); - const response = useTransactionInsightSnap({ + const { + data: response, + error, + loading, + } = useTransactionInsightSnap({ transaction, chainId, snapId: selectedSnap.id, @@ -26,8 +31,8 @@ export const SnapInsight = ({ transaction, chainId, selectedSnap }) => { const data = response?.insights; - const hasNoData = !data || !Object.keys(data).length; - + const hasNoData = + !error && (loading || !data || (data && Object.keys(data).length === 0)); return ( { textAlign={hasNoData && TEXT_ALIGN.CENTER} className="snap-insight" > - {data ? ( + {!loading && !error && ( - {Object.keys(data).length ? ( + {data && Object.keys(data).length > 0 ? ( <> { )} - ) : ( + )} + + {!loading && error && ( + + + + )} + + {loading && ( <> { @@ -57,6 +65,10 @@ export default function CustomSpendingCap({ }; }; + const [customSpendingCapText, setCustomSpendingCapText] = useState( + getInputTextLogic(value).description, + ); + const handleChange = (valueInput) => { let spendingCapError = ''; const inputTextLogic = getInputTextLogic(valueInput); @@ -71,9 +83,19 @@ export default function CustomSpendingCap({ setError(''); } - setValue(valueInput); + dispatch(setCustomTokenAmount(String(valueInput))); }; + useEffect(() => { + if (value !== String(dappProposedValue)) { + setShowUseDefaultButton(true); + } + }, [value, dappProposedValue]); + + useEffect(() => { + passTheErrorText(error); + }, [error, passTheErrorText]); + const chooseTooltipContentText = value > currentTokenBalance ? t('warningTooltipText', [ @@ -100,7 +122,6 @@ export default function CustomSpendingCap({ onClick={(e) => { e.preventDefault(); handleChange(currentTokenBalance); - setValue(currentTokenBalance); }} > {t('max')} @@ -131,6 +152,7 @@ export default function CustomSpendingCap({ } > { - e.preventDefault(); - if (value <= currentTokenBalance || error) { + showUseDefaultButton && ( + + }} + > + {t('useDefault')} + + ) } titleDetailWrapperProps={{ marginBottom: 2, marginRight: 0 }} allowDecimals @@ -202,7 +222,7 @@ CustomSpendingCap.propTypes = { */ siteOrigin: PropTypes.string, /** - * onClick handler for the Edit link + * Parent component's callback function passed in order to get the error text */ - onEdit: PropTypes.func, + passTheErrorText: PropTypes.func, }; diff --git a/ui/components/app/custom-spending-cap/custom-spending-cap.stories.js b/ui/components/app/custom-spending-cap/custom-spending-cap.stories.js index a8f757926..5c30a32e7 100644 --- a/ui/components/app/custom-spending-cap/custom-spending-cap.stories.js +++ b/ui/components/app/custom-spending-cap/custom-spending-cap.stories.js @@ -17,8 +17,8 @@ export default { siteOrigin: { control: { type: 'text' }, }, - onEdit: { - action: 'onEdit', + passTheErrorText: { + action: 'passTheErrorText', }, }, args: { diff --git a/ui/components/app/custom-spending-cap/index.scss b/ui/components/app/custom-spending-cap/index.scss index 00ae12252..e2ab68851 100644 --- a/ui/components/app/custom-spending-cap/index.scss +++ b/ui/components/app/custom-spending-cap/index.scss @@ -21,6 +21,7 @@ position: absolute; margin-top: 55px; margin-inline-start: -75px; + z-index: 1; } } @@ -28,4 +29,10 @@ color: var(--color-error-default); padding-inline-end: 60px; } + + input[type='number']::-webkit-inner-spin-button, + input[type='number']:hover::-webkit-inner-spin-button { + -webkit-appearance: none; + display: none; + } } diff --git a/ui/components/app/flask/update-snap-permission-list/update-snap-permission-list.js b/ui/components/app/flask/update-snap-permission-list/update-snap-permission-list.js index 3ecfa91c7..105574d1d 100644 --- a/ui/components/app/flask/update-snap-permission-list/update-snap-permission-list.js +++ b/ui/components/app/flask/update-snap-permission-list/update-snap-permission-list.js @@ -14,77 +14,89 @@ export default function UpdateSnapPermissionList({ const t = useI18nContext(); const ApprovedPermissions = () => { - return Object.keys(approvedPermissions).map((approvedPermission) => { - const { label, rightIcon } = getPermissionDescription( - t, - approvedPermission, - ); - const { date } = approvedPermissions[approvedPermission]; - const formattedDate = formatDate(date, 'yyyy-MM-dd'); - return ( -
    - -
    - {label} - - {t('approvedOn', [formattedDate])} - + return Object.entries(approvedPermissions).map( + ([permissionName, permissionValue]) => { + const { label, rightIcon } = getPermissionDescription( + t, + permissionName, + permissionValue, + ); + const { date } = permissionValue; + const formattedDate = formatDate(date, 'yyyy-MM-dd'); + return ( +
    + +
    + {label} + + {t('approvedOn', [formattedDate])} + +
    + {rightIcon && }
    - {rightIcon && } -
    - ); - }); + ); + }, + ); }; const RevokedPermissions = () => { - return Object.keys(revokedPermissions).map((revokedPermission) => { - const { label, rightIcon } = getPermissionDescription( - t, - revokedPermission, - ); - return ( -
    - -
    - {label} - - {t('permissionRevoked')} - + return Object.entries(revokedPermissions).map( + ([permissionName, permissionValue]) => { + const { label, rightIcon } = getPermissionDescription( + t, + permissionName, + permissionValue, + ); + return ( +
    + +
    + {label} + + {t('permissionRevoked')} + +
    + {rightIcon && }
    - {rightIcon && } -
    - ); - }); + ); + }, + ); }; const NewPermissions = () => { - return Object.keys(newPermissions).map((newPermission) => { - const { label, rightIcon } = getPermissionDescription(t, newPermission); - return ( -
    - -
    - {label} - - {t('permissionRequested')} - + return Object.entries(newPermissions).map( + ([permissionName, permissionValue]) => { + const { label, rightIcon } = getPermissionDescription( + t, + permissionName, + permissionValue, + ); + return ( +
    + +
    + {label} + + {t('permissionRequested')} + +
    + {rightIcon && }
    - {rightIcon && } -
    - ); - }); + ); + }, + ); }; return ( diff --git a/ui/components/app/modals/contract-details-modal/contract-details-modal.js b/ui/components/app/modals/contract-details-modal/contract-details-modal.js index dd291303f..9095ef160 100644 --- a/ui/components/app/modals/contract-details-modal/contract-details-modal.js +++ b/ui/components/app/modals/contract-details-modal/contract-details-modal.js @@ -25,6 +25,8 @@ import { import { useCopyToClipboard } from '../../../../hooks/useCopyToClipboard'; import UrlIcon from '../../../ui/url-icon/url-icon'; import { getAddressBookEntry } from '../../../../selectors'; +import { ERC1155, ERC721 } from '../../../../../shared/constants/transaction'; +import NftCollectionImage from '../../../ui/nft-collection-image/nft-collection-image'; export default function ContractDetailsModal({ onClose, @@ -35,6 +37,9 @@ export default function ContractDetailsModal({ rpcPrefs, origin, siteImage, + tokenId, + assetName, + assetStandard, }) { const t = useI18nContext(); const [copiedTokenAddress, handleCopyTokenAddress] = useCopyToClipboard(); @@ -43,6 +48,12 @@ export default function ContractDetailsModal({ const addressBookEntry = useSelector((state) => ({ data: getAddressBookEntry(state, toAddress), })); + const nft = + assetStandard === ERC721 || + assetStandard === ERC1155 || + // if we don't have an asset standard but we do have either both an assetname and a tokenID or both a tokenName and tokenId we assume its an NFT + (assetName && tokenId) || + (tokenName && tokenId); return ( @@ -75,7 +86,7 @@ export default function ContractDetailsModal({ marginTop={4} marginBottom={2} > - {t('contractToken')} + {nft ? t('contractNFT') : t('contractToken')} - + {nft ? ( + + + + ) : ( + + )} {ellipsify(tokenAddress)} @@ -166,7 +188,9 @@ export default function ContractDetailsModal({ marginTop={4} marginBottom={2} > - {t('contractRequestingSpendingCap')} + {nft + ? t('contractRequestingAccess') + : t('contractRequestingSpendingCap')} - + {nft ? ( + + ) : ( + + )} {ellipsify(toAddress)} @@ -310,4 +344,16 @@ ContractDetailsModal.propTypes = { * Dapp image */ siteImage: PropTypes.string, + /** + * The token id of the collectible + */ + tokenId: PropTypes.string, + /** + * Token Standard + */ + assetStandard: PropTypes.string, + /** + * The name of the collection + */ + assetName: PropTypes.string, }; diff --git a/ui/components/app/modals/contract-details-modal/index.scss b/ui/components/app/modals/contract-details-modal/index.scss index 52af5a240..1180406c3 100644 --- a/ui/components/app/modals/contract-details-modal/index.scss +++ b/ui/components/app/modals/contract-details-modal/index.scss @@ -1,12 +1,15 @@ .contract-details-modal { - width: 360px !important; + width: 100% !important; + max-width: 408px; &__content { border-bottom: 1px solid var(--color-border-muted); &__contract { &__identicon { - margin: 16px 16px 38px 16px; + margin: 16px; + box-shadow: none; + background: none; } &__identicon-for-unknown-contact { diff --git a/ui/components/app/modals/convert-token-to-nft-modal/convert-token-to-nft-modal.js b/ui/components/app/modals/convert-token-to-nft-modal/convert-token-to-nft-modal.js index 6b5cc9673..edc0fd47a 100644 --- a/ui/components/app/modals/convert-token-to-nft-modal/convert-token-to-nft-modal.js +++ b/ui/components/app/modals/convert-token-to-nft-modal/convert-token-to-nft-modal.js @@ -19,8 +19,8 @@ const ConvertTokenToNFTModal = ({ hideModal, tokenAddress }) => { const history = useHistory(); const t = useI18nContext(); const dispatch = useDispatch(); - const allCollectibles = useSelector(getCollectibles); - const tokenAddedAsNFT = allCollectibles.find(({ address }) => + const allNfts = useSelector(getCollectibles); + const tokenAddedAsNFT = allNfts.find(({ address }) => isEqualCaseInsensitive(address, tokenAddress), ); diff --git a/ui/components/app/signature-request/index.scss b/ui/components/app/signature-request/index.scss index aacb6a2f9..f89b3c667 100644 --- a/ui/components/app/signature-request/index.scss +++ b/ui/components/app/signature-request/index.scss @@ -1,6 +1,7 @@ @import 'signature-request-footer/index'; @import 'signature-request-header/index'; @import 'signature-request-message/index'; +@import 'signature-request-data/index'; .signature-request { display: flex; diff --git a/ui/components/app/signature-request/signature-request-data/index.js b/ui/components/app/signature-request/signature-request-data/index.js new file mode 100644 index 000000000..64ed87d93 --- /dev/null +++ b/ui/components/app/signature-request/signature-request-data/index.js @@ -0,0 +1 @@ +export { default } from './signature-request-data'; diff --git a/ui/components/app/signature-request/signature-request-data/index.scss b/ui/components/app/signature-request/signature-request-data/index.scss new file mode 100644 index 000000000..6b81bd84d --- /dev/null +++ b/ui/components/app/signature-request/signature-request-data/index.scss @@ -0,0 +1,26 @@ +.signature-request-data { + &__node { + &__value { + white-space: pre-line; + overflow: hidden; + word-wrap: break-word; + + &__address { + [dir='rtl'] & { + /*rtl:ignore*/ + direction: ltr; + + /*rtl:ignore*/ + text-align: right; + + span { + display: block; + + /*rtl:ignore*/ + direction: rtl; + } + } + } + } + } +} diff --git a/ui/components/app/signature-request/signature-request-data/signature-request-data.js b/ui/components/app/signature-request/signature-request-data/signature-request-data.js new file mode 100644 index 000000000..6685f1c56 --- /dev/null +++ b/ui/components/app/signature-request/signature-request-data/signature-request-data.js @@ -0,0 +1,79 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; +import { getMetaMaskIdentities, getAccountName } from '../../../../selectors'; +import Address from '../../transaction-decoding/components/decoding/address'; +import { + isValidHexAddress, + toChecksumHexAddress, +} from '../../../../../shared/modules/hexstring-utils'; +import Box from '../../../ui/box'; +import Typography from '../../../ui/typography'; +import { + DISPLAY, + COLORS, + FONT_WEIGHT, + TYPOGRAPHY, +} from '../../../../helpers/constants/design-system'; + +export default function SignatureRequestData({ data }) { + const identities = useSelector(getMetaMaskIdentities); + + return ( + + {Object.entries(data).map(([label, value], i) => ( + + + {label.charAt(0).toUpperCase() + label.slice(1)}:{' '} + + {typeof value === 'object' && value !== null ? ( + + ) : ( + + {isValidHexAddress(value, { + mixedCaseUseChecksum: true, + }) ? ( + +
    + + ) : ( + `${value}` + )} + + )} + + ))} + + ); +} + +SignatureRequestData.propTypes = { + data: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired, +}; diff --git a/ui/components/app/signature-request/signature-request-message/index.js b/ui/components/app/signature-request/signature-request-message/index.js index 5fe8ddb1c..569576fdd 100644 --- a/ui/components/app/signature-request/signature-request-message/index.js +++ b/ui/components/app/signature-request/signature-request-message/index.js @@ -1 +1 @@ -export { default } from './signature-request-message.component'; +export { default } from './signature-request-message'; diff --git a/ui/components/app/signature-request/signature-request-message/index.scss b/ui/components/app/signature-request/signature-request-message/index.scss index b0b5c67ae..54fdb59d4 100644 --- a/ui/components/app/signature-request/signature-request-message/index.scss +++ b/ui/components/app/signature-request/signature-request-message/index.scss @@ -1,75 +1,24 @@ .signature-request-message { flex: 1 60%; - display: flex; max-height: 231px; - flex-direction: column; position: relative; - &__title { - @include H6; - - font-weight: 500; - color: var(--color-text-alternative); - margin-left: 12px; - } - - h2 { - @include H6; - - flex: 1 1 0; - text-align: left; - border-bottom: 1px solid var(--color-border-default); - padding: 0.5rem; - margin: 0; - color: var(--color-text-alternative); - } - - &--root { + &__root { flex: 1 100%; - background-color: var(--color-background-alternative); - padding-bottom: 0.5rem; overflow: auto; - padding-left: 12px; - padding-right: 12px; @include screen-sm-min { width: auto; } } - &--node, - &--node-leaf { - padding-left: 0.3rem; - - &-label { - color: var(--color-text-alternative); - margin-left: 0.5rem; - } - - &-value { - color: var(--color-text-default); - margin-left: 0.5rem; - white-space: pre-line; - overflow: hidden; - word-wrap: break-word; - } - } - - &--node-leaf { - display: flex; - } - &__scroll-button { - display: flex; - align-items: center; - justify-content: center; - border: 1px solid var(--color-border-default); - background: var(--color-background-alternative); - color: var(--color-icon-default); position: absolute; - right: 24px; + right: 28px; bottom: 12px; border-radius: 50%; + height: 24px; + width: 24px; cursor: pointer; } } diff --git a/ui/components/app/signature-request/signature-request-message/signature-request-message.component.js b/ui/components/app/signature-request/signature-request-message/signature-request-message.component.js deleted file mode 100644 index 641931919..000000000 --- a/ui/components/app/signature-request/signature-request-message/signature-request-message.component.js +++ /dev/null @@ -1,103 +0,0 @@ -import React, { PureComponent } from 'react'; -import PropTypes from 'prop-types'; -import { debounce } from 'lodash'; -import classnames from 'classnames'; - -export default class SignatureRequestMessage extends PureComponent { - static propTypes = { - data: PropTypes.object.isRequired, - onMessageScrolled: PropTypes.func, - setMessageRootRef: PropTypes.func, - messageRootRef: PropTypes.object, - messageIsScrollable: PropTypes.bool, - }; - - static contextTypes = { - t: PropTypes.func, - }; - - state = { - messageIsScrolled: false, - }; - - setMessageIsScrolled = () => { - if (!this.props.messageRootRef || this.state.messageIsScrolled) { - return; - } - - const { scrollTop, offsetHeight, scrollHeight } = this.props.messageRootRef; - const isAtBottom = Math.round(scrollTop) + offsetHeight >= scrollHeight; - - if (isAtBottom) { - this.setState({ messageIsScrolled: true }); - this.props.onMessageScrolled(); - } - }; - - onScroll = debounce(this.setMessageIsScrolled, 25); - - renderNode(data) { - return ( -
    - {Object.entries(data).map(([label, value], i) => ( -
    - - {label}:{' '} - - {typeof value === 'object' && value !== null ? ( - this.renderNode(value) - ) : ( - - {`${value}`} - - )} -
    - ))} -
    - ); - } - - renderScrollButton() { - return ( -
    { - this.setState({ messageIsScrolled: true }); - this.props.onMessageScrolled(); - this.props.messageRootRef.scrollTo( - 0, - this.props.messageRootRef.scrollHeight, - ); - }} - className="signature-request-message__scroll-button" - data-testid="signature-request-scroll-button" - > - -
    - ); - } - - render() { - const { data, messageIsScrollable } = this.props; - - return ( -
    - {messageIsScrollable ? this.renderScrollButton() : null} -
    - {this.context.t('signatureRequest1')} -
    -
    - {this.renderNode(data)} -
    -
    - ); - } -} diff --git a/ui/components/app/signature-request/signature-request-message/signature-request-message.js b/ui/components/app/signature-request/signature-request-message/signature-request-message.js new file mode 100644 index 000000000..e86dfba3d --- /dev/null +++ b/ui/components/app/signature-request/signature-request-message/signature-request-message.js @@ -0,0 +1,98 @@ +import React, { useContext, useState } from 'react'; +import PropTypes from 'prop-types'; +import { debounce } from 'lodash'; +import { I18nContext } from '../../../../contexts/i18n'; +import Box from '../../../ui/box'; +import Typography from '../../../ui/typography'; +import { + DISPLAY, + ALIGN_ITEMS, + JUSTIFY_CONTENT, + COLORS, + FONT_WEIGHT, + FLEX_DIRECTION, + SIZES, +} from '../../../../helpers/constants/design-system'; +import SignatureRequestData from '../signature-request-data'; + +export default function SignatureRequestMessage({ + data, + onMessageScrolled, + setMessageRootRef, + messageRootRef, + messageIsScrollable, +}) { + const t = useContext(I18nContext); + const [messageIsScrolled, setMessageIsScrolled] = useState(false); + const setMessageIsScrolledAtBottom = () => { + if (!messageRootRef || messageIsScrolled) { + return; + } + + const { scrollTop, offsetHeight, scrollHeight } = messageRootRef; + const isAtBottom = Math.round(scrollTop) + offsetHeight >= scrollHeight; + + if (isAtBottom) { + setMessageIsScrolled(true); + onMessageScrolled(); + } + }; + + return ( + + {messageIsScrollable ? ( + { + setMessageIsScrolled(true); + onMessageScrolled(); + messageRootRef?.scrollTo(0, messageRootRef?.scrollHeight); + }} + className="signature-request-message__scroll-button" + data-testid="signature-request-scroll-button" + > + + + ) : null} + + + {t('signatureRequest1')} + + + + + ); +} + +SignatureRequestMessage.propTypes = { + data: PropTypes.object.isRequired, + onMessageScrolled: PropTypes.func, + setMessageRootRef: PropTypes.func, + messageRootRef: PropTypes.object, + messageIsScrollable: PropTypes.bool, +}; diff --git a/ui/components/app/signature-request/signature-request-message/signature-request-message.stories.js b/ui/components/app/signature-request/signature-request-message/signature-request-message.stories.js new file mode 100644 index 000000000..9d3743af0 --- /dev/null +++ b/ui/components/app/signature-request/signature-request-message/signature-request-message.stories.js @@ -0,0 +1,58 @@ +import React from 'react'; +import SignatureRequestMessage from './signature-request-message'; + +export default { + title: 'Components/App/SignatureRequestMessage', + id: __filename, + component: SignatureRequestMessage, + argTypes: { + data: { control: 'object' }, + onMessageScrolled: { action: 'onMessageScrolled' }, + setMessageRootRef: { action: 'setMessageRootRef' }, + messageRootRef: { control: 'object' }, + messageIsScrollable: { control: 'boolean' }, + }, +}; + +export const DefaultStory = (args) => { + return ; +}; + +DefaultStory.storyName = 'Default'; + +DefaultStory.args = { + data: JSON.parse( + JSON.stringify({ + domain: { + name: 'happydapp.website', + }, + message: { + string: 'haay wuurl', + number: 42, + }, + primaryType: 'Mail', + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + Group: [ + { name: 'name', type: 'string' }, + { name: 'members', type: 'Person[]' }, + ], + Mail: [ + { name: 'from', type: 'Person' }, + { name: 'to', type: 'Person[]' }, + { name: 'contents', type: 'string' }, + ], + Person: [ + { name: 'name', type: 'string' }, + { name: 'wallets', type: 'address[]' }, + ], + }, + }), + ), + messageIsScrollable: true, +}; diff --git a/ui/components/app/signature-request/signature-request.container.test.js b/ui/components/app/signature-request/signature-request.container.test.js index 551e9645e..085b117d4 100644 --- a/ui/components/app/signature-request/signature-request.container.test.js +++ b/ui/components/app/signature-request/signature-request.container.test.js @@ -8,6 +8,46 @@ import SignatureRequest from './signature-request.container'; describe('Signature Request', () => { const mockStore = { metamask: { + tokenList: { + '0x514910771af9ca656af840dff83e8264ecf986ca': { + address: '0x514910771af9ca656af840dff83e8264ecf986ca', + symbol: 'LINK', + decimals: 18, + name: 'ChainLink Token', + iconUrl: + 'https://crypto.com/price/coin-data/icon/LINK/color_icon.png', + aggregators: [ + 'Aave', + 'Bancor', + 'CMC', + 'Crypto.com', + 'CoinGecko', + '1inch', + 'Paraswap', + 'PMM', + 'Zapper', + 'Zerion', + '0x', + ], + occurrences: 12, + unlisted: false, + }, + }, + identities: { + '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e': { + name: 'Account 2', + address: '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e', + }, + }, + addressBook: { + undefined: { + 0: { + address: '0x39a4e4Af7cCB654dB9500F258c64781c8FbD39F0', + name: '', + isEns: false, + }, + }, + }, provider: { type: 'rpc', }, diff --git a/ui/components/app/transaction-decoding/components/decoding/address/address.component.js b/ui/components/app/transaction-decoding/components/decoding/address/address.component.js index 7e6358634..ddb4d5cd7 100644 --- a/ui/components/app/transaction-decoding/components/decoding/address/address.component.js +++ b/ui/components/app/transaction-decoding/components/decoding/address/address.component.js @@ -5,7 +5,10 @@ import copyToClipboard from 'copy-to-clipboard'; import { shortenAddress } from '../../../../../../helpers/utils/util'; import Identicon from '../../../../../ui/identicon'; import { useI18nContext } from '../../../../../../hooks/useI18nContext'; -import { getAddressBook } from '../../../../../../selectors'; +import { + getMetadataContractName, + getAddressBook, +} from '../../../../../../selectors'; import NicknamePopovers from '../../../../modals/nickname-popovers'; const Address = ({ @@ -20,15 +23,25 @@ const Address = ({ const addressBook = useSelector(getAddressBook); const addressBookEntryObject = addressBook.find( - (entry) => entry.address === checksummedRecipientAddress, + (entry) => + entry.address.toLowerCase() === checksummedRecipientAddress.toLowerCase(), ); const recipientNickname = addressBookEntryObject?.name; + const recipientMetadataName = useSelector((state) => + getMetadataContractName(state, checksummedRecipientAddress), + ); const recipientToRender = addressOnly - ? recipientNickname || + ? recipientName || + recipientNickname || + recipientMetadataName || recipientEns || shortenAddress(checksummedRecipientAddress) - : recipientNickname || recipientEns || recipientName || t('newContract'); + : recipientName || + recipientNickname || + recipientMetadataName || + recipientEns || + t('newContract'); return (
    { diff --git a/ui/components/app/transaction-list-item-details/transaction-list-item-details.container.js b/ui/components/app/transaction-list-item-details/transaction-list-item-details.container.js index 61731c092..86b341533 100644 --- a/ui/components/app/transaction-list-item-details/transaction-list-item-details.container.js +++ b/ui/components/app/transaction-list-item-details/transaction-list-item-details.container.js @@ -8,6 +8,9 @@ import { getIsCustomNetwork, getRpcPrefsForCurrentProvider, getEnsResolutionByAddress, + getAccountName, + getMetadataContractName, + getMetaMaskIdentities, } from '../../../selectors'; import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; import TransactionListItemDetails from './transaction-list-item-details.component'; @@ -20,6 +23,12 @@ const mapStateToProps = (state, ownProps) => { recipientEns = getEnsResolutionByAddress(state, address); } const addressBook = getAddressBook(state); + const identities = getMetaMaskIdentities(state); + const recipientName = getAccountName(identities, recipientAddress); + const recipientMetadataName = getMetadataContractName( + state, + recipientAddress, + ); const getNickName = (address) => { const entry = addressBook.find((contact) => { @@ -38,6 +47,8 @@ const mapStateToProps = (state, ownProps) => { recipientNickname: recipientAddress ? getNickName(recipientAddress) : null, isCustomNetwork, blockExplorerLinkText: getBlockExplorerLinkText(state), + recipientName, + recipientMetadataName, }; }; diff --git a/ui/components/app/whats-new-popup/whats-new-popup.js b/ui/components/app/whats-new-popup/whats-new-popup.js index 858805978..e11405a56 100644 --- a/ui/components/app/whats-new-popup/whats-new-popup.js +++ b/ui/components/app/whats-new-popup/whats-new-popup.js @@ -58,6 +58,10 @@ function getActionFunctionById(id, history) { updateViewedNotifications({ 14: true }); history.push(`${ADVANCED_ROUTE}#backup-userdata`); }, + 16: () => { + updateViewedNotifications({ 16: true }); + history.push(EXPERIMENTAL_ROUTE); + }, }; return actionFunctions[id]; diff --git a/ui/components/component-library/button-base/README.mdx b/ui/components/component-library/button-base/README.mdx index ad79e0747..76d630f88 100644 --- a/ui/components/component-library/button-base/README.mdx +++ b/ui/components/component-library/button-base/README.mdx @@ -22,7 +22,7 @@ The `ButtonBase` accepts all props below as well as all [Box](/docs/ui-component Use the `size` prop and the `SIZES` object from `./ui/helpers/constants/design-system.js` to change the size of `ButtonBase`. Defaults to `SIZES.MD` -Optional: `BUTTON_SIZES` from `./button-base` object can be used instead of `SIZES`. +Optional: `BUTTON_BASE_SIZES` from `./button-base` object can be used instead of `SIZES`. Possible sizes include: @@ -37,7 +37,7 @@ Possible sizes include: ```jsx import { SIZES } from '../../../helpers/constants/design-system'; -import { ButtonBase } from '../ui/component-library'; +import { ButtonBase } from '../../ui/components/component-library'; @@ -55,7 +55,7 @@ Use boolean `block` prop to quickly enable a full width block button ```jsx import { DISPLAY } from '../../../helpers/constants/design-system'; -import { ButtonBase } from '../ui/component-library'; +import { ButtonBase } from '../../ui/components/component-library'; Default Button Block Button @@ -77,7 +77,7 @@ Button `as` options: ```jsx -import { ButtonBase } from '../ui/component-library'; +import { ButtonBase } from '../../ui/components/component-library'; Button Element @@ -86,6 +86,20 @@ import { ButtonBase } from '../ui/component-library'; ``` +### Href + +When an `href` prop is passed it will change the element to an anchor(`a`) tag. + + + + + +```jsx +import { ButtonBase } from '../../ui/components/component-library'; + +Anchor Element; +``` + ### Disabled Use the boolean `disabled` prop to disable button @@ -95,7 +109,7 @@ Use the boolean `disabled` prop to disable button ```jsx -import { ButtonBase } from '../ui/component-library'; +import { ButtonBase } from '../../ui/components/component-library'; Disabled Button; ``` @@ -109,21 +123,21 @@ Use the boolean `loading` prop to set loading spinner ```jsx -import { ButtonBase } from '../ui/component-library'; +import { ButtonBase } from '../../ui/components/component-library'; Loading Button; ``` ### Icon -Use the `icon` prop and the `ICON_NAMES` object from `./ui/components/component-library/icon` to select icon. +Use the `icon` prop and the `ICON_NAMES` object from `./ui/components/component-library` to select icon. ```jsx -import { ButtonBase } from '../ui/component-library'; +import { ButtonBase } from '../../ui/components/component-library'; import { ICON_NAMES } from '../icon'; Button; diff --git a/ui/components/component-library/button-base/__snapshots__/button-base.test.js.snap b/ui/components/component-library/button-base/__snapshots__/button-base.test.js.snap new file mode 100644 index 000000000..a1bcad5e5 --- /dev/null +++ b/ui/components/component-library/button-base/__snapshots__/button-base.test.js.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ButtonBase should render button element correctly and match snapshot 1`] = ` +
    + +
    +`; diff --git a/ui/components/component-library/button-base/button.constants.js b/ui/components/component-library/button-base/button-base.constants.js similarity index 79% rename from ui/components/component-library/button-base/button.constants.js rename to ui/components/component-library/button-base/button-base.constants.js index bcf67e6f1..a5c102366 100644 --- a/ui/components/component-library/button-base/button.constants.js +++ b/ui/components/component-library/button-base/button-base.constants.js @@ -1,6 +1,6 @@ import { SIZES } from '../../../helpers/constants/design-system'; -export const BUTTON_SIZES = { +export const BUTTON_BASE_SIZES = { SM: SIZES.SM, MD: SIZES.MD, LG: SIZES.LG, diff --git a/ui/components/component-library/button-base/button-base.js b/ui/components/component-library/button-base/button-base.js index 9f3a3dffe..426b65a31 100644 --- a/ui/components/component-library/button-base/button-base.js +++ b/ui/components/component-library/button-base/button-base.js @@ -15,14 +15,15 @@ import { SIZES, FLEX_DIRECTION, } from '../../../helpers/constants/design-system'; -import { BUTTON_SIZES } from './button.constants'; +import { BUTTON_BASE_SIZES } from './button-base.constants'; export const ButtonBase = ({ as = 'button', block, children, className, - size = BUTTON_SIZES.MD, + href, + size = BUTTON_BASE_SIZES.MD, icon, iconPositionRight, loading, @@ -30,12 +31,12 @@ export const ButtonBase = ({ iconProps, ...props }) => { - const Tag = props?.href ? 'a' : as; + const Tag = href ? 'a' : as; return ( {icon && ( )} @@ -77,7 +78,7 @@ export const ButtonBase = ({ )} @@ -105,6 +106,10 @@ ButtonBase.propTypes = { * Boolean to disable button */ disabled: PropTypes.bool, + /** + * When an `href` prop is passed, ButtonBase will automatically change the root element to be an `a` (anchor) tag + */ + href: PropTypes.string, /** * Add icon to left side of button text passing icon name * The name of the icon to display. Should be one of ICON_NAMES @@ -125,13 +130,9 @@ ButtonBase.propTypes = { loading: PropTypes.bool, /** * The size of the ButtonBase. - * Possible values could be 'SIZES.AUTO', 'SIZES.SM', 'SIZES.MD', 'SIZES.LG', + * Possible values could be 'SIZES.AUTO', 'SIZES.SM'(32px), 'SIZES.MD'(40px), 'SIZES.LG'(48px), */ - size: PropTypes.oneOf(Object.values(BUTTON_SIZES)), - /** - * Addition style properties to apply to the button. - */ - style: PropTypes.object, + size: PropTypes.oneOf(Object.values(BUTTON_BASE_SIZES)), /** * ButtonBase accepts all the props from Box */ diff --git a/ui/components/component-library/button-base/button-base.stories.js b/ui/components/component-library/button-base/button-base.stories.js index 320223fa8..620e4d124 100644 --- a/ui/components/component-library/button-base/button-base.stories.js +++ b/ui/components/component-library/button-base/button-base.stories.js @@ -9,7 +9,7 @@ import { import Box from '../../ui/box/box'; import { ICON_NAMES } from '../icon'; import { Text } from '../text'; -import { BUTTON_SIZES } from './button.constants'; +import { BUTTON_BASE_SIZES } from './button-base.constants'; import { ButtonBase } from './button-base'; import README from './README.mdx'; @@ -66,7 +66,7 @@ export default { }, size: { control: 'select', - options: Object.values(BUTTON_SIZES), + options: Object.values(BUTTON_BASE_SIZES), }, marginTop: { options: marginSizeControlOptions, @@ -145,6 +145,12 @@ export const As = (args) => ( ); +export const Href = (args) => Anchor Element; + +Href.args = { + href: '/metamask', +}; + export const Disabled = (args) => ( Disabled Button ); diff --git a/ui/components/component-library/button-base/button-base.test.js b/ui/components/component-library/button-base/button-base.test.js index a62bec7aa..5cd14b841 100644 --- a/ui/components/component-library/button-base/button-base.test.js +++ b/ui/components/component-library/button-base/button-base.test.js @@ -1,17 +1,18 @@ /* eslint-disable jest/require-top-level-describe */ import { render } from '@testing-library/react'; import React from 'react'; -import { BUTTON_SIZES } from './button.constants'; +import { BUTTON_BASE_SIZES } from './button-base.constants'; import { ButtonBase } from './button-base'; describe('ButtonBase', () => { - it('should render button element correctly', () => { + it('should render button element correctly and match snapshot', () => { const { getByTestId, getByText, container } = render( Button base, ); expect(getByText('Button base')).toBeDefined(); expect(container.querySelector('button')).toBeDefined(); expect(getByTestId('button-base')).toHaveClass('mm-button'); + expect(container).toMatchSnapshot(); }); it('should render anchor element correctly', () => { @@ -40,23 +41,35 @@ describe('ButtonBase', () => { it('should render with different size classes', () => { const { getByTestId } = render( <> - - - - + + + + , ); - expect(getByTestId(BUTTON_SIZES.AUTO)).toHaveClass( - `mm-button--size-${BUTTON_SIZES.AUTO}`, + expect(getByTestId(BUTTON_BASE_SIZES.AUTO)).toHaveClass( + `mm-button--size-${BUTTON_BASE_SIZES.AUTO}`, ); - expect(getByTestId(BUTTON_SIZES.SM)).toHaveClass( - `mm-button--size-${BUTTON_SIZES.SM}`, + expect(getByTestId(BUTTON_BASE_SIZES.SM)).toHaveClass( + `mm-button--size-${BUTTON_BASE_SIZES.SM}`, ); - expect(getByTestId(BUTTON_SIZES.MD)).toHaveClass( - `mm-button--size-${BUTTON_SIZES.MD}`, + expect(getByTestId(BUTTON_BASE_SIZES.MD)).toHaveClass( + `mm-button--size-${BUTTON_BASE_SIZES.MD}`, ); - expect(getByTestId(BUTTON_SIZES.LG)).toHaveClass( - `mm-button--size-${BUTTON_SIZES.LG}`, + expect(getByTestId(BUTTON_BASE_SIZES.LG)).toHaveClass( + `mm-button--size-${BUTTON_BASE_SIZES.LG}`, ); }); diff --git a/ui/components/component-library/button-base/index.js b/ui/components/component-library/button-base/index.js index 789a9db9d..3d030b89b 100644 --- a/ui/components/component-library/button-base/index.js +++ b/ui/components/component-library/button-base/index.js @@ -1,2 +1,2 @@ export { ButtonBase } from './button-base'; -export { BUTTON_SIZES } from './button.constants'; +export { BUTTON_BASE_SIZES } from './button-base.constants'; diff --git a/ui/components/component-library/button-icon/README.mdx b/ui/components/component-library/button-icon/README.mdx new file mode 100644 index 000000000..a2f89fa09 --- /dev/null +++ b/ui/components/component-library/button-icon/README.mdx @@ -0,0 +1,144 @@ +import { Story, Canvas, ArgsTable } from '@storybook/addon-docs'; +import { ButtonIcon } from './button-icon'; + +# ButtonIcon + +The `ButtonIcon` is used for icons associated with a user action. + + + + + +## Props + +The `ButtonIcon` accepts all props below as well as all [Box](/docs/ui-components-ui-box-box-stories-js--default-story#props) component props + + + +### Icon\* + +Use the required `icon` prop with `ICON_NAMES` object from `./ui/components/component-library/icon` to select icon. + + + + + +```jsx +import { ButtonIcon } from '../ui/component-library'; +import { ICON_NAMES } from '../icon'; + +; +``` + +### Size + +Use the `size` prop and the `SIZES` object from `./ui/helpers/constants/design-system.js` +to change the size of `ButtonIcon`. Defaults to `SIZES.SM` + +Optional: `BUTTON_ICON_SIZES` from `./button-icon` object can be used instead of `SIZES`. + +Possible sizes include: + +- `SIZES.SM` 24px +- `SIZES.LG` 32px + + + + + +```jsx +import { SIZES } from '../../../helpers/constants/design-system'; +import { ButtonIcon } from '../ui/component-library'; + + + +``` + +### Aria Label + +Use the `ariaLabel` prop to set the name of the ButtonIcon for proper accessibility + + + + + +```jsx +import { ButtonIcon } from '../ui/component-library'; + + + + +``` + +### As + +Use the `as` box prop to change the element of `ButtonIcon`. Defaults to `button`. + +Button `as` options: + +- `button` +- `a` + + + + + +```jsx +import { ButtonIcon } from '../ui/component-library'; + + + + +``` + +### Href + +When an `href` prop is passed it will change the element to an anchor(`a`) tag. + + + + + +```jsx +import { ButtonIcon } from '../ui/component-library'; + +; +``` + +### Color + +Use the `color` prop and the `COLORS` object to change the color of the `ButtonIcon`. Defaults to `COLORS.ICON_DEFAULT`. + + + + + +```jsx +import { ButtonIcon } from '../ui/component-library'; + +; +``` + +### Disabled + +Use the boolean `disabled` prop to disable button + + + + + +```jsx +import { ButtonIcon } from '../ui/component-library'; + +; +``` diff --git a/ui/components/component-library/button-icon/__snapshots__/button-icon.test.js.snap b/ui/components/component-library/button-icon/__snapshots__/button-icon.test.js.snap new file mode 100644 index 000000000..ce55c094a --- /dev/null +++ b/ui/components/component-library/button-icon/__snapshots__/button-icon.test.js.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ButtonIcon should render button element correctly 1`] = ` +
    + +
    +`; diff --git a/ui/components/component-library/button-icon/button-icon.constants.js b/ui/components/component-library/button-icon/button-icon.constants.js new file mode 100644 index 000000000..2fd26011e --- /dev/null +++ b/ui/components/component-library/button-icon/button-icon.constants.js @@ -0,0 +1,6 @@ +import { SIZES } from '../../../helpers/constants/design-system'; + +export const BUTTON_ICON_SIZES = { + SM: SIZES.SM, + LG: SIZES.LG, +}; diff --git a/ui/components/component-library/button-icon/button-icon.js b/ui/components/component-library/button-icon/button-icon.js new file mode 100644 index 000000000..8d8ad11ca --- /dev/null +++ b/ui/components/component-library/button-icon/button-icon.js @@ -0,0 +1,101 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; + +import { + ALIGN_ITEMS, + BORDER_RADIUS, + COLORS, + DISPLAY, + JUSTIFY_CONTENT, +} from '../../../helpers/constants/design-system'; + +import Box from '../../ui/box'; +import { Icon } from '../icon'; + +import { BUTTON_ICON_SIZES } from './button-icon.constants'; + +export const ButtonIcon = ({ + ariaLabel, + as = 'button', + className, + color = COLORS.ICON_DEFAULT, + href, + size = BUTTON_ICON_SIZES.LG, + icon, + disabled, + iconProps, + ...props +}) => { + const Tag = href ? 'a' : as; + return ( + + + + ); +}; + +ButtonIcon.propTypes = { + /** + * String that adds an accessible name for ButtonIcon + */ + ariaLabel: PropTypes.string.isRequired, + /** + * The polymorphic `as` prop allows you to change the root HTML element of the Button component between `button` and `a` tag + */ + as: PropTypes.string, + /** + * An additional className to apply to the ButtonIcon. + */ + className: PropTypes.string, + /** + * The color of the ButtonIcon component should use the COLOR object from + * ./ui/helpers/constants/design-system.js + */ + color: PropTypes.oneOf(Object.values(COLORS)), + /** + * Boolean to disable button + */ + disabled: PropTypes.bool, + /** + * When an `href` prop is passed, ButtonIcon will automatically change the root element to be an `a` (anchor) tag + */ + href: PropTypes.string, + /** + * The name of the icon to display. Should be one of ICON_NAMES + */ + icon: PropTypes.string.isRequired, // Can't set PropTypes.oneOf(ICON_NAMES) because ICON_NAMES is an environment variable + /** + * iconProps accepts all the props from Icon + */ + iconProps: PropTypes.object, + /** + * The size of the ButtonIcon. + * Possible values could be 'SIZES.SM', 'SIZES.LG', + */ + size: PropTypes.oneOf(Object.values(BUTTON_ICON_SIZES)), + /** + * ButtonIcon accepts all the props from Box + */ + ...Box.propTypes, +}; diff --git a/ui/components/component-library/button-icon/button-icon.scss b/ui/components/component-library/button-icon/button-icon.scss new file mode 100644 index 000000000..3658d6590 --- /dev/null +++ b/ui/components/component-library/button-icon/button-icon.scss @@ -0,0 +1,32 @@ +.mm-button-icon { + --button-icon-size: var(--size, 24px); + --button-icon-opacity-hover: 0.5; // TODO: replace with design tokens + --button-icon-opacity-disabled: 0.3; // TODO: replace with design tokens + + height: var(--button-icon-size); + width: var(--button-icon-size); + padding: 0; + cursor: pointer; + + // ButtonIcon default states + &:active, + &:hover { + opacity: var(--button-icon-opacity-hover); + } + + &--disabled, + &:disabled { + opacity: var(--button-icon-opacity-disabled); + cursor: not-allowed; + } + + // ButtonIcon Sizes + &--size-sm { + --button-icon-size: 24px; + } + + &--size-lg { + --button-icon-size: 32px; + } +} + diff --git a/ui/components/component-library/button-icon/button-icon.stories.js b/ui/components/component-library/button-icon/button-icon.stories.js new file mode 100644 index 000000000..32db51f96 --- /dev/null +++ b/ui/components/component-library/button-icon/button-icon.stories.js @@ -0,0 +1,186 @@ +import React from 'react'; +import { + ALIGN_ITEMS, + COLORS, + DISPLAY, + FLEX_DIRECTION, + SIZES, +} from '../../../helpers/constants/design-system'; +import Box from '../../ui/box/box'; +import { ICON_NAMES } from '../icon'; +import { BUTTON_ICON_SIZES } from './button-icon.constants'; +import { ButtonIcon } from './button-icon'; +import README from './README.mdx'; + +const marginSizeControlOptions = [ + undefined, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 'auto', +]; + +export default { + title: 'Components/ComponentLibrary/ButtonIcon', + id: __filename, + component: ButtonIcon, + parameters: { + docs: { + page: README, + }, + }, + argTypes: { + ariaLabel: { + control: 'text', + }, + as: { + control: 'select', + options: ['button', 'a'], + }, + className: { + control: 'text', + }, + color: { + control: 'select', + options: Object.values(COLORS), + }, + disabled: { + control: 'boolean', + }, + href: { + control: 'string', + }, + icon: { + control: 'select', + options: Object.values(ICON_NAMES), + }, + size: { + control: 'select', + options: Object.values(BUTTON_ICON_SIZES), + }, + marginTop: { + options: marginSizeControlOptions, + control: 'select', + table: { category: 'box props' }, + }, + marginRight: { + options: marginSizeControlOptions, + control: 'select', + table: { category: 'box props' }, + }, + marginBottom: { + options: marginSizeControlOptions, + control: 'select', + table: { category: 'box props' }, + }, + marginLeft: { + options: marginSizeControlOptions, + control: 'select', + table: { category: 'box props' }, + }, + }, +}; + +export const DefaultStory = (args) => ; + +DefaultStory.args = { + icon: ICON_NAMES.CLOSE_OUTLINE, + ariaLabel: 'Close', +}; + +DefaultStory.storyName = 'Default'; + +export const Icon = (args) => ( + +); + +export const Size = (args) => ( + + + + +); + +export const AriaLabel = (args) => ( + <> + + + +); + +export const As = (args) => ( + + + + +); + +export const Href = (args) => ( + +); + +Href.args = { + href: 'https://metamask.io/', + color: COLORS.PRIMARY_DEFAULT, +}; + +export const Color = (args) => ( + +); + +Color.args = { + color: COLORS.PRIMARY_DEFAULT, +}; + +export const Disabled = (args) => ( + +); + +Disabled.args = { + disabled: true, +}; diff --git a/ui/components/component-library/button-icon/button-icon.test.js b/ui/components/component-library/button-icon/button-icon.test.js new file mode 100644 index 000000000..cb1521c64 --- /dev/null +++ b/ui/components/component-library/button-icon/button-icon.test.js @@ -0,0 +1,146 @@ +/* eslint-disable jest/require-top-level-describe */ +import { render } from '@testing-library/react'; +import React from 'react'; +import { COLORS } from '../../../helpers/constants/design-system'; +import { BUTTON_ICON_SIZES } from './button-icon.constants'; +import { ButtonIcon } from './button-icon'; + +describe('ButtonIcon', () => { + it('should render button element correctly', () => { + const { getByTestId, container } = render( + , + ); + expect(container.querySelector('button')).toBeDefined(); + expect(getByTestId('button-icon')).toHaveClass('mm-button-icon'); + expect(container).toMatchSnapshot(); + }); + + it('should render anchor element correctly', () => { + const { getByTestId, container } = render( + , + ); + expect(getByTestId('button-icon')).toHaveClass('mm-button-icon'); + const anchor = container.getElementsByTagName('a').length; + expect(anchor).toBe(1); + }); + + it('should render anchor element correctly using href', () => { + const { getByTestId, getByRole } = render( + , + ); + expect(getByTestId('button-icon')).toHaveClass('mm-button-icon'); + expect(getByRole('link')).toBeDefined(); + }); + + it('should render with different size classes', () => { + const { getByTestId } = render( + <> + + + , + ); + expect(getByTestId(BUTTON_ICON_SIZES.SM)).toHaveClass( + `mm-button-icon--size-${BUTTON_ICON_SIZES.SM}`, + ); + expect(getByTestId(BUTTON_ICON_SIZES.LG)).toHaveClass( + `mm-button-icon--size-${BUTTON_ICON_SIZES.LG}`, + ); + }); + + it('should render with different colors', () => { + const { getByTestId } = render( + <> + + + , + ); + expect(getByTestId(COLORS.ICON_DEFAULT)).toHaveClass( + `box--color-${COLORS.ICON_DEFAULT}`, + ); + expect(getByTestId(COLORS.ERROR_DEFAULT)).toHaveClass( + `box--color-${COLORS.ERROR_DEFAULT}`, + ); + }); + + it('should render with added classname', () => { + const { getByTestId } = render( + , + ); + expect(getByTestId('classname')).toHaveClass('mm-button-icon--test'); + }); + + it('should render with different button states', () => { + const { getByTestId } = render( + <> + + , + ); + + expect(getByTestId('disabled')).toHaveClass(`mm-button-icon--disabled`); + expect(getByTestId('disabled')).toBeDisabled(); + }); + it('should render with icon', () => { + const { getByTestId } = render( + , + ); + + expect(getByTestId('button-icon')).toBeDefined(); + }); + + it('should render with aria-label', () => { + const { getByLabelText } = render( + , + ); + + expect(getByLabelText('add')).toBeDefined(); + }); +}); diff --git a/ui/components/component-library/button-icon/index.js b/ui/components/component-library/button-icon/index.js new file mode 100644 index 000000000..c9d51bb07 --- /dev/null +++ b/ui/components/component-library/button-icon/index.js @@ -0,0 +1,2 @@ +export { ButtonIcon } from './button-icon'; +export { BUTTON_ICON_SIZES } from './button-icon.constants'; diff --git a/ui/components/component-library/button-link/README.mdx b/ui/components/component-library/button-link/README.mdx index d1a743912..0d51dc036 100644 --- a/ui/components/component-library/button-link/README.mdx +++ b/ui/components/component-library/button-link/README.mdx @@ -35,7 +35,7 @@ Possible sizes include: ```jsx import { SIZES } from '../../../helpers/constants/design-system'; -import { ButtonLink } from '../ui/component-library/button/button-link/button-link'; +import { ButtonLink } from '../../ui/components/component-library'; @@ -43,16 +43,16 @@ import { ButtonLink } from '../ui/component-library/button/button-link/button-li ``` -### Type +### Danger -Use the `type` prop and the `BUTTON_TYPES` object from `./ui/helpers/constants/design-system.js` to change the context of `ButtonLink`. +Use the `danger` boolean prop to change the `ButtonPrimary` to danger color. - + ```jsx -import { ButtonLink } from '../ui/component-library/button/button-link/button-link'; +import { ButtonLink } from '../../ui/components/component-library'; Normal Danger @@ -67,7 +67,7 @@ When an `href` is passed the tag element will switch to an `anchor`(`a`) tag. ```jsx -import { ButtonLink } from '../ui/component-library/button/button-link/button-link'; +import { ButtonLink } from '../../ui/components/component-library'; Href Example; ``` diff --git a/ui/components/component-library/button-link/button-link.stories.js b/ui/components/component-library/button-link/button-link.stories.js index e721459f4..d068ff0ff 100644 --- a/ui/components/component-library/button-link/button-link.stories.js +++ b/ui/components/component-library/button-link/button-link.stories.js @@ -139,7 +139,7 @@ export const Size = (args) => ( ); -export const Type = (args) => ( +export const Danger = (args) => ( Normal {/* Test Anchor tag to match exactly as button */} diff --git a/ui/components/component-library/button-link/button-link.test.js b/ui/components/component-library/button-link/button-link.test.js index 473714fb0..2c246f969 100644 --- a/ui/components/component-library/button-link/button-link.test.js +++ b/ui/components/component-library/button-link/button-link.test.js @@ -53,7 +53,7 @@ describe('ButtonLink', () => { ); }); - it('should render with different types', () => { + it('should render as danger', () => { const { getByTestId } = render( <> diff --git a/ui/components/component-library/button-primary/README.mdx b/ui/components/component-library/button-primary/README.mdx index 69a110540..48f883d8f 100644 --- a/ui/components/component-library/button-primary/README.mdx +++ b/ui/components/component-library/button-primary/README.mdx @@ -34,23 +34,23 @@ Possible sizes include: ```jsx import { SIZES } from '../../../helpers/constants/design-system'; -import { ButtonPrimary } from '../ui/component-library/button/button-primary/button-primary'; +import { ButtonPrimary } from '../../ui/components/component-library'; ``` -### Type +### Danger -Use the `type` prop and the `BUTTON_TYPES` object from `./ui/helpers/constants/design-system.js` to change the context of `ButtonPrimary`. +Use the `danger` boolean prop to change the `ButtonPrimary` to danger color. - + ```jsx -import { ButtonPrimary } from '../ui/component-library/button/button-primary/button-primary'; +import { ButtonPrimary } from '../../ui/components/component-library'; Normal Danger diff --git a/ui/components/component-library/button-primary/__snapshots__/button-primary.test.js.snap b/ui/components/component-library/button-primary/__snapshots__/button-primary.test.js.snap new file mode 100644 index 000000000..786e23cda --- /dev/null +++ b/ui/components/component-library/button-primary/__snapshots__/button-primary.test.js.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ButtonPrimary should render button element correctly 1`] = ` +
    + +
    +`; diff --git a/ui/components/component-library/button-primary/button-primary.js b/ui/components/component-library/button-primary/button-primary.js index 78dc3e93d..6b7fce938 100644 --- a/ui/components/component-library/button-primary/button-primary.js +++ b/ui/components/component-library/button-primary/button-primary.js @@ -28,11 +28,11 @@ ButtonPrimary.propTypes = { */ className: PropTypes.string, /** - * Boolean to change button type to Danger when true + * When true, `ButtonPrimary` color becomes Danger. */ danger: PropTypes.bool, /** - * The possible size values for ButtonPrimary: 'SIZES.SM', 'SIZES.MD', 'SIZES.LG', + * Possible size values: 'SIZES.SM'(32px), 'SIZES.MD'(40px), 'SIZES.LG'(48px). * Default value is 'SIZES.MD'. */ size: PropTypes.oneOf(Object.values(BUTTON_PRIMARY_SIZES)), diff --git a/ui/components/component-library/button-primary/button-primary.stories.js b/ui/components/component-library/button-primary/button-primary.stories.js index 8f9c3cafc..a125c6999 100644 --- a/ui/components/component-library/button-primary/button-primary.stories.js +++ b/ui/components/component-library/button-primary/button-primary.stories.js @@ -1,5 +1,9 @@ import React from 'react'; -import { ALIGN_ITEMS, DISPLAY } from '../../../helpers/constants/design-system'; +import { + ALIGN_ITEMS, + DISPLAY, + SIZES, +} from '../../../helpers/constants/design-system'; import Box from '../../ui/box/box'; import { ICON_NAMES } from '../icon'; import { ButtonPrimary } from './button-primary'; @@ -110,19 +114,19 @@ DefaultStory.storyName = 'Default'; export const Size = (args) => ( - + Small Button - + Medium (Default) Button - + Large Button ); -export const Type = (args) => ( +export const Danger = (args) => ( Normal {/* Test Anchor tag to match exactly as button */} diff --git a/ui/components/component-library/button-primary/button-primary.test.js b/ui/components/component-library/button-primary/button-primary.test.js index 34751ae7f..83892c345 100644 --- a/ui/components/component-library/button-primary/button-primary.test.js +++ b/ui/components/component-library/button-primary/button-primary.test.js @@ -14,6 +14,7 @@ describe('ButtonPrimary', () => { expect(getByText('Button Primary')).toBeDefined(); expect(container.querySelector('button')).toBeDefined(); expect(getByTestId('button-primary')).toHaveClass('mm-button'); + expect(container).toMatchSnapshot(); }); it('should render anchor element correctly', () => { @@ -66,7 +67,7 @@ describe('ButtonPrimary', () => { ); }); - it('should render with different types', () => { + it('should render as danger', () => { const { getByTestId } = render( <> diff --git a/ui/components/component-library/button-primary/index.js b/ui/components/component-library/button-primary/index.js index e6b99af4c..744f8bb3a 100644 --- a/ui/components/component-library/button-primary/index.js +++ b/ui/components/component-library/button-primary/index.js @@ -1 +1,2 @@ export { ButtonPrimary } from './button-primary'; +export { BUTTON_PRIMARY_SIZES } from './button-primary.constants'; diff --git a/ui/components/component-library/button-secondary/README.mdx b/ui/components/component-library/button-secondary/README.mdx index 16a5a74a6..04eff904c 100644 --- a/ui/components/component-library/button-secondary/README.mdx +++ b/ui/components/component-library/button-secondary/README.mdx @@ -34,23 +34,23 @@ Possible sizes include: ```jsx import { SIZES } from '../../../helpers/constants/design-system'; -import { ButtonSecondary } from '../ui/component-library/button/button-secondary/button-secondary'; +import { ButtonSecondary } from '../../ui/components/component-library'; ``` -### Type +### Danger -Use the `type` prop and the `BUTTON_TYPES` object from `./ui/helpers/constants/design-system.js` to change the context of `ButtonSecondary`. +Use the `danger` boolean prop to change the `ButtonSecondary` to danger color. - + ```jsx -import { ButtonSecondary } from '../ui/component-library/button/button-secondary/button-secondary'; +import { ButtonSecondary } from '../../ui/components/component-library'; Normal Danger diff --git a/ui/components/component-library/button-secondary/__snapshots__/button-secondary.test.js.snap b/ui/components/component-library/button-secondary/__snapshots__/button-secondary.test.js.snap new file mode 100644 index 000000000..05673f3ca --- /dev/null +++ b/ui/components/component-library/button-secondary/__snapshots__/button-secondary.test.js.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ButtonSecondary should render button element correctly 1`] = ` +
    + +
    +`; diff --git a/ui/components/component-library/button-secondary/button-secondary.js b/ui/components/component-library/button-secondary/button-secondary.js index c8d4ed608..eff2e3349 100644 --- a/ui/components/component-library/button-secondary/button-secondary.js +++ b/ui/components/component-library/button-secondary/button-secondary.js @@ -28,11 +28,11 @@ ButtonSecondary.propTypes = { */ className: PropTypes.string, /** - * Boolean to change button type to Danger when true + * When true, ButtonSecondary color becomes Danger. */ danger: PropTypes.bool, /** - * The possible size values for ButtonSecondary: 'SIZES.SM', 'SIZES.MD', 'SIZES.LG', + * Possible size values: 'SIZES.SM'(32px), 'SIZES.MD'(40px), 'SIZES.LG'(48px). * Default value is 'SIZES.MD'. */ size: PropTypes.oneOf(Object.values(BUTTON_SECONDARY_SIZES)), diff --git a/ui/components/component-library/button-secondary/button-secondary.stories.js b/ui/components/component-library/button-secondary/button-secondary.stories.js index 399cb412e..194b55cca 100644 --- a/ui/components/component-library/button-secondary/button-secondary.stories.js +++ b/ui/components/component-library/button-secondary/button-secondary.stories.js @@ -1,5 +1,9 @@ import React from 'react'; -import { ALIGN_ITEMS, DISPLAY } from '../../../helpers/constants/design-system'; +import { + ALIGN_ITEMS, + DISPLAY, + SIZES, +} from '../../../helpers/constants/design-system'; import Box from '../../ui/box/box'; import { ICON_NAMES } from '../icon'; import { ButtonSecondary } from './button-secondary'; @@ -110,19 +114,19 @@ DefaultStory.storyName = 'Default'; export const Size = (args) => ( - + Small Button - + Medium (Default) Button - + Large Button ); -export const Type = (args) => ( +export const Danger = (args) => ( Normal {/* Test Anchor tag to match exactly as button */} diff --git a/ui/components/component-library/button-secondary/button-secondary.test.js b/ui/components/component-library/button-secondary/button-secondary.test.js index 61f910ab9..8b2b14896 100644 --- a/ui/components/component-library/button-secondary/button-secondary.test.js +++ b/ui/components/component-library/button-secondary/button-secondary.test.js @@ -14,6 +14,7 @@ describe('ButtonSecondary', () => { expect(getByText('Button Secondary')).toBeDefined(); expect(container.querySelector('button')).toBeDefined(); expect(getByTestId('button-secondary')).toHaveClass('mm-button'); + expect(container).toMatchSnapshot(); }); it('should render anchor element correctly', () => { @@ -68,7 +69,7 @@ describe('ButtonSecondary', () => { ); }); - it('should render with different types', () => { + it('should render as danger', () => { const { getByTestId } = render( <> diff --git a/ui/components/component-library/button-secondary/index.js b/ui/components/component-library/button-secondary/index.js index 6b1d2869f..bccde44c2 100644 --- a/ui/components/component-library/button-secondary/index.js +++ b/ui/components/component-library/button-secondary/index.js @@ -1 +1,2 @@ export { ButtonSecondary } from './button-secondary'; +export { BUTTON_SECONDARY_SIZES } from './button-secondary.constants'; diff --git a/ui/components/component-library/button/README.mdx b/ui/components/component-library/button/README.mdx new file mode 100644 index 000000000..9c6c18f0d --- /dev/null +++ b/ui/components/component-library/button/README.mdx @@ -0,0 +1,178 @@ +import { Story, Canvas, ArgsTable } from '@storybook/addon-docs'; +import { Button } from './button'; + +# Button + +The `Button` is used for user actions it unifies `ButtonPrimary`, `ButtonSecondary` and `ButtonLink` + + + + + +## Props + +The `Button` accepts all props below as well as all [ButtonPrimary](/ui-components-component-library-button-primary-button-primary-stories-js--default-story), [ButtonSecondary](/ui-components-component-library-button-secondary-button-secondary-stories-js--default-story), [ButtonLink](/ui-components-component-library-button-link-button-link-stories-js--default-story), and [Box](/docs/ui-components-ui-box-box-stories-js--default-story#props) component props + + + +### Type + +Use the `type` prop and the `BUTTON_TYPES` object from `./button.constants.js` to change the `Button` type. + +Possible types include: + +- `BUTTON_TYPES.PRIMARY` +- `BUTTON_TYPES.SECONDARY` +- `BUTTON_TYPES.LINK` + + + + + +```jsx +import { Button, BUTTON_TYPES } from '../ui/component-library/button'; + + + + +``` + +### Size + +Use the `size` prop and the `SIZES` object from `./ui/helpers/constants/design-system.js` to change the size of `Button`. Defaults to `SIZES.MD` + +Optional: `BUTTON_SIZES` from `./button` object can be used instead of `SIZES`. + +Possible sizes include: + +- `SIZES.AUTO` inherits the font-size of the parent element. +- `SIZES.SM` 32px +- `SIZES.MD` 40px +- `SIZES.LG` 48px + + + + + +```jsx +import { SIZES } from '../../../helpers/constants/design-system'; +import { Button } from '../ui/component-library/button/button/button'; + + + +``` + +### Href + +When an `href` is passed the tag element will switch to an `anchor`(`a`) tag. + + + + + +```jsx +import { Button } from '../ui/component-library/button/button/button'; + +; +``` + +### Block + +Use boolean `block` prop to quickly enable a full width block button + + + + + +```jsx +import { DISPLAY } from '../../../helpers/constants/design-system'; +import { Button } from '../ui/component-library'; + + + +``` + +### As + +Use the `as` box prop to change the element of `Button`. Defaults to `button`. + +When an `href` prop is passed it will change the element to an anchor(`a`) tag. + +Button `as` options: + +- `button` +- `a` + + + + + +```jsx +import { Button } from '../ui/component-library'; + + + + +``` + +### Disabled + +Use the boolean `disabled` prop to disable button + + + + + +```jsx +import { Button } from '../ui/component-library'; + +; +``` + +### Loading + +Use the boolean `loading` prop to set loading spinner + + + + + +```jsx +import { Button } from '../ui/component-library'; + +; +``` + +### Icon + +Use the `icon` prop and the `ICON_NAMES` object from `./ui/components/component-library/icon` to select icon. + + + + + +```jsx +import { Button } from '../ui/component-library'; +import { ICON_NAMES } from '../icon'; + +; +``` diff --git a/ui/components/component-library/button/__snapshots__/button.test.js.snap b/ui/components/component-library/button/__snapshots__/button.test.js.snap new file mode 100644 index 000000000..a2da87852 --- /dev/null +++ b/ui/components/component-library/button/__snapshots__/button.test.js.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Button should render button element correctly 1`] = ` +
    + +
    +`; + +exports[`Button should render with different button types 1`] = ` +
    + + + +
    +`; diff --git a/ui/components/component-library/button/button.constants.js b/ui/components/component-library/button/button.constants.js new file mode 100644 index 000000000..803673f31 --- /dev/null +++ b/ui/components/component-library/button/button.constants.js @@ -0,0 +1,14 @@ +import { SIZES } from '../../../helpers/constants/design-system'; + +export const BUTTON_SIZES = { + SM: SIZES.SM, + MD: SIZES.MD, + LG: SIZES.LG, + AUTO: SIZES.AUTO, +}; + +export const BUTTON_TYPES = { + PRIMARY: 'primary', + SECONDARY: 'secondary', + LINK: 'link', +}; diff --git a/ui/components/component-library/button/button.js b/ui/components/component-library/button/button.js new file mode 100644 index 000000000..eaaa5486b --- /dev/null +++ b/ui/components/component-library/button/button.js @@ -0,0 +1,34 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { ButtonPrimary } from '../button-primary'; +import { ButtonSecondary } from '../button-secondary'; +import { ButtonLink } from '../button-link'; + +import { BUTTON_TYPES } from './button.constants'; + +export const Button = ({ type, ...props }) => { + switch (type) { + case BUTTON_TYPES.PRIMARY: + return ; + case BUTTON_TYPES.SECONDARY: + return ; + case BUTTON_TYPES.LINK: + return ; + default: + return ; + } +}; + +Button.propTypes = { + /** + * Select the type of Button. + * Possible values could be 'BUTTON_TYPES.PRIMARY', 'BUTTON_TYPES.SECONDARY', 'BUTTON_TYPES.LINK' + * Button will default to `BUTTON_TYPES.PRIMARY` + */ + type: PropTypes.oneOf(Object.values(BUTTON_TYPES)), + /** + * Button accepts all the props from ButtonPrimary (same props as ButtonSecondary & ButtonLink) + */ + ...ButtonPrimary.propTypes, +}; diff --git a/ui/components/component-library/button/button.stories.js b/ui/components/component-library/button/button.stories.js new file mode 100644 index 000000000..cce693001 --- /dev/null +++ b/ui/components/component-library/button/button.stories.js @@ -0,0 +1,212 @@ +import React from 'react'; +import { + ALIGN_ITEMS, + DISPLAY, + FLEX_DIRECTION, + SIZES, + TEXT, +} from '../../../helpers/constants/design-system'; +import { ICON_NAMES } from '../icon'; +import { BUTTON_LINK_SIZES } from '../button-link/button-link.constants'; +import Box from '../../ui/box/box'; +import { Text } from '../text'; +import README from './README.mdx'; +import { Button, BUTTON_TYPES } from '.'; + +const marginSizeControlOptions = [ + undefined, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 'auto', +]; + +export default { + title: 'Components/ComponentLibrary/Button', + id: __filename, + component: Button, + parameters: { + docs: { + page: README, + }, + controls: { sort: 'alpha' }, + }, + argTypes: { + as: { + control: 'select', + options: ['button', 'a'], + }, + block: { + control: 'boolean', + }, + children: { + control: 'text', + }, + className: { + control: 'text', + }, + danger: { + control: 'boolean', + }, + disabled: { + control: 'boolean', + }, + href: { + control: 'text', + }, + icon: { + control: 'select', + options: Object.values(ICON_NAMES), + }, + iconPositionRight: { + control: 'boolean', + }, + iconProps: { + control: 'object', + }, + loading: { + control: 'boolean', + }, + size: { + control: 'select', + options: Object.values(BUTTON_LINK_SIZES), + }, + type: { + options: Object.values(BUTTON_TYPES), + control: 'select', + }, + marginTop: { + options: marginSizeControlOptions, + control: 'select', + table: { category: 'box props' }, + }, + marginRight: { + options: marginSizeControlOptions, + control: 'select', + table: { category: 'box props' }, + }, + marginBottom: { + options: marginSizeControlOptions, + control: 'select', + table: { category: 'box props' }, + }, + marginLeft: { + options: marginSizeControlOptions, + control: 'select', + table: { category: 'box props' }, + }, + }, + args: { + children: 'Button', + }, +}; + +export const DefaultStory = (args) => + + +
    +); + +export const Size = (args) => ( + <> + + + + + + + {' '} + inherits the font-size of the parent element. Auto size only used for + ButtonLink. + + +); + +export const Danger = (args) => ( + + + {/* Test Anchor tag to match exactly as button */} + + +); + +export const Href = (args) => ; + +Href.args = { + href: '/metamask', +}; + +export const Block = (args) => ( + <> + + + +); + +export const As = (args) => ( + + + + +); + +export const Disabled = (args) => ; + +Disabled.args = { + disabled: true, +}; + +export const Loading = (args) => ; + +Loading.args = { + loading: true, +}; + +export const Icon = (args) => ( + +); diff --git a/ui/components/component-library/button/button.test.js b/ui/components/component-library/button/button.test.js new file mode 100644 index 000000000..8f4f288e8 --- /dev/null +++ b/ui/components/component-library/button/button.test.js @@ -0,0 +1,157 @@ +/* eslint-disable jest/require-top-level-describe */ +import { render } from '@testing-library/react'; +import React from 'react'; +import { BUTTON_SIZES, BUTTON_TYPES } from './button.constants'; +import { Button } from './button'; + +describe('Button', () => { + it('should render button element correctly', () => { + const { getByTestId, getByText, container } = render( + , + ); + expect(getByText('Button')).toBeDefined(); + expect(container.querySelector('button')).toBeDefined(); + expect(getByTestId('button')).toHaveClass('mm-button'); + expect(container).toMatchSnapshot(); + }); + + it('should render anchor element correctly', () => { + const { getByTestId, container } = render( + , + ); + expect(getByTestId('button')).toHaveClass('mm-button'); + const anchor = container.getElementsByTagName('a').length; + expect(anchor).toBe(1); + }); + + it('should render anchor element correctly by href only being passed', () => { + const { getByTestId, container } = render( + , + ); + expect(getByTestId('button')).toHaveClass('mm-button'); + const anchor = container.getElementsByTagName('a').length; + expect(anchor).toBe(1); + }); + + it('should render button as block', () => { + const { getByTestId } = render( + + + , + ); + expect(getByTestId(BUTTON_TYPES.PRIMARY)).toHaveClass( + `mm-button-${BUTTON_TYPES.PRIMARY}`, + ); + expect(getByTestId(BUTTON_TYPES.SECONDARY)).toHaveClass( + `mm-button-${BUTTON_TYPES.SECONDARY}`, + ); + expect(getByTestId(BUTTON_TYPES.LINK)).toHaveClass( + `mm-button-${BUTTON_TYPES.LINK}`, + ); + expect(container).toMatchSnapshot(); + }); + + it('should render with different size classes', () => { + const { getByTestId } = render( + <> + + + + + , + ); + expect(getByTestId(BUTTON_SIZES.AUTO)).toHaveClass( + `mm-button--size-${BUTTON_SIZES.AUTO}`, + ); + expect(getByTestId(BUTTON_SIZES.SM)).toHaveClass( + `mm-button--size-${BUTTON_SIZES.SM}`, + ); + expect(getByTestId(BUTTON_SIZES.MD)).toHaveClass( + `mm-button--size-${BUTTON_SIZES.MD}`, + ); + expect(getByTestId(BUTTON_SIZES.LG)).toHaveClass( + `mm-button--size-${BUTTON_SIZES.LG}`, + ); + }); + + it('should render with added classname', () => { + const { getByTestId } = render( + , + ); + expect(getByTestId('classname')).toHaveClass('mm-button--test'); + }); + + it('should render with different button states', () => { + const { getByTestId } = render( + <> + + + , + ); + expect(getByTestId('loading')).toHaveClass(`mm-button--loading`); + expect(getByTestId('disabled')).toHaveClass(`mm-button--disabled`); + }); + it('should render with icon', () => { + const { getByTestId } = render( + , + ); + + expect(getByTestId('base-button-icon')).toBeDefined(); + }); +}); + +it('should render as danger', () => { + const { getByTestId } = render( + <> + + , + ); + + expect(getByTestId('danger')).toHaveClass('mm-button-primary--type-danger'); +}); diff --git a/ui/components/component-library/button/index.js b/ui/components/component-library/button/index.js new file mode 100644 index 000000000..1a9c9819b --- /dev/null +++ b/ui/components/component-library/button/index.js @@ -0,0 +1,2 @@ +export { Button } from './button'; +export { BUTTON_TYPES } from './button.constants'; diff --git a/ui/components/component-library/component-library-components.scss b/ui/components/component-library/component-library-components.scss index af5003d1e..898d3945d 100644 --- a/ui/components/component-library/component-library-components.scss +++ b/ui/components/component-library/component-library-components.scss @@ -1,16 +1,28 @@ -/** Please import your files in alphabetical order **/ +/** +* Please import your styles in order of atomicity. +* The most atomic styles should be imported first. +* This will help improve specificity and reduce the chance of +* unintended overrides. +**/ +// Atoms +@import 'text/text'; +@import 'icon/icon'; +@import 'label/label'; +@import 'tag/tag'; +@import 'base-avatar/base-avatar'; @import 'avatar-account/avatar-account'; @import 'avatar-favicon/avatar-favicon'; @import 'avatar-network/avatar-network'; @import 'avatar-token/avatar-token'; @import 'avatar-with-badge/avatar-with-badge'; -@import 'base-avatar/base-avatar'; @import 'button-base/button-base'; +@import 'button-icon/button-icon'; @import 'button-link/button-link'; @import 'button-primary/button-primary'; @import 'button-secondary/button-secondary'; -@import 'icon/icon'; -@import 'tag/tag'; -@import 'text/text'; +// Molecules +@import 'picker-network/picker-network'; +@import 'tag-url/tag-url'; @import 'text-field/text-field'; @import 'text-field-base/text-field-base'; +@import 'text-field-search/text-field-search'; diff --git a/ui/components/component-library/help-text/README.mdx b/ui/components/component-library/help-text/README.mdx new file mode 100644 index 000000000..03c2cf50a --- /dev/null +++ b/ui/components/component-library/help-text/README.mdx @@ -0,0 +1,84 @@ +import { Story, Canvas, ArgsTable } from '@storybook/addon-docs'; + +import { HelpText } from './help-text'; + +# HelpText + +The `HelpText` is intended to be used as the help or error text under a form element + + + + + +## Props + +The `HelpText` accepts all props below as well as all [Text](/docs/ui-components-component-library-text-text-stories-js--default-story#props) and [Box](/docs/ui-components-ui-box-box-stories-js--default-story#props) component props. + + + +### Children + +The `children` of the `HelpText` can be plain text or react nodes. + + + + + +```jsx +import { SIZES, COLORS } from '../../../helpers/constants/design-system'; +import { Icon, ICON_NAMES } from '../../ui/components/component-library'; +import { HelpText } from '../../ui/components/component-library'; + +Plain text + + Text and icon + + +``` + +### Error + +Use the `error` prop to show the `HelpText` in error state. + + + + + +```jsx +import { HelpText } from '../../ui/components/component-library'; + +This HelpText in error state; +``` + +### Color + +It may be useful to change the color of the `HelpText`. Use the `color` prop and the `COLORS` object to change the color of the `HelpText`. Defaults to `COLORS.TEXT_DEFAULT`. + + + + + +```jsx +import { COLORS } from '../../../helpers/constants/design-system'; +import { HelpText } from '../../ui/components/component-library'; + + + + The HelpText default color is COLORS.TEXT_DEFAULT + + + This HelpText color is COLORS.INFO_DEFAULT + + + This HelpText color is COLORS.WARNING_DEFAULT + + + This HelpText color is COLORS.SUCCESS_DEFAULT + +; +``` diff --git a/ui/components/component-library/help-text/help-text.js b/ui/components/component-library/help-text/help-text.js new file mode 100644 index 000000000..2eeb66841 --- /dev/null +++ b/ui/components/component-library/help-text/help-text.js @@ -0,0 +1,54 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; + +import { + COLORS, + TEXT, + TEXT_COLORS, +} from '../../../helpers/constants/design-system'; + +import { Text } from '../text'; + +export const HelpText = ({ + error, + color = COLORS.TEXT_DEFAULT, + className, + children, + ...props +}) => ( + + {children} + +); + +HelpText.propTypes = { + /** + * If the HelperText should display in error state + * Will override the color prop if true + */ + error: PropTypes.bool, + /** + * The color of the HelpText will be overridden if error is true + * Defaults to COLORS.TEXT_DEFAULT + */ + color: PropTypes.oneOf(Object.values(TEXT_COLORS)), + /** + * The content of the help-text + */ + children: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + /** + * Additional classNames to be added to the HelpText component + */ + className: PropTypes.string, + /** + * HelpText also accepts all Text and Box props + */ + ...Text.propTypes, +}; diff --git a/ui/components/component-library/help-text/help-text.stories.js b/ui/components/component-library/help-text/help-text.stories.js new file mode 100644 index 000000000..07853b56c --- /dev/null +++ b/ui/components/component-library/help-text/help-text.stories.js @@ -0,0 +1,88 @@ +import React from 'react'; +import { + DISPLAY, + FLEX_DIRECTION, + COLORS, + TEXT_COLORS, + SIZES, +} from '../../../helpers/constants/design-system'; + +import Box from '../../ui/box'; +import { Icon, ICON_NAMES } from '../icon'; + +import { HelpText } from './help-text'; + +import README from './README.mdx'; + +export default { + title: 'Components/ComponentLibrary/HelpText', + id: __filename, + component: HelpText, + parameters: { + docs: { + page: README, + }, + }, + argTypes: { + children: { + control: 'text', + }, + className: { + control: 'text', + }, + error: { + control: 'boolean', + }, + color: { + control: 'select', + options: Object.values(TEXT_COLORS), + }, + }, + args: { + children: 'Help text', + }, +}; + +const Template = (args) => ; + +export const DefaultStory = Template.bind({}); +DefaultStory.storyName = 'Default'; + +export const Children = (args) => ( + + Plain text + + Text and icon + + + +); + +export const ErrorStory = (args) => ( + + This HelpText in error state + +); +ErrorStory.storyName = 'Error'; + +export const Color = (args) => ( + + + This HelpText default color is COLORS.TEXT_DEFAULT + + + This HelpText color is COLORS.INFO_DEFAULT + + + This HelpText color is COLORS.WARNING_DEFAULT + + + This HelpText color is COLORS.SUCCESS_DEFAULT + + +); diff --git a/ui/components/component-library/help-text/help-text.test.js b/ui/components/component-library/help-text/help-text.test.js new file mode 100644 index 000000000..9391ed0cb --- /dev/null +++ b/ui/components/component-library/help-text/help-text.test.js @@ -0,0 +1,52 @@ +/* eslint-disable jest/require-top-level-describe */ +import { render } from '@testing-library/react'; +import React from 'react'; +import { COLORS } from '../../../helpers/constants/design-system'; +import { Icon, ICON_NAMES } from '../icon'; + +import { HelpText } from './help-text'; + +describe('HelpText', () => { + it('should render with text inside the HelpText', () => { + const { getByText } = render(help text); + expect(getByText('help text')).toBeDefined(); + }); + it('should render with react nodes inside the HelpText', () => { + const { getByText, getByTestId } = render( + + help text + , + ); + expect(getByText('help text')).toBeDefined(); + expect(getByTestId('icon')).toBeDefined(); + }); + it('should render with and additional className', () => { + const { getByText } = render( + help text, + ); + expect(getByText('help text')).toBeDefined(); + expect(getByText('help text')).toHaveClass('test-class'); + }); + it('should render with error state', () => { + const { getByText } = render( + <> + error + , + ); + expect(getByText('error')).toHaveClass('text--color-error-default'); + }); + it('should render with different colors', () => { + const { getByText } = render( + <> + default + warning + success + info + , + ); + expect(getByText('default')).toHaveClass('text--color-text-default'); + expect(getByText('warning')).toHaveClass('text--color-warning-default'); + expect(getByText('success')).toHaveClass('text--color-success-default'); + expect(getByText('info')).toHaveClass('text--color-info-default'); + }); +}); diff --git a/ui/components/component-library/help-text/index.js b/ui/components/component-library/help-text/index.js new file mode 100644 index 000000000..cbc88e159 --- /dev/null +++ b/ui/components/component-library/help-text/index.js @@ -0,0 +1 @@ +export { HelpText } from './help-text'; diff --git a/ui/components/component-library/icon/README.mdx b/ui/components/component-library/icon/README.mdx index 950e2d07b..d18913802 100644 --- a/ui/components/component-library/icon/README.mdx +++ b/ui/components/component-library/icon/README.mdx @@ -23,7 +23,7 @@ Use the `name` prop and the `ICON_NAMES` object to change the icon. Use the [IconSearch](/ui-components-component-library-icon-icon-stories-js--name) story to find the icon you want to use. ```jsx -import { Icon, ICON_NAMES } from '../ui/component-library'; +import { Icon, ICON_NAMES } from '../../ui/components/component-library'; @@ -55,7 +55,7 @@ Possible sizes include: ```jsx import { SIZES } from '../../../helpers/constants/design-system'; -import { Icon, ICON_NAMES } from '../ui/component-library'; +import { Icon, ICON_NAMES } from '../../ui/components/component-library'; @@ -79,7 +79,7 @@ Use the `color` prop and the `COLORS` object from `./ui/helpers/constants/design ```jsx import { COLORS } from '../../../helpers/constants/design-system'; -import { Icon, ICON_NAMES } from '../ui/component-library'; +import { Icon, ICON_NAMES } from '../../ui/components/component-library'; diff --git a/ui/components/component-library/index.js b/ui/components/component-library/index.js new file mode 100644 index 000000000..6927e2334 --- /dev/null +++ b/ui/components/component-library/index.js @@ -0,0 +1,25 @@ +export { AvatarAccount } from './avatar-account'; +export { AvatarFavicon } from './avatar-favicon'; +export { AvatarNetwork } from './avatar-network'; +export { AvatarToken } from './avatar-token'; +export { AvatarWithBadge } from './avatar-with-badge'; +export { BaseAvatar } from './base-avatar'; +export { Button } from './button'; +export { ButtonBase } from './button-base'; +export { ButtonIcon } from './button-icon'; +export { ButtonLink } from './button-link'; +export { ButtonPrimary } from './button-primary'; +export { ButtonSecondary } from './button-secondary'; +export { HelpText } from './help-text'; +export { Icon, ICON_NAMES } from './icon'; +export { Label } from './label'; +export { PickerNetwork } from './picker-network'; +export { Tag } from './tag'; +export { TagUrl } from './tag-url'; +export { Text } from './text'; +export { TextField } from './text-field'; +export { + TextFieldBase, + TEXT_FIELD_BASE_SIZES, + TEXT_FIELD_BASE_TYPES, +} from './text-field-base'; diff --git a/ui/components/component-library/label/README.mdx b/ui/components/component-library/label/README.mdx new file mode 100644 index 000000000..5f83c96d5 --- /dev/null +++ b/ui/components/component-library/label/README.mdx @@ -0,0 +1,95 @@ +import { Story, Canvas, ArgsTable } from '@storybook/addon-docs'; + +import { Label } from './label'; + +# Label + +The `Label` is a component used to label form inputs. + + + + + +## Props + +The `Label` accepts all props below as well as all [Text](/docs/ui-components-component-library-text-text-stories-js--default-story#props) and [Box](/docs/ui-components-ui-box-box-stories-js--default-story#props) component props. + + + +### Children + +The `children` of the label can be text or a react node. + + + + + +```jsx +import { DISPLAY, ALIGN_ITEMS, FLEX_DIRECTION, SIZES, COLORS } from '../../../helpers/constants/design-system'; +import { Icon, ICON_NAMES } from '../../ui/components/component-library'; +import { Label } from '../../ui/components/component-library'; +import { TextFieldBase } from '../../ui/components/component-library'; + + + + +``` + +### Html For + +Use the `htmlFor` prop to allow the `Label` to focus on an input with the same id when clicked. The cursor will also change to a `pointer` when the `htmlFor` has a value + + + + + +```jsx +import { TextFieldBase } from '../../ui/components/component-library'; +import { Label } from '../../ui/components/component-library'; + + + +``` + +### Required + +Use the `required` prop to add a required red asterisk next to the `children` of the `Label`. Note the required asterisk will always render after the `children`. + + + + + +```jsx +import { Label } from '../../ui/components/component-library'; + +; +``` + +### Disabled + +Use the `disabled` prop to set the `Label` in disabled state + + + + + +```jsx +import { Label } from '../../ui/components/component-library'; + +; +``` diff --git a/ui/components/component-library/label/index.js b/ui/components/component-library/label/index.js new file mode 100644 index 000000000..f28365d90 --- /dev/null +++ b/ui/components/component-library/label/index.js @@ -0,0 +1 @@ +export { Label } from './label'; diff --git a/ui/components/component-library/label/label.js b/ui/components/component-library/label/label.js new file mode 100644 index 000000000..aece4d958 --- /dev/null +++ b/ui/components/component-library/label/label.js @@ -0,0 +1,73 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; + +import { + COLORS, + FONT_WEIGHT, + TEXT, + DISPLAY, + ALIGN_ITEMS, +} from '../../../helpers/constants/design-system'; +import { Text } from '../text'; + +export const Label = ({ + htmlFor, + required, + disabled, + className, + children, + ...props +}) => ( + + {children} + {required && ( + + )} + +); + +Label.propTypes = { + /** + * The content of the label + */ + children: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + /** + * The id of the input associated with the label + */ + htmlFor: PropTypes.string, + /** + * If true the label will display as required + */ + required: PropTypes.bool, + /** + * Whether the label is disabled or not + */ + disabled: PropTypes.bool, + /** + * Additional classNames to be added to the label component + */ + className: PropTypes.string, +}; diff --git a/ui/components/component-library/label/label.scss b/ui/components/component-library/label/label.scss new file mode 100644 index 000000000..425048540 --- /dev/null +++ b/ui/components/component-library/label/label.scss @@ -0,0 +1,11 @@ +.mm-label { + --label-opacity-disabled: 0.5; // TODO: replace with design token + + &--html-for { + cursor: pointer; + } + + &--disabled { + opacity: var(--label-opacity-disabled); + } +} diff --git a/ui/components/component-library/label/label.stories.js b/ui/components/component-library/label/label.stories.js new file mode 100644 index 000000000..0d3048bbc --- /dev/null +++ b/ui/components/component-library/label/label.stories.js @@ -0,0 +1,112 @@ +import React, { useState } from 'react'; +import { + DISPLAY, + FLEX_DIRECTION, + COLORS, + SIZES, + ALIGN_ITEMS, +} from '../../../helpers/constants/design-system'; + +import Box from '../../ui/box'; +import { Icon, ICON_NAMES } from '../icon'; +import { TextFieldBase } from '../text-field-base'; + +import { Label } from './label'; + +import README from './README.mdx'; + +export default { + title: 'Components/ComponentLibrary/Label', + id: __filename, + component: Label, + parameters: { + docs: { + page: README, + }, + }, + argTypes: { + htmlFor: { + control: 'text', + }, + required: { + control: 'boolean', + }, + disabled: { + control: 'boolean', + }, + children: { + control: 'text', + }, + className: { + control: 'text', + }, + }, + args: { + children: 'Label', + }, +}; + +const Template = (args) =>
    } - showClear - />, - ); - const textField = getByRole('textbox'); - fireEvent.change(textField, { target: { value: 'text value' } }); - expect(textField.value).toBe('text value'); - fireEvent.click(getByTestId('clear-button')); - expect(textField.value).toBe(''); + it('should fire onClick event when passed to clearButtonOnClick when clear button is clicked', async () => { + // As showClearButton is intended to be used with a controlled input we need to use renderControlledInput + const fn = jest.fn(); + const { user, getByRole } = renderControlledInput(TextField, { + showClearButton: true, + clearButtonOnClick: fn, + }); + await user.type(getByRole('textbox'), 'test value'); + await user.click(getByRole('button', { name: /Clear/u })); + expect(fn).toHaveBeenCalledTimes(1); }); - it('should fire onClear event when passed to onClear prop', () => { - const onClear = jest.fn(); - const { getByRole, getByTestId } = render( - , - ); - const textField = getByRole('textbox'); - fireEvent.change(textField, { target: { value: 'text value' } }); - expect(textField.value).toBe('text value'); - fireEvent.click(getByTestId('clear-button')); - expect(onClear).toHaveBeenCalledTimes(1); - }); - it('should fire clearButtonProps.onClick event when passed to clearButtonProps.onClick prop', () => { - const onClear = jest.fn(); - const onClick = jest.fn(); - const { getByRole, getByTestId } = render( - , - ); - const textField = getByRole('textbox'); - fireEvent.change(textField, { target: { value: 'text value' } }); - expect(textField.value).toBe('text value'); - fireEvent.click(getByTestId('clear-button')); - expect(onClear).toHaveBeenCalledTimes(1); - expect(onClick).toHaveBeenCalledTimes(1); + it('should fire onClick event when passed to clearButtonProps.onClick prop', async () => { + // As showClearButton is intended to be used with a controlled input we need to use renderControlledInput + const fn = jest.fn(); + const { user, getByRole } = renderControlledInput(TextField, { + showClearButton: true, + clearButtonProps: { onClick: fn }, + }); + await user.type(getByRole('textbox'), 'test value'); + await user.click(getByRole('button', { name: /Clear/u })); + expect(fn).toHaveBeenCalledTimes(1); }); it('should be able to accept inputProps', () => { - const { getByRole } = render( + const { getByTestId } = render( , ); - const textField = getByRole('textbox'); - expect(textField).toBeDefined(); + expect(getByTestId('text-field')).toBeDefined(); }); }); diff --git a/ui/components/component-library/text/README.mdx b/ui/components/component-library/text/README.mdx index 4f3650ebf..c06c2364f 100644 --- a/ui/components/component-library/text/README.mdx +++ b/ui/components/component-library/text/README.mdx @@ -25,7 +25,7 @@ Use the `variant` prop and the `TEXT` object from `./ui/helpers/constants/design ```jsx -import { Text } from '../../ui/component-library/text'; +import { Text } from '../../ui/components/component-library'; import { TEXT } from '../../../helpers/constants/design-system'; display-md @@ -47,7 +47,7 @@ Use the `color` prop and the `COLORS` object from `./ui/helpers/constants/design ```jsx -import { Text } from '../../ui/component-library/text'; +import { Text } from '../../ui/components/component-library'; import { COLORS } from '../../../helpers/constants/design-system'; @@ -104,7 +104,7 @@ Use the `fontWeight` prop and the `FONT_WEIGHT` object from `./ui/helpers/consta ```jsx -import { Text } from '../../ui/component-library/text'; +import { Text } from '../../ui/components/component-library'; import { FONT_WEIGHT } from '../../../helpers/constants/design-system'; @@ -130,7 +130,7 @@ Use the `fontStyle` prop and the `FONT_STYLE` object from `./ui/helpers/constant ```jsx -import { Text } from '../../ui/component-library/text'; +import { Text } from '../../ui/components/component-library'; import { FONT_STYLE } from '../../../helpers/constants/design-system'; @@ -150,7 +150,7 @@ Use the `textTransform` prop and the `TEXT_TRANSFORM` object from `./ui/helpers/ ```jsx -import { Text } from '../../ui/component-library/text'; +import { Text } from '../../ui/components/component-library'; import { TEXT_TRANSFORM } from '../../../helpers/constants/design-system'; @@ -173,7 +173,7 @@ Use the `textAlign` prop and the `TEXT_ALIGN` object from `./ui/helpers/constant ```jsx -import { Text } from '../../ui/component-library/text'; +import { Text } from '../../ui/components/component-library'; import { TEXT_ALIGN } from '../../../helpers/constants/design-system'; @@ -202,7 +202,7 @@ Use the `overflowWrap` prop and the `OVERFLOW_WRAP` object from `./ui/helpers/co ```jsx -import { Text } from '../../ui/component-library/text'; +import { Text } from '../../ui/components/component-library'; import { OVERFLOW_WRAP } from '../../../helpers/constants/design-system';
    ```jsx -import { Text } from '../../ui/component-library/text'; +import { Text } from '../../ui/components/component-library';
    ```jsx -import { Text } from '../../ui/component-library/text'; +import { Text } from '../../ui/components/component-library'; dd div diff --git a/ui/components/ui/box/README.mdx b/ui/components/ui/box/README.mdx index ff443c1f6..23b1a76fc 100644 --- a/ui/components/ui/box/README.mdx +++ b/ui/components/ui/box/README.mdx @@ -209,6 +209,7 @@ import Box from '../ui/box'; BORDER_RADIUS.LG 8px BORDER_RADIUS.XL 12px BORDER_RADIUS.PILL 9999px +BORDER_RADIUS.FULL 50% ``` ### Responsive Props diff --git a/ui/components/ui/box/box.scss b/ui/components/ui/box/box.scss index 63f9fb9fa..b29da6bdb 100644 --- a/ui/components/ui/box/box.scss +++ b/ui/components/ui/box/box.scss @@ -157,6 +157,10 @@ $attributesToApplyExtraProperties: margin; border-radius: 9999px; } + &--rounded-full { + border-radius: 50%; + } + // breakpoint classes @each $breakpoint, $min-width in $screen-sizes-map { @media screen and (min-width: $min-width) { diff --git a/ui/components/ui/box/box.stories.js b/ui/components/ui/box/box.stories.js index 6d63f186e..cd9712269 100644 --- a/ui/components/ui/box/box.stories.js +++ b/ui/components/ui/box/box.stories.js @@ -482,75 +482,90 @@ export const BorderColor = () => { export const BorderRadius = () => { return ( - + <> - BORDER_RADIUS.NONE 0px + + BORDER_RADIUS.NONE 0px + + + BORDER_RADIUS.XS 2px + + + BORDER_RADIUS.SM 4px + + + BORDER_RADIUS.MD 6px + + + BORDER_RADIUS.LG 8px + + + BORDER_RADIUS.XL 12px + + + BORDER_RADIUS.PILL 9999px + - BORDER_RADIUS.XS 2px + BORDER_RADIUS.FULL 50% - - BORDER_RADIUS.SM 4px - - - BORDER_RADIUS.MD 6px - - - BORDER_RADIUS.LG 8px - - - BORDER_RADIUS.XL 12px - - - BORDER_RADIUS.PILL 9999px - - + ); }; diff --git a/ui/components/ui/box/box.test.js b/ui/components/ui/box/box.test.js index 8e8534589..1f8a0beac 100644 --- a/ui/components/ui/box/box.test.js +++ b/ui/components/ui/box/box.test.js @@ -287,6 +287,7 @@ describe('Box', () => { border radius lg border radius xl border radius pill + border radius full border radius none , ); @@ -297,6 +298,7 @@ describe('Box', () => { expect(getByText('border radius lg')).toHaveClass('box--rounded-lg'); expect(getByText('border radius xl')).toHaveClass('box--rounded-xl'); expect(getByText('border radius pill')).toHaveClass('box--rounded-pill'); + expect(getByText('border radius full')).toHaveClass('box--rounded-full'); expect(getByText('border radius none')).toHaveClass('box--rounded-none'); }); it('should render the Box with the responsive borderRadius classes', () => { @@ -317,6 +319,7 @@ describe('Box', () => { BORDER_RADIUS.XL, BORDER_RADIUS.PILL, BORDER_RADIUS.NONE, + BORDER_RADIUS.FULL, ]} > Border radius set 2 @@ -341,6 +344,9 @@ describe('Box', () => { expect(getByText('Border radius set 2')).toHaveClass( 'box--md:rounded-none', ); + expect(getByText('Border radius set 2')).toHaveClass( + 'box--lg:rounded-full', + ); }); }); describe('display, gap, flexDirection, flexWrap, alignItems, justifyContent', () => { diff --git a/ui/components/ui/button-group/__snapshots__/button-group-component.test.js.snap b/ui/components/ui/button-group/__snapshots__/button-group-component.test.js.snap new file mode 100644 index 000000000..7e8116227 --- /dev/null +++ b/ui/components/ui/button-group/__snapshots__/button-group-component.test.js.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ButtonGroup Component should match snapshot 1`] = ` +
    +
    + +
    +
    +`; diff --git a/ui/components/ui/button-group/button-group-component.test.js b/ui/components/ui/button-group/button-group-component.test.js index 64aef67b4..2aaccad78 100644 --- a/ui/components/ui/button-group/button-group-component.test.js +++ b/ui/components/ui/button-group/button-group-component.test.js @@ -1,130 +1,30 @@ import React from 'react'; -import { shallow } from 'enzyme'; -import sinon from 'sinon'; -import ButtonGroup from './button-group.component'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import ButtonGroup from '.'; describe('ButtonGroup Component', () => { - let wrapper; - - const childButtonSpies = { - onClick: sinon.spy(), + const props = { + defaultActiveButtonIndex: 1, + disabled: false, + className: 'someClassName', + style: { + color: 'red', + }, }; const mockButtons = [ - , - , - , + , + , ]; - beforeAll(() => { - sinon.spy(ButtonGroup.prototype, 'handleButtonClick'); - sinon.spy(ButtonGroup.prototype, 'renderButtons'); - }); - - beforeEach(() => { - wrapper = shallow( - - {mockButtons} - , + it('should match snapshot', () => { + const { container } = renderWithProvider( + {mockButtons}, ); - }); - afterEach(() => { - childButtonSpies.onClick.resetHistory(); - ButtonGroup.prototype.handleButtonClick.resetHistory(); - ButtonGroup.prototype.renderButtons.resetHistory(); - }); - - afterAll(() => { - sinon.restore(); - }); - - describe('componentDidUpdate', () => { - it('should set the activeButtonIndex to the updated newActiveButtonIndex', () => { - expect(wrapper.state('activeButtonIndex')).toStrictEqual(1); - wrapper.setProps({ newActiveButtonIndex: 2 }); - expect(wrapper.state('activeButtonIndex')).toStrictEqual(2); - }); - - it('should not set the activeButtonIndex to an updated newActiveButtonIndex that is not a number', () => { - expect(wrapper.state('activeButtonIndex')).toStrictEqual(1); - wrapper.setProps({ newActiveButtonIndex: null }); - expect(wrapper.state('activeButtonIndex')).toStrictEqual(1); - }); - }); - - describe('handleButtonClick', () => { - it('should set the activeButtonIndex', () => { - expect(wrapper.state('activeButtonIndex')).toStrictEqual(1); - wrapper.instance().handleButtonClick(2); - expect(wrapper.state('activeButtonIndex')).toStrictEqual(2); - }); - }); - - describe('renderButtons', () => { - it('should render a button for each child', () => { - const childButtons = wrapper.find('.button-group__button'); - expect(childButtons).toHaveLength(3); - }); - - it('should render the correct button with an active state', () => { - const childButtons = wrapper.find('.button-group__button'); - const activeChildButton = wrapper.find('.button-group__button--active'); - expect(childButtons.get(1)).toStrictEqual(activeChildButton.get(0)); - }); - - it("should call handleButtonClick and the respective button's onClick method when a button is clicked", () => { - expect(ButtonGroup.prototype.handleButtonClick.callCount).toStrictEqual( - 0, - ); - expect(childButtonSpies.onClick.callCount).toStrictEqual(0); - const childButtons = wrapper.find('.button-group__button'); - childButtons.at(0).props().onClick(); - childButtons.at(1).props().onClick(); - childButtons.at(2).props().onClick(); - expect(ButtonGroup.prototype.handleButtonClick.callCount).toStrictEqual( - 3, - ); - expect(childButtonSpies.onClick.callCount).toStrictEqual(3); - }); - - it('should render all child buttons as disabled if props.disabled is true', () => { - const childButtons = wrapper.find('.button-group__button'); - childButtons.forEach((button) => { - expect(button.props().disabled).toBeUndefined(); - }); - wrapper.setProps({ disabled: true }); - const disabledChildButtons = wrapper.find('[disabled=true]'); - expect(disabledChildButtons).toHaveLength(3); - }); - - it('should render the children of the button', () => { - const mockClass = wrapper.find('.mockClass'); - expect(mockClass).toHaveLength(1); - }); - }); - - describe('render', () => { - it('should render a div with the expected class and style', () => { - expect(wrapper.find('div').at(0).props().className).toStrictEqual( - 'someClassName', - ); - expect(wrapper.find('div').at(0).props().style).toStrictEqual({ - color: 'red', - }); - }); - - it('should call renderButtons when rendering', () => { - expect(ButtonGroup.prototype.renderButtons.callCount).toStrictEqual(1); - wrapper.instance().render(); - expect(ButtonGroup.prototype.renderButtons.callCount).toStrictEqual(2); - }); + expect(container).toMatchSnapshot(); }); }); diff --git a/ui/components/ui/dropdown/dropdown.js b/ui/components/ui/dropdown/dropdown.js index 3a6de0dca..8bfde46ff 100644 --- a/ui/components/ui/dropdown/dropdown.js +++ b/ui/components/ui/dropdown/dropdown.js @@ -8,9 +8,10 @@ const Dropdown = ({ disabled = false, onChange, options, - selectedOption = null, + selectedOption = '', style, title, + 'data-testid': dataTestId, }) => { const _onChange = useCallback( (event) => { @@ -25,6 +26,7 @@ const Dropdown = ({
    @@ -260,6 +261,7 @@ export default class AdvancedTab extends PureComponent {
    +
    +
    +
    +
    +
    + + Show incoming transactions + +
    + Select this to use Etherscan to show incoming transactions in the transactions list +
    +
    +
    +
    +
    +`; + +exports[`View Price Quote Difference displays an error when in high bucket 1`] = ` +
    +
    +
    + +
    +
    +
    +
    +
    + Price difference of ~% +
    +
    +
    + +
    +
    +
    + You are about to swap 1 ETH (~) for 42.947749 LINK (~). +
    + +
    +
    +
    +
    +
    +
    +
    +`; + +exports[`View Price Quote Difference displays an error when in medium bucket 1`] = ` +
    +
    +
    + +
    +
    +
    +
    +
    + Price difference of ~% +
    +
    +
    + +
    +
    +
    + You are about to swap 1 ETH (~) for 42.947749 LINK (~). +
    + +
    +
    +
    +
    +
    +
    +
    +`; + +exports[`View Price Quote Difference should match snapshot 1`] = ` +
    +
    +
    + +
    +
    +
    +
    +
    + Price difference of ~% +
    +
    +
    + +
    +
    +
    + You are about to swap 1 ETH (~) for 42.947749 LINK (~). +
    + +
    +
    +
    +
    +
    +
    +
    +`; diff --git a/ui/pages/swaps/view-quote/view-quote-price-difference.test.js b/ui/pages/swaps/view-quote/view-quote-price-difference.test.js index 08aef036b..806d4b9c0 100644 --- a/ui/pages/swaps/view-quote/view-quote-price-difference.test.js +++ b/ui/pages/swaps/view-quote/view-quote-price-difference.test.js @@ -1,15 +1,12 @@ import React from 'react'; -import { shallow } from 'enzyme'; -import { Provider } from 'react-redux'; import configureMockStore from 'redux-mock-store'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; import { NETWORK_TYPES } from '../../../../shared/constants/network'; import { GAS_RECOMMENDATIONS } from '../../../../shared/constants/gas'; import ViewQuotePriceDifference from './view-quote-price-difference'; describe('View Price Quote Difference', () => { - const t = (key) => `translate ${key}`; - - const state = { + const mockState = { metamask: { tokens: [], provider: { type: NETWORK_TYPES.RPC, nickname: '', rpcUrl: '' }, @@ -19,7 +16,7 @@ describe('View Price Quote Difference', () => { }, }; - const store = configureMockStore()(state); + const mockStore = configureMockStore()(mockState); // Sample transaction is 1 $ETH to ~42.880915 $LINK const DEFAULT_PROPS = { @@ -85,57 +82,37 @@ describe('View Price Quote Difference', () => { destinationTokenValue: '42.947749', }; - let component; - function renderComponent(props) { - component = shallow( - - - , - { - context: { t }, - }, + it('should match snapshot', () => { + const { container } = renderWithProvider( + , + mockStore, ); - } - afterEach(() => { - component.unmount(); - }); - - it('does not render when there is no quote', () => { - const props = { ...DEFAULT_PROPS, usedQuote: null }; - renderComponent(props); - - const wrappingDiv = component.find( - '.view-quote__price-difference-warning-wrapper', - ); - expect(wrappingDiv).toHaveLength(0); - }); - - it('does not render when the item is in the low bucket', () => { - const props = { ...DEFAULT_PROPS }; - props.usedQuote.priceSlippage.bucket = GAS_RECOMMENDATIONS.LOW; - - renderComponent(props); - const wrappingDiv = component.find( - '.view-quote__price-difference-warning-wrapper', - ); - expect(wrappingDiv).toHaveLength(0); + expect(container).toMatchSnapshot(); }); it('displays an error when in medium bucket', () => { const props = { ...DEFAULT_PROPS }; props.usedQuote.priceSlippage.bucket = GAS_RECOMMENDATIONS.MEDIUM; - renderComponent(props); - expect(component.html()).toContain(GAS_RECOMMENDATIONS.MEDIUM); + const { container } = renderWithProvider( + , + mockStore, + ); + + expect(container).toMatchSnapshot(); }); it('displays an error when in high bucket', () => { const props = { ...DEFAULT_PROPS }; props.usedQuote.priceSlippage.bucket = GAS_RECOMMENDATIONS.HIGH; - renderComponent(props); - expect(component.html()).toContain(GAS_RECOMMENDATIONS.HIGH); + const { container } = renderWithProvider( + , + mockStore, + ); + + expect(container).toMatchSnapshot(); }); it('displays a fiat error when calculationError is present', () => { @@ -143,7 +120,11 @@ describe('View Price Quote Difference', () => { props.usedQuote.priceSlippage.calculationError = 'Could not determine price.'; - renderComponent(props); - expect(component.html()).toContain(GAS_RECOMMENDATIONS.HIGH); + const { container } = renderWithProvider( + , + mockStore, + ); + + expect(container).toMatchSnapshot(); }); }); diff --git a/ui/pages/swaps/view-quote/view-quote.js b/ui/pages/swaps/view-quote/view-quote.js index 7897b61a7..f35be9c4f 100644 --- a/ui/pages/swaps/view-quote/view-quote.js +++ b/ui/pages/swaps/view-quote/view-quote.js @@ -11,6 +11,7 @@ import { useHistory } from 'react-router-dom'; import BigNumber from 'bignumber.js'; import { isEqual } from 'lodash'; import classnames from 'classnames'; +import { captureException } from '@sentry/browser'; import { I18nContext } from '../../../contexts/i18n'; import SelectQuotePopover from '../select-quote-popover'; @@ -63,9 +64,9 @@ import { getHardwareWalletType, checkNetworkAndAccountSupports1559, getUSDConversionRate, + getIsMultiLayerFeeNetwork, } from '../../../selectors'; import { getNativeCurrency, getTokens } from '../../../ducks/metamask/metamask'; - import { safeRefetchQuotes, setCustomApproveTxData, @@ -113,6 +114,8 @@ import { toPrecisionWithoutTrailingZeros, } from '../../../../shared/lib/transactions-controller-utils'; import { calcTokenValue } from '../../../../shared/lib/swaps-utils'; +import fetchEstimatedL1Fee from '../../../helpers/utils/optimism/fetchEstimatedL1Fee'; +import { sumHexes } from '../../../helpers/utils/transactions.util'; import ViewQuotePriceDifference from './view-quote-price-difference'; let intervalId; @@ -128,6 +131,7 @@ export default function ViewQuote() { const [selectQuotePopoverShown, setSelectQuotePopoverShown] = useState(false); const [warningHidden, setWarningHidden] = useState(false); const [originalApproveAmount, setOriginalApproveAmount] = useState(null); + const [multiLayerL1FeeTotal, setMultiLayerL1FeeTotal] = useState(null); // We need to have currentTimestamp in state, otherwise it would change with each rerender. const [currentTimestamp] = useState(Date.now()); @@ -161,6 +165,7 @@ export default function ViewQuote() { const { balance: ethBalance } = useSelector(getSelectedAccount, shallowEqual); const conversionRate = useSelector(conversionRateSelector); const USDConversionRate = useSelector(getUSDConversionRate); + const isMultiLayerFeeNetwork = useSelector(getIsMultiLayerFeeNetwork); const currentCurrency = useSelector(getCurrentCurrency); const swapsTokens = useSelector(getTokens, isEqual); const networkAndAccountSupports1559 = useSelector( @@ -261,8 +266,13 @@ export default function ViewQuote() { maxPriorityFeePerGas, ); } - - const gasTotalInWeiHex = calcGasTotal(maxGasLimit, maxFeePerGas || gasPrice); + let gasTotalInWeiHex = calcGasTotal(maxGasLimit, maxFeePerGas || gasPrice); + if (multiLayerL1FeeTotal !== null) { + gasTotalInWeiHex = sumHexes( + gasTotalInWeiHex || '0x0', + multiLayerL1FeeTotal || '0x0', + ); + } const { tokensWithBalances } = useTokenTracker(swapsTokens, true); const balanceToken = @@ -303,6 +313,7 @@ export default function ViewQuote() { smartTransactionsOptInStatus && smartTransactionFees?.tradeTxFees, nativeCurrencySymbol, + multiLayerL1FeeTotal, ); }, [ quotes, @@ -318,6 +329,7 @@ export default function ViewQuote() { nativeCurrencySymbol, smartTransactionsEnabled, smartTransactionsOptInStatus, + multiLayerL1FeeTotal, ]); const renderableDataForUsedQuote = renderablePopoverData.find( @@ -351,6 +363,7 @@ export default function ViewQuote() { sourceAmount: usedQuote.sourceAmount, chainId, nativeCurrencySymbol, + multiLayerL1FeeTotal, }); additionalTrackingParams.reg_tx_fee_in_usd = Number(feeInUsd); additionalTrackingParams.reg_tx_fee_in_eth = Number(rawEthFee); @@ -367,6 +380,7 @@ export default function ViewQuote() { sourceAmount: usedQuote.sourceAmount, chainId, nativeCurrencySymbol, + multiLayerL1FeeTotal, }); let { feeInFiat: maxFeeInFiat, @@ -875,6 +889,25 @@ export default function ViewQuote() { submitClicked, ]); + useEffect(() => { + if (!isMultiLayerFeeNetwork) { + return; + } + const getEstimatedL1Fee = async () => { + try { + const result = await fetchEstimatedL1Fee(global.eth, { + txParams: unsignedTransaction, + chainId, + }); + setMultiLayerL1FeeTotal(result); + } catch (e) { + captureException(e); + setMultiLayerL1FeeTotal(null); + } + }; + getEstimatedL1Fee(); + }, [unsignedTransaction, isMultiLayerFeeNetwork, chainId]); + useEffect(() => { if (currentSmartTransactionsEnabled && smartTransactionsOptInStatus) { // Removes a smart transactions error when the component loads. diff --git a/ui/pages/token-allowance/token-allowance.js b/ui/pages/token-allowance/token-allowance.js index 33cde65fd..4d02f788e 100644 --- a/ui/pages/token-allowance/token-allowance.js +++ b/ui/pages/token-allowance/token-allowance.js @@ -29,6 +29,7 @@ import { transactionFeeSelector, getKnownMethodData, getRpcPrefsForCurrentProvider, + getCustomTokenAmount, } from '../../selectors'; import { NETWORK_TO_NAME_MAP } from '../../../shared/constants/network'; import { @@ -39,6 +40,10 @@ import { import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck'; import { getMostRecentOverviewPage } from '../../ducks/history/history'; import ApproveContentCard from '../../components/app/approve-content-card/approve-content-card'; +import CustomSpendingCap from '../../components/app/custom-spending-cap/custom-spending-cap'; +import Dialog from '../../components/ui/dialog'; +import { useGasFeeContext } from '../../contexts/gasFee'; +import { getCustomTxParamsData } from '../confirm-approve/confirm-approve.util'; export default function TokenAllowance({ origin, @@ -58,7 +63,7 @@ export default function TokenAllowance({ data, isSetApproveForAll, isApprovalOrRejection, - customTxParamsData, + decimals, dappProposedTokenAmount, currentTokenBalance, toAddress, @@ -71,11 +76,22 @@ export default function TokenAllowance({ const [showContractDetails, setShowContractDetails] = useState(false); const [showFullTxDetails, setShowFullTxDetails] = useState(false); - const [isFirstPage, setIsFirstPage] = useState(false); + const [isFirstPage, setIsFirstPage] = useState(true); + const [errorText, setErrorText] = useState(''); const currentAccount = useSelector(getCurrentAccountWithSendEtherInfo); const networkIdentifier = useSelector(getNetworkIdentifier); const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); + const customTokenAmount = useSelector(getCustomTokenAmount); + + const customPermissionAmount = customTokenAmount.toString(); + + const customTxParamsData = customTokenAmount + ? getCustomTxParamsData(data, { + customPermissionAmount, + decimals, + }) + : null; let fullTxData = { ...txData }; @@ -92,6 +108,13 @@ export default function TokenAllowance({ const fee = useSelector((state) => transactionFeeSelector(state, fullTxData)); const methodData = useSelector((state) => getKnownMethodData(state, data)); + const { balanceError } = useGasFeeContext(); + + const disableNextButton = + isFirstPage && (customTokenAmount === '' || errorText !== ''); + + const disableApproveButton = !isFirstPage && balanceError; + const networkName = NETWORK_TO_NAME_MAP[fullTxData.chainId] || networkIdentifier; @@ -105,9 +128,10 @@ export default function TokenAllowance({ : transactionData; const handleReject = () => { + dispatch(updateCustomNonce('')); + dispatch(cancelTx(fullTxData)).then(() => { dispatch(clearConfirmTransaction()); - dispatch(updateCustomNonce('')); history.push(mostRecentOverviewPage); }); }; @@ -128,17 +152,35 @@ export default function TokenAllowance({ fullTxData.originalApprovalAmount = dappProposedTokenAmount; } + if (customTokenAmount) { + fullTxData.customTokenAmount = customTokenAmount; + fullTxData.finalApprovalAmount = customTokenAmount; + } else if (dappProposedTokenAmount !== undefined) { + fullTxData.finalApprovalAmount = dappProposedTokenAmount; + } + if (currentTokenBalance) { fullTxData.currentTokenBalance = currentTokenBalance; } + dispatch(updateCustomNonce('')); + dispatch(updateAndApproveTx(customNonceMerge(fullTxData))).then(() => { dispatch(clearConfirmTransaction()); - dispatch(updateCustomNonce('')); history.push(mostRecentOverviewPage); }); }; + const handleNextClick = () => { + setShowFullTxDetails(false); + setIsFirstPage(false); + }; + + const handleBackClick = () => { + setShowFullTxDetails(false); + setIsFirstPage(true); + }; + return ( {!isFirstPage && ( - - setIsFirstPage(true)} - /> + {isFirstPage ? ( + setErrorText(value)} + /> + ) : ( + handleBackClick()} + /> + )} + {!isFirstPage && balanceError && ( + + {t('insufficientFundsForGas')} + + )} {!isFirstPage && ( @@ -328,7 +389,8 @@ export default function TokenAllowance({ cancelText={t('reject')} submitText={isFirstPage ? t('next') : t('approveButtonText')} onCancel={() => handleReject()} - onSubmit={() => (isFirstPage ? setIsFirstPage(false) : handleApprove())} + onSubmit={() => (isFirstPage ? handleNextClick() : handleApprove())} + disabled={disableNextButton || disableApproveButton} /> {showContractDetails && ( + {isBeta() ? ( +
    + {t('beta')} +
    + ) : null}

    {t('welcomeBack')}

    {t('unlockMessage')}
    diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 9c6cce639..9567f3443 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -71,6 +71,7 @@ import { import { isEqualCaseInsensitive } from '../../shared/modules/string-utils'; import { hexToDecimal } from '../../shared/lib/metamask-controller-utils'; import { formatMoonpaySymbol } from '../helpers/utils/moonpay'; +import { TRANSACTION_STATUSES } from '../../shared/constants/transaction'; ///: BEGIN:ONLY_INCLUDE_IN(flask) import { SNAPS_VIEW_ROUTE } from '../helpers/constants/routes'; import { getPermissionSubjects } from './permissions'; @@ -411,6 +412,21 @@ export function getAddressBookEntryOrAccountName(state, address) { return entry && entry.name !== '' ? entry.name : address; } +export function getAccountName(identities, address) { + const entry = Object.values(identities).find((identity) => + isEqualCaseInsensitive(identity.address, toChecksumHexAddress(address)), + ); + return entry && entry.name !== '' ? entry.name : ''; +} + +export function getMetadataContractName(state, address) { + const tokenList = getTokenList(state); + const entry = Object.values(tokenList).find((identity) => + isEqualCaseInsensitive(identity.address, toChecksumHexAddress(address)), + ); + return entry && entry.name !== '' ? entry.name : ''; +} + export function accountsWithSendEtherInfoSelector(state) { const accounts = getMetaMaskAccounts(state); const identities = getMetaMaskIdentities(state); @@ -791,6 +807,9 @@ const createDeepEqualSelector = createSelectorCreator(defaultMemoize, isEqual); export const getUnapprovedTransactions = (state) => state.metamask.unapprovedTxs; +const getCurrentNetworkTransactionList = (state) => + state.metamask.currentNetworkTxList; + export const getTxData = (state) => state.confirmTransaction.txData; export const getUnapprovedTransaction = createDeepEqualSelector( @@ -803,11 +822,26 @@ export const getUnapprovedTransaction = createDeepEqualSelector( }, ); +export const getTransaction = createDeepEqualSelector( + getCurrentNetworkTransactionList, + (_, transactionId) => transactionId, + (unapprovedTxs, transactionId) => { + return ( + Object.values(unapprovedTxs).find(({ id }) => id === transactionId) || {} + ); + }, +); + export const getFullTxData = createDeepEqualSelector( getTxData, - (state, transactionId) => getUnapprovedTransaction(state, transactionId), - (_state, _transactionId, customTxParamsData) => customTxParamsData, - (txData, transaction, customTxParamsData) => { + (state, transactionId, status) => { + if (status === TRANSACTION_STATUSES.UNAPPROVED) { + return getUnapprovedTransaction(state, transactionId); + } + return getTransaction(state, transactionId); + }, + (_state, _transactionId, _status, customTxParamsData) => customTxParamsData, + (txData, transaction, _status, customTxParamsData) => { let fullTxData = { ...txData, ...transaction }; if (transaction && transaction.simulationFails) { txData.simulationFails = transaction.simulationFails; @@ -922,12 +956,13 @@ function getAllowedAnnouncementIds(state) { 7: false, 8: supportsWebHid && currentKeyringIsLedger && currentlyUsingLedgerLive, 9: false, - 10: true, - 11: true, + 10: false, + 11: false, 12: false, 13: false, 14: false, - 15: true, + 15: false, + 16: true, }; } @@ -978,6 +1013,10 @@ export function getShowPortfolioTooltip(state) { return state.metamask.showPortfolioTooltip; } +export function getShowBetaHeader(state) { + return state.metamask.showBetaHeader; +} + /** * To get the useTokenDetection flag which determines whether a static or dynamic token list is used * @@ -989,13 +1028,13 @@ export function getUseTokenDetection(state) { } /** - * To get the useCollectibleDetection flag which determines whether we autodetect NFTs + * To get the useNftDetection flag which determines whether we autodetect NFTs * * @param {*} state * @returns Boolean */ -export function getUseCollectibleDetection(state) { - return Boolean(state.metamask.useCollectibleDetection); +export function getUseNftDetection(state) { + return Boolean(state.metamask.useNftDetection); } /** @@ -1221,6 +1260,16 @@ export function getIstokenDetectionInactiveOnNonMainnetSupportedNetwork(state) { return isDynamicTokenListAvailable && !useTokenDetection && !isMainnet; } +/** + * To get the `improvedTokenAllowanceEnabled` value which determines whether we use the improved token allowance + * + * @param {*} state + * @returns Boolean + */ +export function getIsImprovedTokenAllowanceEnabled(state) { + return state.metamask.improvedTokenAllowanceEnabled; +} + export function getIsCustomNetwork(state) { const chainId = getCurrentChainId(state); @@ -1288,3 +1337,7 @@ export function getShouldShowSeedPhraseReminder(state) { dismissSeedBackUpReminder === false ); } + +export function getCustomTokenAmount(state) { + return state.appState.customTokenAmount; +} diff --git a/ui/store/actionConstants.js b/ui/store/actionConstants.js index f9878968e..4a48085eb 100644 --- a/ui/store/actionConstants.js +++ b/ui/store/actionConstants.js @@ -120,3 +120,6 @@ export const TOGGLE_CURRENCY_INPUT_SWITCH = 'TOGGLE_CURRENCY_INPUT_SWITCH'; // Token detection v2 export const SET_NEW_TOKENS_IMPORTED = 'SET_NEW_TOKENS_IMPORTED'; + +// Token allowance +export const SET_CUSTOM_TOKEN_AMOUNT = 'SET_CUSTOM_TOKEN_AMOUNT'; diff --git a/ui/store/actions.js b/ui/store/actions.js index fb9639282..1d79a543d 100644 --- a/ui/store/actions.js +++ b/ui/store/actions.js @@ -1781,7 +1781,7 @@ export async function getBalancesInSingleCall(tokens) { return await submitRequestToBackground('getBalancesInSingleCall', [tokens]); } -export function addCollectible(address, tokenID, dontShowLoadingIndicator) { +export function addNft(address, tokenID, dontShowLoadingIndicator) { return async (dispatch) => { if (!address) { throw new Error('MetaMask - Cannot add collectible without address'); @@ -1793,7 +1793,7 @@ export function addCollectible(address, tokenID, dontShowLoadingIndicator) { dispatch(showLoadingIndication()); } try { - await submitRequestToBackground('addCollectible', [address, tokenID]); + await submitRequestToBackground('addNft', [address, tokenID]); } catch (error) { log.error(error); dispatch(displayWarning(error.message)); @@ -1804,7 +1804,7 @@ export function addCollectible(address, tokenID, dontShowLoadingIndicator) { }; } -export function addCollectibleVerifyOwnership( +export function addNftVerifyOwnership( address, tokenID, dontShowLoadingIndicator, @@ -1820,7 +1820,7 @@ export function addCollectibleVerifyOwnership( dispatch(showLoadingIndication()); } try { - await submitRequestToBackground('addCollectibleVerifyOwnership', [ + await submitRequestToBackground('addNftVerifyOwnership', [ address, tokenID, ]); @@ -1841,11 +1841,7 @@ export function addCollectibleVerifyOwnership( }; } -export function removeAndIgnoreCollectible( - address, - tokenID, - dontShowLoadingIndicator, -) { +export function removeAndIgnoreNft(address, tokenID, dontShowLoadingIndicator) { return async (dispatch) => { if (!address) { throw new Error('MetaMask - Cannot ignore collectible without address'); @@ -1857,10 +1853,7 @@ export function removeAndIgnoreCollectible( dispatch(showLoadingIndication()); } try { - await submitRequestToBackground('removeAndIgnoreCollectible', [ - address, - tokenID, - ]); + await submitRequestToBackground('removeAndIgnoreNft', [address, tokenID]); } catch (error) { log.error(error); dispatch(displayWarning(error.message)); @@ -1871,7 +1864,7 @@ export function removeAndIgnoreCollectible( }; } -export function removeCollectible(address, tokenID, dontShowLoadingIndicator) { +export function removeNft(address, tokenID, dontShowLoadingIndicator) { return async (dispatch) => { if (!address) { throw new Error('MetaMask - Cannot remove collectible without address'); @@ -1883,7 +1876,7 @@ export function removeCollectible(address, tokenID, dontShowLoadingIndicator) { dispatch(showLoadingIndication()); } try { - await submitRequestToBackground('removeCollectible', [address, tokenID]); + await submitRequestToBackground('removeNft', [address, tokenID]); } catch (error) { log.error(error); dispatch(displayWarning(error.message)); @@ -1894,31 +1887,27 @@ export function removeCollectible(address, tokenID, dontShowLoadingIndicator) { }; } -export async function checkAndUpdateAllCollectiblesOwnershipStatus() { - await submitRequestToBackground( - 'checkAndUpdateAllCollectiblesOwnershipStatus', - ); +export async function checkAndUpdateAllNftsOwnershipStatus() { + await submitRequestToBackground('checkAndUpdateAllNftsOwnershipStatus'); } -export async function isCollectibleOwner( +export async function isNftOwner( ownerAddress, collectibleAddress, collectibleId, ) { - return await submitRequestToBackground('isCollectibleOwner', [ + return await submitRequestToBackground('isNftOwner', [ ownerAddress, collectibleAddress, collectibleId, ]); } -export async function checkAndUpdateSingleCollectibleOwnershipStatus( - collectible, -) { - await submitRequestToBackground( - 'checkAndUpdateSingleCollectibleOwnershipStatus', - [collectible, false], - ); +export async function checkAndUpdateSingleNftOwnershipStatus(collectible) { + await submitRequestToBackground('checkAndUpdateSingleNftOwnershipStatus', [ + collectible, + false, + ]); } export async function getTokenStandardAndDetails( @@ -2729,11 +2718,11 @@ export function setUseTokenDetection(val) { }; } -export function setUseCollectibleDetection(val) { +export function setUseNftDetection(val) { return (dispatch) => { dispatch(showLoadingIndication()); - log.debug(`background.setUseCollectibleDetection`); - callBackgroundMethod('setUseCollectibleDetection', [val], (err) => { + log.debug(`background.setUseNftDetection`); + callBackgroundMethod('setUseNftDetection', [val], (err) => { dispatch(hideLoadingIndication()); if (err) { dispatch(displayWarning(err.message)); @@ -2755,11 +2744,11 @@ export function setOpenSeaEnabled(val) { }; } -export function detectCollectibles() { +export function detectNfts() { return async (dispatch) => { dispatch(showLoadingIndication()); - log.debug(`background.detectCollectibles`); - await submitRequestToBackground('detectCollectibles'); + log.debug(`background.detectNfts`); + await submitRequestToBackground('detectNfts'); dispatch(hideLoadingIndication()); await forceUpdateMetamaskState(dispatch); }; @@ -3516,7 +3505,10 @@ export async function closeNotificationPopup() { * @returns {Promise} */ export function trackMetaMetricsEvent(payload, options) { - return submitRequestToBackground('trackMetaMetricsEvent', [payload, options]); + return submitRequestToBackground('trackMetaMetricsEvent', [ + { ...payload, actionId: generateActionId() }, + options, + ]); } export function createEventFragment(options) { @@ -3548,7 +3540,10 @@ export function finalizeEventFragment(id, options) { * @param {MetaMetricsPageOptions} options - options for handling the page view */ export function trackMetaMetricsPage(payload, options) { - return submitRequestToBackground('trackMetaMetricsPage', [payload, options]); + return submitRequestToBackground('trackMetaMetricsPage', [ + { ...payload, actionId: generateActionId() }, + options, + ]); } export function updateViewedNotifications(notificationIdViewedStatusMap) { @@ -3578,6 +3573,7 @@ export async function setSmartTransactionsOptInStatus( prevOptInState, ) { trackMetaMetricsEvent({ + actionId: generateActionId(), event: 'STX OptIn', category: EVENT.CATEGORIES.SWAPS, sensitiveProperties: { @@ -3779,6 +3775,10 @@ export function hidePortfolioTooltip() { return submitRequestToBackground('setShowPortfolioTooltip', [false]); } +export function hideBetaHeader() { + return submitRequestToBackground('setShowBetaHeader', [false]); +} + export function setCollectiblesDetectionNoticeDismissed() { return submitRequestToBackground('setCollectiblesDetectionNoticeDismissed', [ true, @@ -3789,6 +3789,20 @@ export function setEnableEIP1559V2NoticeDismissed() { return submitRequestToBackground('setEnableEIP1559V2NoticeDismissed', [true]); } +export function setImprovedTokenAllowanceEnabled( + improvedTokenAllowanceEnabled, +) { + return async () => { + try { + await submitRequestToBackground('setImprovedTokenAllowanceEnabled', [ + improvedTokenAllowanceEnabled, + ]); + } catch (error) { + log.error(error); + } + }; +} + export function setFirstTimeUsedNetwork(chainId) { return submitRequestToBackground('setFirstTimeUsedNetwork', [chainId]); } @@ -3827,7 +3841,10 @@ export function addCustomNetwork(customRpc) { return async (dispatch) => { try { dispatch(setNewCustomNetworkAdded(customRpc)); - await submitRequestToBackground('addCustomNetwork', [customRpc]); + await submitRequestToBackground('addCustomNetwork', [ + customRpc, + generateActionId(), + ]); } catch (error) { log.error(error); dispatch(displayWarning('Had a problem changing networks!')); diff --git a/yarn.lock b/yarn.lock index c0eb9f1b1..21d3a0916 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2784,49 +2784,6 @@ resolved "https://registry.yarnpkg.com/@metamask/contract-metadata/-/contract-metadata-1.36.0.tgz#8e277190195e9c26733752457d2004d149fd7e0e" integrity sha512-weTsrXfDQHOgYaiI5giMcOAsD3ChcwnoryasT7xmAfLSKIbKP3RTTUu63VWYBoFCBZugHrhKD6z+N+nm8qAWBQ== -"@metamask/controllers@^31.0.0": - version "31.2.0" - resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-31.2.0.tgz#65176b1adb8d30034b80cd537f518c840612e620" - integrity sha512-9UiTq1dGQNG9ZXkBGOhgwJcRXIUM+wUiltLRSCGxVAqIgEcCK34ZbMrHV6DMRoYhl3izfiZSVeYkta1Tg+mwyw== - dependencies: - "@ethereumjs/common" "^2.3.1" - "@ethereumjs/tx" "^3.2.1" - "@ethersproject/abi" "^5.7.0" - "@ethersproject/contracts" "^5.7.0" - "@ethersproject/providers" "^5.7.0" - "@keystonehq/metamask-airgapped-keyring" "^0.6.1" - "@metamask/contract-metadata" "^1.35.0" - "@metamask/metamask-eth-abis" "3.0.0" - "@metamask/types" "^1.1.0" - "@types/uuid" "^8.3.0" - abort-controller "^3.0.0" - async-mutex "^0.2.6" - babel-runtime "^6.26.0" - deep-freeze-strict "^1.1.1" - eth-ens-namehash "^2.0.8" - eth-json-rpc-infura "^5.1.0" - eth-keyring-controller "^7.0.2" - eth-method-registry "1.1.0" - eth-phishing-detect "^1.2.0" - eth-query "^2.1.2" - eth-rpc-errors "^4.0.0" - eth-sig-util "^3.0.0" - ethereumjs-util "^7.0.10" - ethereumjs-wallet "^1.0.1" - ethjs-unit "^0.1.6" - fast-deep-equal "^3.1.3" - immer "^9.0.6" - isomorphic-fetch "^3.0.0" - json-rpc-engine "^6.1.0" - jsonschema "^1.2.4" - multiformats "^9.5.2" - nanoid "^3.1.31" - punycode "^2.1.1" - single-call-balance-checker-abi "^1.0.0" - uuid "^8.3.2" - web3 "^0.20.7" - web3-provider-engine "^16.0.3" - "@metamask/controllers@^32.0.2": version "32.0.2" resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-32.0.2.tgz#4841e4b8622c0e9a2cc948ef5f7e8a00473055e8" @@ -2870,10 +2827,53 @@ web3 "^0.20.7" web3-provider-engine "^16.0.3" +"@metamask/controllers@^33.0.0": + version "33.0.0" + resolved "https://registry.yarnpkg.com/@metamask/controllers/-/controllers-33.0.0.tgz#908c05f6bee741c0beecd9d85d50c304afa38f2b" + integrity sha512-ImnoLztyyE9qswPAv6zk7d40d5FTMPiJYqAjjnJz+hEYhhGPGYI87+2OF/i+kVLv3gatyBQzNxvE1qtQSDWJsg== + dependencies: + "@ethereumjs/common" "^2.3.1" + "@ethereumjs/tx" "^3.2.1" + "@ethersproject/abi" "^5.7.0" + "@ethersproject/contracts" "^5.7.0" + "@ethersproject/providers" "^5.7.0" + "@keystonehq/metamask-airgapped-keyring" "^0.6.1" + "@metamask/contract-metadata" "^1.35.0" + "@metamask/metamask-eth-abis" "3.0.0" + "@metamask/types" "^1.1.0" + "@types/uuid" "^8.3.0" + abort-controller "^3.0.0" + async-mutex "^0.2.6" + babel-runtime "^6.26.0" + deep-freeze-strict "^1.1.1" + eth-ens-namehash "^2.0.8" + eth-json-rpc-infura "^5.1.0" + eth-keyring-controller "^7.0.2" + eth-method-registry "1.1.0" + eth-phishing-detect "^1.2.0" + eth-query "^2.1.2" + eth-rpc-errors "^4.0.0" + eth-sig-util "^3.0.0" + ethereumjs-util "^7.0.10" + ethereumjs-wallet "^1.0.1" + ethjs-unit "^0.1.6" + fast-deep-equal "^3.1.3" + immer "^9.0.6" + isomorphic-fetch "^3.0.0" + json-rpc-engine "^6.1.0" + jsonschema "^1.2.4" + multiformats "^9.5.2" + nanoid "^3.1.31" + punycode "^2.1.1" + single-call-balance-checker-abi "^1.0.0" + uuid "^8.3.2" + web3 "^0.20.7" + web3-provider-engine "^16.0.3" + "@metamask/design-tokens@^1.6.0", "@metamask/design-tokens@^1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@metamask/design-tokens/-/design-tokens-1.9.0.tgz#2b173c671f36b0d3faa2e31ea4bf66e811a7ff49" - integrity sha512-L3oIhbE7MVQgiX7bEqdlU32jNyLbYXCj9sJNCOzACIHycB1TIO8TS34dEI7FAf9pC8o0qvMI3k8ur+SD9myVxw== + version "1.11.0" + resolved "https://registry.yarnpkg.com/@metamask/design-tokens/-/design-tokens-1.11.0.tgz#c45d2951c976b9c906fd2a43323446248dbc1a09" + integrity sha512-1lZR8DoqKffBswMy3Jbp8z+zQTbRyi2k6XE9qhe4m5NZd9JfirH4tJzq740HC6Hvz/MI0I7MPJkNAkLPNx8ESA== "@metamask/eslint-config-jest@^9.0.0": version "9.0.0" @@ -2973,21 +2973,22 @@ resolved "https://registry.yarnpkg.com/@metamask/etherscan-link/-/etherscan-link-2.2.0.tgz#76314d0c1405a0669fc4a0a19e0877bd3d0c389f" integrity sha512-xUgehvgU+ZbzeJ44m4sUtsyf6Dwou+SlYhiKfi6lkRcbWh6Jl3TCi0YM9C7XWgxfnLSdQBO1ndvcp0kslKgMsA== -"@metamask/execution-environments@^0.22.3": - version "0.22.3" - resolved "https://registry.yarnpkg.com/@metamask/execution-environments/-/execution-environments-0.22.3.tgz#19feb8506d3a703b35cd1ea9df73370498f62915" - integrity sha512-KZaAxxOOfuDR6V0JcsDdUzHTMrrQAwlHP5QZ8Zy+PSYVzr0LsecxmQ4FHg5AHQE8yplzPws6ZBmAfzFxp6HY3A== +"@metamask/execution-environments@^0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@metamask/execution-environments/-/execution-environments-0.23.0.tgz#55b573ebc116d17b3b19cf15335c4acd97cabaff" + integrity sha512-OU0gEQ/oDMf19b7B7RZX3jWaZCAjDQA1+6cuGBKbxbFsgEYg3tHUuvn2KtH4Lah4SUGHAv3JZoTShX9j+wHGyg== dependencies: "@metamask/object-multiplex" "^1.2.0" "@metamask/post-message-stream" "^6.0.0" "@metamask/providers" "^9.0.0" - "@metamask/snap-types" "^0.22.3" - "@metamask/snap-utils" "^0.22.3" - "@metamask/utils" "^3.1.0" + "@metamask/snap-types" "^0.23.0" + "@metamask/snap-utils" "^0.23.0" + "@metamask/utils" "^3.3.0" eth-rpc-errors "^4.0.3" pump "^3.0.0" - ses "^0.15.15" + ses "^0.17.0" stream-browserify "^3.0.0" + superstruct "^0.16.7" "@metamask/forwarder@^1.1.0": version "1.1.0" @@ -3002,11 +3003,12 @@ color "^0.11.3" mersenne-twister "^1.1.0" -"@metamask/key-tree@^5.0.2": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@metamask/key-tree/-/key-tree-5.0.2.tgz#2d71ba26faddbad1735eeaa04dc37f1dfabfa9cf" - integrity sha512-82m+GbL/EybAslxuPcnJtt2SUYPrggMeS25lILr5p+ZnM5G58pOoFmT3G2VMdZOE+tdUoX2dC4sOD7VcQ0pmQQ== +"@metamask/key-tree@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@metamask/key-tree/-/key-tree-6.0.0.tgz#9320ea075ef5a02709170fdceacc46a45ebb2364" + integrity sha512-zxdzDQrcltQ7ojkwCZmFnokWhgxWZKw+jxHqykX59f09obYdYzUS54qgjLzuxxWmhJApu6y9mfI1I1Ov34VNyQ== dependencies: + "@metamask/utils" "^3.3.0" "@noble/ed25519" "^1.6.0" "@noble/hashes" "^1.0.0" "@noble/secp256k1" "^1.5.5" @@ -3064,13 +3066,6 @@ pump "^3.0.0" ses "0.12.4" -"@metamask/post-message-stream@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@metamask/post-message-stream/-/post-message-stream-4.0.0.tgz#72f120e562346ca86ccc9b3684023ad44265f0df" - integrity sha512-r0JcoWXNuHycProx8ClxiIElJY/GVb/0/WWXTMsZu7qDejLo52VNXlwfydCdVjbMXeoT2nK1Yt3d5gjmHy5BWw== - dependencies: - readable-stream "2.3.3" - "@metamask/post-message-stream@^5.1.0": version "5.1.0" resolved "https://registry.yarnpkg.com/@metamask/post-message-stream/-/post-message-stream-5.1.0.tgz#3e9a2ed11b540b3868a0a28ffa8ec5a6e5fca29d" @@ -3088,9 +3083,9 @@ readable-stream "2.3.3" "@metamask/providers@^10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/@metamask/providers/-/providers-10.0.0.tgz#8858f3a0d991fe3462cd056a52a74f3ffabc9373" - integrity sha512-GAaXm8+dIN66KfD4EUETyxxmUMDTnpoOsgtoWIS8kI8PdVL1wNazMK2HD03n4b66r0c2SaFdQLxvjeSpu5TyWA== + version "10.2.0" + resolved "https://registry.yarnpkg.com/@metamask/providers/-/providers-10.2.0.tgz#8131de667db0c55a61a150438c2a7f17b2d53615" + integrity sha512-qO3cOZZr/YJ8LLOqhR+51GGBiRknalfV/na7hwXyqZ1R/uxeLeNdqCyg+g8l3Z8JcLoEiaKGNJOEV3FFyLw8mQ== dependencies: "@metamask/object-multiplex" "^1.1.0" "@metamask/safe-event-emitter" "^2.0.0" @@ -3101,7 +3096,7 @@ fast-deep-equal "^2.0.1" is-stream "^2.0.0" json-rpc-engine "^6.1.0" - json-rpc-middleware-stream "^4.0.0" + json-rpc-middleware-stream "^4.2.0" pump "^3.0.0" webextension-polyfill-ts "^0.25.0" @@ -3123,19 +3118,19 @@ pump "^3.0.0" webextension-polyfill-ts "^0.25.0" -"@metamask/rpc-methods@^0.22.2", "@metamask/rpc-methods@^0.22.3": - version "0.22.3" - resolved "https://registry.yarnpkg.com/@metamask/rpc-methods/-/rpc-methods-0.22.3.tgz#5d0c81177f902753f35f52c3cbccf22c1d58bf83" - integrity sha512-PxZdv0Tsz9xp/eI834bunnLprtxRsEOEQRoBD+0GED7a70FBC3AjxdzAnw0n9eoJ1magMCnUvzL28qvgKzGePQ== +"@metamask/rpc-methods@^0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@metamask/rpc-methods/-/rpc-methods-0.23.0.tgz#51e1ebb91891d7227d346b229a6f112b67984c8e" + integrity sha512-jUsdkyOgOv3e7HMuLK/HNLVw431gguNAQf1vYRihV4+OdUZjV0sqVYliCkYuC5F4GPSlqnUK4JEDRI8e8rivsw== dependencies: - "@metamask/controllers" "^31.0.0" - "@metamask/key-tree" "^5.0.2" - "@metamask/snap-utils" "^0.22.3" + "@metamask/controllers" "^32.0.2" + "@metamask/key-tree" "^6.0.0" + "@metamask/snap-utils" "^0.23.0" "@metamask/types" "^1.1.0" - "@metamask/utils" "^3.1.0" + "@metamask/utils" "^3.3.0" eth-rpc-errors "^4.0.2" nanoid "^3.1.31" - superstruct "^0.16.5" + superstruct "^0.16.7" "@metamask/safe-event-emitter@^2.0.0": version "2.0.0" @@ -3162,22 +3157,23 @@ isomorphic-fetch "^3.0.0" lodash "^4.17.21" -"@metamask/snap-controllers@^0.22.2": - version "0.22.3" - resolved "https://registry.yarnpkg.com/@metamask/snap-controllers/-/snap-controllers-0.22.3.tgz#c6b11ef7df752dc4d53b016aa62df9ebac5d3ec3" - integrity sha512-tLNbZVeTPYDkHwU+sb+MmrkFzR0HCKnfOecBC2krIhUzpHEvNgw8/hPRGj0lMi0/j54PfZbOAFf4ixgXM9jW6w== +"@metamask/snap-controllers@^0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@metamask/snap-controllers/-/snap-controllers-0.23.0.tgz#edcf3ec723c94899ec5b31ec54c6ed94be0b8551" + integrity sha512-TqPmlVDdbUu0gsYtoSHXxKQihXxhcRhPde7TnvrauouA13DAi1c/kiOSpCY8S14mMaLVSoc3WBm1t8YziG/cig== dependencies: "@metamask/browser-passworder" "^3.0.0" - "@metamask/controllers" "^31.0.0" - "@metamask/execution-environments" "^0.22.3" + "@metamask/controllers" "^32.0.2" + "@metamask/execution-environments" "^0.23.0" "@metamask/object-multiplex" "^1.1.0" "@metamask/post-message-stream" "^6.0.0" - "@metamask/rpc-methods" "^0.22.3" - "@metamask/snap-types" "^0.22.3" - "@metamask/snap-utils" "^0.22.3" - "@metamask/utils" "^3.1.0" + "@metamask/rpc-methods" "^0.23.0" + "@metamask/snap-types" "^0.23.0" + "@metamask/snap-utils" "^0.23.0" + "@metamask/utils" "^3.3.0" "@xstate/fsm" "^2.0.0" concat-stream "^2.0.0" + cron-parser "^4.5.0" eth-rpc-errors "^4.0.2" gunzip-maybe "^1.4.2" immer "^9.0.6" @@ -3188,33 +3184,33 @@ readable-web-to-node-stream "^3.0.2" tar-stream "^2.2.0" -"@metamask/snap-types@^0.22.3": - version "0.22.3" - resolved "https://registry.yarnpkg.com/@metamask/snap-types/-/snap-types-0.22.3.tgz#b3edeb93ed59b68b50c0d2a4bf92c5eea4fbe0b2" - integrity sha512-42kmDr7HF4oHghq4jbGGmRcI76L6NgiHLd1jmF0d6ShC4j+tCWZY2K62znk4NGeI0m6YZyVKaiTNwAaPWnG1hA== +"@metamask/snap-types@^0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@metamask/snap-types/-/snap-types-0.23.0.tgz#bcd491a100b2410cb91bbecc59007a278fa508d5" + integrity sha512-l5QK9XKw5aGEn8ohofyopTQndtlrVzYjXyZik3RhxZccRcGA4rp7juvTuIoyBE/0fiqTc30BHCmKtVvkaTyd9Q== dependencies: "@metamask/providers" "^9.0.0" - "@metamask/snap-utils" "^0.22.3" + "@metamask/snap-utils" "^0.23.0" "@metamask/types" "^1.1.0" -"@metamask/snap-utils@^0.22.2", "@metamask/snap-utils@^0.22.3": - version "0.22.3" - resolved "https://registry.yarnpkg.com/@metamask/snap-utils/-/snap-utils-0.22.3.tgz#a17a56773919a717062ba9728165d7ce90c6df30" - integrity sha512-sleBuowQMUa9+Eb4OxRs38qxmxO7WCXWmiYkmbOhyk3lGhllbQN3wU/GOvKtC/JH7cSrot2U+ce+M70hWuL9Vg== +"@metamask/snap-utils@^0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@metamask/snap-utils/-/snap-utils-0.23.0.tgz#9a116a2361910ade229749e1f65f8312567faa01" + integrity sha512-S0/D+OZ70tvpyMIBIJJzZp/0X1SPVfofbGU3wQCG0OX7TWMpLsnQnBZfRyK7Jw+ZC+j7fyYMUyXEpvimYfHQBA== dependencies: "@babel/core" "^7.18.6" "@babel/types" "^7.18.7" - "@metamask/snap-types" "^0.22.3" - "@metamask/utils" "^3.1.0" + "@metamask/snap-types" "^0.23.0" + "@metamask/utils" "^3.3.0" "@noble/hashes" "^1.1.3" "@scure/base" "^1.1.1" - ajv "^8.11.0" + cron-parser "^4.5.0" eth-rpc-errors "^4.0.3" fast-deep-equal "^3.1.3" rfdc "^1.3.0" semver "^7.3.7" - ses "^0.15.17" - superstruct "^0.16.5" + ses "^0.17.0" + superstruct "^0.16.7" "@metamask/test-dapp@^5.2.1": version "5.2.1" @@ -3233,10 +3229,10 @@ dependencies: fast-deep-equal "^3.1.3" -"@metamask/utils@^3.0.1", "@metamask/utils@^3.0.3", "@metamask/utils@^3.1.0": - version "3.3.1" - resolved "https://registry.yarnpkg.com/@metamask/utils/-/utils-3.3.1.tgz#04a00a24469e3eb03bde111432053c05afb45326" - integrity sha512-r65Swl91wQ2YDkEQXZah1l7it0iBJK+trTeX9uPHplLQ0lzWZ/yODbEMFZVrStRQxDU8RARXryDyfUX5CLVvLA== +"@metamask/utils@^3.0.1", "@metamask/utils@^3.0.3", "@metamask/utils@^3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@metamask/utils/-/utils-3.3.0.tgz#e5e7885c72a30f50f9e2b29690f543da1d6ab496" + integrity sha512-GT8jMTCiGl3z9L1lvALjgW/6urJsl5Cwnix4C65NzJInF0cK2GxqpLkEMQJ50Mdky2qc2P7+F5++d4utvx2TtA== dependencies: "@types/debug" "^4.1.7" debug "^4.3.4" @@ -3546,6 +3542,11 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== +"@sindresorhus/is@^4.0.0": + version "4.6.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" + integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== + "@sinonjs/commons@^1", "@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0": version "1.7.0" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.0.tgz#f90ffc52a2e519f018b13b6c4da03cbff36ebed6" @@ -4471,6 +4472,13 @@ dependencies: defer-to-connect "^1.0.1" +"@szmarczak/http-timer@^4.0.5": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807" + integrity sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w== + dependencies: + defer-to-connect "^2.0.0" + "@testim/chrome-version@^1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@testim/chrome-version/-/chrome-version-1.1.3.tgz#fbb68696899d7b8c1b9b891eded9c04fe2cd5529" @@ -4802,6 +4810,16 @@ "@types/insert-module-globals" "*" "@types/node" "*" +"@types/cacheable-request@^6.0.1": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.2.tgz#c324da0197de0a98a2312156536ae262429ff6b9" + integrity sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA== + dependencies: + "@types/http-cache-semantics" "*" + "@types/keyv" "*" + "@types/node" "*" + "@types/responselike" "*" + "@types/chrome@^0.0.136": version "0.0.136" resolved "https://registry.yarnpkg.com/@types/chrome/-/chrome-0.0.136.tgz#7c011b9f997b0156f25a140188a0c5689d3f368f" @@ -4960,6 +4978,11 @@ resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz#3c9ee980f1a10d6021ae6632ca3e79ca2ec4fb50" integrity sha512-giAlZwstKbmvMk1OO7WXSj4OZ0keXAcl2TQq4LWHiiPH2ByaH7WeUzng+Qej8UPxxv+8lRTuouo0iaNDBuzIBA== +"@types/http-cache-semantics@*": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812" + integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ== + "@types/insert-module-globals@*": version "7.0.2" resolved "https://registry.yarnpkg.com/@types/insert-module-globals/-/insert-module-globals-7.0.2.tgz#0d44216a6489829897d7c8a97dbf8250444c95f8" @@ -5031,6 +5054,13 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/keyv@*": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-4.2.0.tgz#65b97868ab757906f2dbb653590d7167ad023fa0" + integrity sha512-xoBtGl5R9jeKUhc8ZqeYaRDx04qqJ10yhhXYGmJ4Jr8qKpvMsDQQrNUvF/wUJ4klOtmJeJM+p2Xo3zp9uaC3tw== + dependencies: + keyv "*" + "@types/lodash@^4.14.107", "@types/lodash@^4.14.136", "@types/lodash@^4.14.167", "@types/lodash@^4.14.176": version "4.14.184" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.184.tgz#23f96cd2a21a28e106dc24d825d4aa966de7a9fe" @@ -5203,6 +5233,13 @@ dependencies: redux "*" +"@types/responselike@*", "@types/responselike@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" + integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== + dependencies: + "@types/node" "*" + "@types/sass@*": version "1.43.1" resolved "https://registry.yarnpkg.com/@types/sass/-/sass-1.43.1.tgz#86bb0168e9e881d7dade6eba16c9ed6d25dc2f68" @@ -6100,10 +6137,10 @@ adjust-sourcemap-loader@3.0.0: loader-utils "^2.0.0" regex-parser "^2.2.11" -adm-zip@0.4.16: - version "0.4.16" - resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.16.tgz#cf4c508fdffab02c269cbc7f471a875f05570365" - integrity sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg== +adm-zip@0.5.9: + version "0.5.9" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.9.tgz#b33691028333821c0cf95c31374c5462f2905a83" + integrity sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg== aes-js@3.0.0: version "3.0.0" @@ -6194,7 +6231,7 @@ ajv-merge-patch@5.0.1: fast-json-patch "^2.0.6" json-merge-patch "^1.0.2" -ajv@8.11.0, ajv@^8.11.0: +ajv@8.11.0: version "8.11.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== @@ -6770,17 +6807,17 @@ atomic-sleep@^1.0.0: resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== -autoprefixer@^8.0.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-8.1.0.tgz#374cf35be1c0e8fce97408d876f95f66f5cb4641" - integrity sha512-b6mjq6VZ0guW6evRkKXL5sSSvIXICAE9dyWReZ3l/riidU7bVaJMe5cQ512SmaLA4Pvgnhi5MFsMs/Mvyh9//Q== +autoprefixer@^10.2.6: + version "10.4.13" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.13.tgz#b5136b59930209a321e9fa3dca2e7c4d223e83a8" + integrity sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg== dependencies: - browserslist "^3.1.1" - caniuse-lite "^1.0.30000810" + browserslist "^4.21.4" + caniuse-lite "^1.0.30001426" + fraction.js "^4.2.0" normalize-range "^0.1.2" - num2fraction "^1.2.2" - postcss "^6.0.19" - postcss-value-parser "^3.2.3" + picocolors "^1.0.0" + postcss-value-parser "^4.2.0" autoprefixer@^9.8.0, autoprefixer@^9.8.6: version "9.8.8" @@ -7684,23 +7721,15 @@ browserify@^17.0.0: vm-browserify "^1.0.0" xtend "^4.0.0" -browserslist@^3.1.1: - version "3.2.8" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-3.2.8.tgz#b0005361d6471f0f5952797a76fc985f1f978fc6" - integrity sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ== +browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.20.2, browserslist@^4.21.3, browserslist@^4.21.4: + version "4.21.4" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987" + integrity sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw== dependencies: - caniuse-lite "^1.0.30000844" - electron-to-chromium "^1.3.47" - -browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.20.2, browserslist@^4.21.3: - version "4.21.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.3.tgz#5df277694eb3c48bc5c4b05af3e8b7e09c5a6d1a" - integrity sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ== - dependencies: - caniuse-lite "^1.0.30001370" - electron-to-chromium "^1.4.202" + caniuse-lite "^1.0.30001400" + electron-to-chromium "^1.4.251" node-releases "^2.0.6" - update-browserslist-db "^1.0.5" + update-browserslist-db "^1.0.9" bs58@^2.0.1: version "2.0.1" @@ -7919,6 +7948,11 @@ cache-content-type@^1.0.0: mime-types "^2.1.18" ylru "^1.2.0" +cacheable-lookup@^5.0.3: + version "5.0.4" + resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005" + integrity sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA== + cacheable-lookup@^6.0.0: version "6.0.4" resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-6.0.4.tgz#65c0e51721bb7f9f2cb513aed6da4a1b93ad7dc8" @@ -7937,6 +7971,19 @@ cacheable-request@^6.0.0: normalize-url "^4.1.0" responselike "^1.0.2" +cacheable-request@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.2.tgz#ea0d0b889364a25854757301ca12b2da77f91d27" + integrity sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^4.0.0" + lowercase-keys "^2.0.0" + normalize-url "^6.0.1" + responselike "^2.0.0" + cached-path-relative@^1.0.0, cached-path-relative@^1.0.2: version "1.1.0" resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.1.0.tgz#865576dfef39c0d6a7defde794d078f5308e3ef3" @@ -8033,10 +8080,10 @@ camelcase@^6.0.0, camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== -caniuse-lite@^1.0.30000810, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001312, caniuse-lite@^1.0.30001370: - version "1.0.30001312" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz#e11eba4b87e24d22697dae05455d5aea28550d5f" - integrity sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ== +caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001400, caniuse-lite@^1.0.30001426: + version "1.0.30001431" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001431.tgz#e7c59bd1bc518fae03a4656be442ce6c4887a795" + integrity sha512-zBUoFU0ZcxpvSt9IU66dXVT/3ctO1cy4y9cscs1szkPlcWb6pasYM144GqrUygUbT+k7cmUCW61cvskjcv0enQ== capture-exit@^2.0.0: version "2.0.0" @@ -9077,7 +9124,7 @@ create-ecdh@^4.0.0: bn.js "^4.1.0" elliptic "^6.0.0" -create-error-class@^3.0.0, create-error-class@^3.0.1: +create-error-class@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" integrity sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y= @@ -9112,6 +9159,13 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cron-parser@^4.5.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.6.0.tgz#404c3fdbff10ae80eef6b709555d577ef2fd2e0d" + integrity sha512-guZNLMGUgg6z4+eGhmHGw7ft+v6OQeuHzd1gcLxCo9Yg/qoxmG3nindp2/uwGCLizEisf2H0ptqeVXeoCpP6FA== + dependencies: + luxon "^3.0.1" + cross-fetch@^2.1.0, cross-fetch@^3.1.4, cross-fetch@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" @@ -9586,6 +9640,11 @@ defer-to-connect@^1.0.1: resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== +defer-to-connect@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" + integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== + deferred-leveldown@~1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/deferred-leveldown/-/deferred-leveldown-1.2.2.tgz#3acd2e0b75d1669924bc0a4b642851131173e1eb" @@ -10176,7 +10235,7 @@ drbg.js@^1.0.1: create-hash "^1.1.2" create-hmac "^1.1.4" -duplexer2@^0.1.2, duplexer2@^0.1.4, duplexer2@~0.1.0, duplexer2@~0.1.2, duplexer2@~0.1.4: +duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2, duplexer2@~0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" integrity sha1-ixLauHjA1p4+eJEFFmKjL8a93ME= @@ -10241,10 +10300,10 @@ ejs@^3.0.2: dependencies: jake "^10.6.1" -electron-to-chromium@^1.3.47, electron-to-chromium@^1.4.202: - version "1.4.206" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.206.tgz#580ff85b54d7ec0c05f20b1e37ea0becdd7b0ee4" - integrity sha512-h+Fadt1gIaQ06JaIiyqPsBjJ08fV5Q7md+V8bUvQW/9OvXfL2LRICTz2EcnnCP7QzrFTS6/27MRV6Bl9Yn97zA== +electron-to-chromium@^1.4.251: + version "1.4.284" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592" + integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA== electron@^11.1.0: version "11.5.0" @@ -12528,6 +12587,11 @@ forwarded@~0.1.2: resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= +fraction.js@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" + integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== + fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -12743,16 +12807,16 @@ gauge@~2.7.3: strip-ansi "^3.0.1" wide-align "^1.1.0" -geckodriver@^1.21.0: - version "1.21.0" - resolved "https://registry.yarnpkg.com/geckodriver/-/geckodriver-1.21.0.tgz#1f04780ebfb451ffd08fa8fddc25cc26e37ac4a2" - integrity sha512-NamdJwGIWpPiafKQIvGman95BBi/SBqHddRXAnIEpFNFCFToTW0sEA0nUckMKCBNn1DVIcLfULfyFq/sTn9bkA== +geckodriver@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/geckodriver/-/geckodriver-3.2.0.tgz#6b0a85e2aafbce209bca30e2d53af857707b1034" + integrity sha512-p+qR2RKlI/TQoCEYrSuTaYCLqsJNni96WmEukTyXmOmLn+3FLdgPAEwMZ0sG2Cwi9hozUzGAWyT6zLuhF6cpiQ== dependencies: - adm-zip "0.4.16" + adm-zip "0.5.9" bluebird "3.7.2" - got "5.6.0" - https-proxy-agent "5.0.0" - tar "6.0.2" + got "11.8.5" + https-proxy-agent "5.0.1" + tar "6.1.11" generic-names@^2.0.1: version "2.0.1" @@ -13202,27 +13266,22 @@ gonzales-pe@^4.2.3, gonzales-pe@^4.3.0: dependencies: minimist "^1.2.5" -got@5.6.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/got/-/got-5.6.0.tgz#bb1d7ee163b78082bbc8eb836f3f395004ea6fbf" - integrity sha1-ux1+4WO3gIK7yOuDbz85UATqb78= +got@11.8.5: + version "11.8.5" + resolved "https://registry.yarnpkg.com/got/-/got-11.8.5.tgz#ce77d045136de56e8f024bebb82ea349bc730046" + integrity sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ== dependencies: - create-error-class "^3.0.1" - duplexer2 "^0.1.4" - is-plain-obj "^1.0.0" - is-redirect "^1.0.0" - is-retry-allowed "^1.0.0" - is-stream "^1.0.0" - lowercase-keys "^1.0.0" - node-status-codes "^1.0.0" - object-assign "^4.0.1" - parse-json "^2.1.0" - pinkie-promise "^2.0.0" - read-all-stream "^3.0.0" - readable-stream "^2.0.5" - timed-out "^2.0.0" - unzip-response "^1.0.0" - url-parse-lax "^1.0.0" + "@sindresorhus/is" "^4.0.0" + "@szmarczak/http-timer" "^4.0.5" + "@types/cacheable-request" "^6.0.1" + "@types/responselike" "^1.0.0" + cacheable-lookup "^5.0.3" + cacheable-request "^7.0.2" + decompress-response "^6.0.0" + http2-wrapper "^1.0.0-beta.5.2" + lowercase-keys "^2.0.0" + p-cancelable "^2.0.0" + responselike "^2.0.0" got@^6.7.1: version "6.7.1" @@ -13314,17 +13373,17 @@ gud@^1.0.0: resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0" integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw== -gulp-autoprefixer@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/gulp-autoprefixer/-/gulp-autoprefixer-5.0.0.tgz#8237c278a69775270a1cafe7d6f101cfcd585544" - integrity sha1-gjfCeKaXdScKHK/n1vEBz81YVUQ= +gulp-autoprefixer@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/gulp-autoprefixer/-/gulp-autoprefixer-8.0.0.tgz#ee2413d6cb9f7cc2f01b7d9835b46e58e326b88d" + integrity sha512-sVR++PIaXpa81p52dmmA/jt50bw0egmylK5mjagfgOJ8uLDGaF9tHyzvetkY9Uo0gBZUS5sVqN3kX/GlUKOyog== dependencies: - autoprefixer "^8.0.0" - fancy-log "^1.3.2" + autoprefixer "^10.2.6" + fancy-log "^1.3.3" plugin-error "^1.0.1" - postcss "^6.0.1" - through2 "^2.0.0" - vinyl-sourcemaps-apply "^0.2.0" + postcss "^8.3.0" + through2 "^4.0.2" + vinyl-sourcemaps-apply "^0.2.1" gulp-cli@^2.2.0: version "2.3.0" @@ -13990,12 +14049,20 @@ http2-wrapper@2.0.5: quick-lru "^5.1.1" resolve-alpn "^1.1.1" +http2-wrapper@^1.0.0-beta.5.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d" + integrity sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg== + dependencies: + quick-lru "^5.1.1" + resolve-alpn "^1.0.0" + https-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= -https-proxy-agent@5, https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: +https-proxy-agent@5, https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== @@ -14003,14 +14070,6 @@ https-proxy-agent@5, https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: agent-base "6" debug "4" -https-proxy-agent@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" - integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== - dependencies: - agent-base "6" - debug "4" - human-signals@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" @@ -14744,7 +14803,7 @@ is-path-inside@^3.0.2: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== -is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: +is-plain-obj@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= @@ -15769,6 +15828,11 @@ json-buffer@3.0.0: resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + json-merge-patch@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json-merge-patch/-/json-merge-patch-1.0.2.tgz#c4626811943b2f362f8607ae8f03d528875465b0" @@ -15825,10 +15889,10 @@ json-rpc-middleware-stream@^3.0.0: "@metamask/safe-event-emitter" "^2.0.0" readable-stream "^2.3.3" -json-rpc-middleware-stream@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/json-rpc-middleware-stream/-/json-rpc-middleware-stream-4.0.0.tgz#54de6f57d795525547219c1581858962618adeeb" - integrity sha512-+s8ps2cO+zmw21W7TA9wVD/5fwNi4C2O7NzLxlrEDrfzNcs3YX76Qw2fXuQLKVFeP654CIa6nkfqkFU9o7x/3g== +json-rpc-middleware-stream@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/json-rpc-middleware-stream/-/json-rpc-middleware-stream-4.2.0.tgz#a235814e031a2f85cc14b213aa7d42b75527104b" + integrity sha512-vaPaFVnhozfAVx6ImO3YuSrzl6A1OCktDi9Prw6NASn2VRYfe3pzv+2QGzFHRjViztr61oHi6KC3k8cujrfK7A== dependencies: "@metamask/safe-event-emitter" "^2.0.0" readable-stream "^2.3.3" @@ -16076,6 +16140,13 @@ keygrip@~1.1.0: dependencies: tsscmp "1.0.6" +keyv@*, keyv@^4.0.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.0.tgz#dbce9ade79610b6e641a9a65f2f6499ba06b9bc6" + integrity sha512-2YvuMsA+jnFGtBareKqgANOEKe1mk3HKiXu2fRmAfyxG0MJAywNhi5ttWA3PMjl4NmpyjZNbFifR2vNjW1znfA== + dependencies: + json-buffer "3.0.1" + keyv@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" @@ -16814,10 +16885,10 @@ ltgt@~2.2.0: resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.2.1.tgz#f35ca91c493f7b73da0e07495304f17b31f87ee5" integrity sha1-81ypHEk/e3PaDgdJUwTxezH4fuU= -luxon@^1.26.0: - version "1.26.0" - resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.26.0.tgz#d3692361fda51473948252061d0f8561df02b578" - integrity sha512-+V5QIQ5f6CDXQpWNICELwjwuHdqeJM1UenlZWx5ujcRMc9venvluCjFb4t5NYLhb6IhkbMVOxzVuOqkgMxee2A== +luxon@^3.0.1, luxon@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.1.0.tgz#9ac33d7142b7ea18d4ec8583cdeb0b079abef60d" + integrity sha512-7w6hmKC0/aoWnEsmPCu5Br54BmbmUp5GfcqBxQngRcXJ+q5fdfjEzn7dxmJh2YdDhgW8PccYtlWKSv4tQkrTQg== madge@^5.0.1: version "5.0.1" @@ -17428,7 +17499,7 @@ minizlib@^1.3.3: dependencies: minipass "^2.9.0" -minizlib@^2.1.0, minizlib@^2.1.1: +minizlib@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== @@ -17734,7 +17805,7 @@ nanoid@^2.0.0, nanoid@^2.1.6: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280" integrity sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA== -nanoid@^3.1.31, nanoid@^3.3.1, nanoid@^3.3.3: +nanoid@^3.1.31, nanoid@^3.3.1, nanoid@^3.3.3, nanoid@^3.3.4: version "3.3.4" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== @@ -18039,11 +18110,6 @@ node-source-walk@^4.0.0, node-source-walk@^4.2.0: dependencies: "@babel/parser" "^7.0.0" -node-status-codes@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/node-status-codes/-/node-status-codes-1.0.0.tgz#5ae5541d024645d32a58fcddc9ceecea7ae3ac2f" - integrity sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8= - nofilter@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/nofilter/-/nofilter-1.0.4.tgz#78d6f4b6a613e7ced8b015cec534625f7667006e" @@ -18128,6 +18194,11 @@ normalize-url@^4.1.0: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== +normalize-url@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" + integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== + now-and-later@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-2.0.0.tgz#bc61cbb456d79cb32207ce47ca05136ff2e7d6ee" @@ -18610,6 +18681,11 @@ p-cancelable@^1.0.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== +p-cancelable@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" + integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== + p-defer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" @@ -18871,7 +18947,7 @@ parse-headers@^2.0.0: for-each "^0.3.3" string.prototype.trim "^1.1.2" -parse-json@^2.1.0, parse-json@^2.2.0: +parse-json@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= @@ -19530,15 +19606,15 @@ postcss-syntax@^0.36.2: resolved "https://registry.yarnpkg.com/postcss-syntax/-/postcss-syntax-0.36.2.tgz#f08578c7d95834574e5593a82dfbfa8afae3b51c" integrity sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w== -postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0, postcss-value-parser@^3.3.1: +postcss-value-parser@^3.3.0, postcss-value-parser@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== -postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" - integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== +postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== postcss-values-parser@^2.0.1: version "2.0.1" @@ -19567,7 +19643,7 @@ postcss@7.0.21: source-map "^0.6.1" supports-color "^6.1.0" -postcss@8.4.13, postcss@^8.1.10, postcss@^8.1.7, postcss@^8.2.13: +postcss@8.4.13: version "8.4.13" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.13.tgz#7c87bc268e79f7f86524235821dfdf9f73e5d575" integrity sha512-jtL6eTBrza5MPzy8oJLFuUscHDXTV5KcLlqAWHl5q5WYRfnNRGSmOZmOZ1T6Gy7A99mOZfqungmZMpMmCVJ8ZA== @@ -19576,7 +19652,7 @@ postcss@8.4.13, postcss@^8.1.10, postcss@^8.1.7, postcss@^8.2.13: picocolors "^1.0.0" source-map-js "^1.0.2" -postcss@^6.0.1, postcss@^6.0.19, postcss@^6.0.23: +postcss@^6.0.23: version "6.0.23" resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag== @@ -19593,6 +19669,15 @@ postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0. picocolors "^0.2.1" source-map "^0.6.1" +postcss@^8.1.10, postcss@^8.1.7, postcss@^8.2.13, postcss@^8.3.0: + version "8.4.18" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.18.tgz#6d50046ea7d3d66a85e0e782074e7203bc7fbca2" + integrity sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA== + dependencies: + nanoid "^3.3.4" + picocolors "^1.0.0" + source-map-js "^1.0.2" + prebuild-install@^5.3.4: version "5.3.6" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.3.6.tgz#7c225568d864c71d89d07f8796042733a3f54291" @@ -20565,14 +20650,6 @@ react@^16.12.0: object-assign "^4.1.1" prop-types "^15.6.2" -read-all-stream@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/read-all-stream/-/read-all-stream-3.1.0.tgz#35c3e177f2078ef789ee4bfafa4373074eaef4fa" - integrity sha1-NcPhd/IHjveJ7kv6+kNzB06u9Po= - dependencies: - pinkie-promise "^2.0.0" - readable-stream "^2.0.0" - read-installed@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/read-installed/-/read-installed-4.0.3.tgz#ff9b8b67f187d1e4c29b9feb31f6b223acd19067" @@ -21227,7 +21304,7 @@ reselect@^4.0.0: resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== -resolve-alpn@^1.1.1: +resolve-alpn@^1.0.0, resolve-alpn@^1.1.1: version "1.2.1" resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== @@ -21330,6 +21407,13 @@ responselike@^1.0.2: dependencies: lowercase-keys "^1.0.0" +responselike@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.1.tgz#9a0bc8fdc252f3fb1cca68b016591059ba1422bc" + integrity sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw== + dependencies: + lowercase-keys "^2.0.0" + restore-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" @@ -21926,10 +22010,10 @@ ses@0.12.4, ses@^0.12.4: "@agoric/make-hardener" "^0.1.2" "@agoric/transform-module" "^0.4.1" -ses@^0.15.15, ses@^0.15.17: - version "0.15.23" - resolved "https://registry.yarnpkg.com/ses/-/ses-0.15.23.tgz#067a2d856ea1304093dff5d27eaa906c495962d8" - integrity sha512-mFV5a0alaJkJcWU3AgT/2yRasEZZNv78PY6UmRdt6KhndtOMMkQl7vAvwWp9md8YBBG3zVJ5J82PT0uPY7atXw== +ses@^0.17.0: + version "0.17.0" + resolved "https://registry.yarnpkg.com/ses/-/ses-0.17.0.tgz#4e37cd1c4003e4448df2e84983900ccc5e2f095a" + integrity sha512-ObQ4DF4OgkmuPVRZLSmB1E+8jWh6lnlSpN9JHnphAUb/5J6k7da+7kj63cXrz53NDPd69rUV3DsfRBNBx8xcPQ== set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" @@ -23019,7 +23103,7 @@ superagent@^3.8.1: qs "^6.5.1" readable-stream "^2.3.5" -superstruct@^0.16.5, superstruct@^0.16.7: +superstruct@^0.16.7: version "0.16.7" resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.16.7.tgz#78bb71209d71e6107a260afc166580b137bd243a" integrity sha512-4ZZTrXlP4XzCrgh4vOfPDL6dL7zZm5aPl78eczwFSrwvxtsEnKRrSGID6Sbt0agycUoo4auRdWSNTX+oQ3KFyA== @@ -23173,15 +23257,15 @@ tar-stream@^2.1.4, tar-stream@^2.2.0: inherits "^2.0.3" readable-stream "^3.1.1" -tar@6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.2.tgz#5df17813468a6264ff14f766886c622b84ae2f39" - integrity sha512-Glo3jkRtPcvpDlAs/0+hozav78yoXKFr+c4wgw62NNMO3oo4AaJdCo21Uu7lcwr55h39W2XD1LMERc64wtbItg== +tar@6.1.11, tar@^6.0.2: + version "6.1.11" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" + integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== dependencies: chownr "^2.0.0" fs-minipass "^2.0.0" minipass "^3.0.0" - minizlib "^2.1.0" + minizlib "^2.1.1" mkdirp "^1.0.3" yallist "^4.0.0" @@ -23198,18 +23282,6 @@ tar@^4: safe-buffer "^5.2.1" yallist "^3.1.1" -tar@^6.0.2: - version "6.1.3" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.3.tgz#e44b97ee7d6cc7a4c574e8b01174614538291825" - integrity sha512-3rUqwucgVZXTeyJyL2jqtUau8/8r54SioM1xj3AmTX3HnWQdj2AydfJ2qYYayPyIIznSplcvU9mhBb7dR2XF3w== - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^3.0.0" - minizlib "^2.1.1" - mkdirp "^1.0.3" - yallist "^4.0.0" - tcp-port-used@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/tcp-port-used/-/tcp-port-used-1.0.1.tgz#46061078e2d38c73979a2c2c12b5a674e6689d70" @@ -23395,11 +23467,6 @@ time-stamp@^1.0.0: resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3" integrity sha1-dkpaEa9QVhkhsTPztE5hhofg9cM= -timed-out@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-2.0.0.tgz#f38b0ae81d3747d628001f41dafc652ace671c0a" - integrity sha1-84sK6B03R9YoAB9B2vxlKs5nHAo= - timed-out@^4.0.0, timed-out@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" @@ -24145,11 +24212,6 @@ untildify@^2.0.0: dependencies: os-homedir "^1.0.0" -unzip-response@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-1.0.2.tgz#b984f0877fc0a89c2c773cc1ef7b5b232b5b06fe" - integrity sha1-uYTwh3/AqJwsdzzB73tbIytbBv4= - unzip-response@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" @@ -24165,10 +24227,10 @@ upath@^1.1.1: resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== -update-browserslist-db@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz#be06a5eedd62f107b7c19eb5bcefb194411abf38" - integrity sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q== +update-browserslist-db@^1.0.9: + version "1.0.10" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" + integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== dependencies: escalade "^3.1.1" picocolors "^1.0.0"