mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-26 12:29:06 +01:00
Merge remote-tracking branch 'origin/develop' into master-sync
This commit is contained in:
commit
74719a8102
@ -12,7 +12,7 @@ executors:
|
|||||||
NODE_OPTIONS: --max_old_space_size=2048
|
NODE_OPTIONS: --max_old_space_size=2048
|
||||||
shellcheck:
|
shellcheck:
|
||||||
docker:
|
docker:
|
||||||
- image: koalaman/shellcheck-alpine@sha256:35882cba254810c7de458528011e935ba2c4f3ebcb224275dfa7ebfa930ef294
|
- image: koalaman/shellcheck-alpine@sha256:dfaf08fab58c158549d3be64fb101c626abc5f16f341b569092577ae207db199
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
test_and_release:
|
test_and_release:
|
||||||
@ -25,7 +25,9 @@ workflows:
|
|||||||
only:
|
only:
|
||||||
- /^Version-v(\d+)[.](\d+)[.](\d+)/
|
- /^Version-v(\d+)[.](\d+)[.](\d+)/
|
||||||
- prep-deps
|
- prep-deps
|
||||||
- test-deps-audit
|
- test-deps-audit:
|
||||||
|
requires:
|
||||||
|
- prep-deps
|
||||||
- test-deps-depcheck:
|
- test-deps-depcheck:
|
||||||
requires:
|
requires:
|
||||||
- prep-deps
|
- prep-deps
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
set -u
|
set -u
|
||||||
|
set -x
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
|
|
||||||
# use `improved-yarn-audit` since that allows for exclude
|
# use `improved-yarn-audit` since that allows for exclude
|
||||||
|
70
.github/workflows/codeql-analysis.yml
vendored
Normal file
70
.github/workflows/codeql-analysis.yml
vendored
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
|
# to commit it to your repository.
|
||||||
|
#
|
||||||
|
# You may wish to alter this file to override the set of languages analyzed,
|
||||||
|
# or to provide custom queries or build logic.
|
||||||
|
#
|
||||||
|
# ******** NOTE ********
|
||||||
|
# We have attempted to detect the languages in your repository. Please check
|
||||||
|
# the `language` matrix defined below to confirm you have the correct set of
|
||||||
|
# supported CodeQL languages.
|
||||||
|
#
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ develop, Version-v*, cla-signatures, master, snaps ]
|
||||||
|
pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [ develop ]
|
||||||
|
schedule:
|
||||||
|
- cron: '28 12 * * 0'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [ 'javascript' ]
|
||||||
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||||
|
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v1
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v1
|
||||||
|
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 https://git.io/JvXDl
|
||||||
|
|
||||||
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||||
|
# and modify them (or add more) to build your code if your project
|
||||||
|
# uses a compiled language
|
||||||
|
|
||||||
|
#- run: |
|
||||||
|
# make bootstrap
|
||||||
|
# make release
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v1
|
@ -1,5 +1,5 @@
|
|||||||
node_modules/**
|
node_modules/**
|
||||||
lavamoat/*/policy.json
|
lavamoat/**/policy.json
|
||||||
dist/**
|
dist/**
|
||||||
builds/**
|
builds/**
|
||||||
test-*/**
|
test-*/**
|
||||||
|
14
README.md
14
README.md
@ -67,9 +67,17 @@ Whenever you change dependencies (adding, removing, or updating, either in `pack
|
|||||||
* The `allow-scripts` configuration in `package.json`
|
* The `allow-scripts` configuration in `package.json`
|
||||||
* Run `yarn allow-scripts auto` to update the `allow-scripts` configuration automatically. This config determines whether the package's install/postinstall scripts are allowed to run. Review each new package to determine whether the install script needs to run or not, testing if necessary.
|
* Run `yarn allow-scripts auto` to update the `allow-scripts` configuration automatically. This config determines whether the package's install/postinstall scripts are allowed to run. Review each new package to determine whether the install script needs to run or not, testing if necessary.
|
||||||
* Unfortunately, `yarn allow-scripts auto` will behave inconsistently on different platforms. macOS and Windows users may see extraneous changes relating to optional dependencies.
|
* Unfortunately, `yarn allow-scripts auto` will behave inconsistently on different platforms. macOS and Windows users may see extraneous changes relating to optional dependencies.
|
||||||
* The LavaMoat auto-generated policy in `lavamoat/node/policy.json`
|
* The LavaMoat policy files. The _tl;dr_ is to run `yarn lavamoat:auto` to update these files, but there can be devils in the details. Continue reading for more information.
|
||||||
* Run `yarn lavamoat:auto` to re-generate this policy file. Review the changes to determine whether the access granted to each package seems appropriate.
|
* There are two sets of LavaMoat policy files:
|
||||||
* Unfortunately, `yarn lavamoat:auto` will behave inconsistently on different platforms. macOS and Windows users may see extraneous changes relating to optional dependencies.
|
* The production LavaMoat policy files (`lavamoat/browserify/*/policy.json`), which are re-generated using `yarn lavamoat:background:auto`.
|
||||||
|
* These should be regenerated whenever the production dependencies for the background change.
|
||||||
|
* The build system LavaMoat policy file (`lavamoat/build-system/policy.json`), which is re-generated using `yarn lavamoat:build:auto`.
|
||||||
|
* This should be regenerated whenever the dependencies used by the build system itself change.
|
||||||
|
* Whenever you regenerate a policy file, review the changes to determine whether the access granted to each package seems appropriate.
|
||||||
|
* Unfortunately, `yarn lavamoat:auto` will behave inconsistently on different platforms.
|
||||||
|
macOS and Windows users may see extraneous changes relating to optional dependencies.
|
||||||
|
* Keep in mind that any kind of dynamic import or dynamic use of globals may elude LavaMoat's static analysis.
|
||||||
|
Refer to the LavaMoat documentation or ask for help if you run into any issues.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
@ -43,9 +43,15 @@
|
|||||||
"activityLog": {
|
"activityLog": {
|
||||||
"message": "activity log"
|
"message": "activity log"
|
||||||
},
|
},
|
||||||
|
"add": {
|
||||||
|
"message": "Add"
|
||||||
|
},
|
||||||
"addANetwork": {
|
"addANetwork": {
|
||||||
"message": "Add a network"
|
"message": "Add a network"
|
||||||
},
|
},
|
||||||
|
"addANickname": {
|
||||||
|
"message": "Add a nickname"
|
||||||
|
},
|
||||||
"addAcquiredTokens": {
|
"addAcquiredTokens": {
|
||||||
"message": "Add the tokens you've acquired using MetaMask"
|
"message": "Add the tokens you've acquired using MetaMask"
|
||||||
},
|
},
|
||||||
@ -82,6 +88,9 @@
|
|||||||
"addFriendsAndAddresses": {
|
"addFriendsAndAddresses": {
|
||||||
"message": "Add friends and addresses you trust"
|
"message": "Add friends and addresses you trust"
|
||||||
},
|
},
|
||||||
|
"addMemo": {
|
||||||
|
"message": "Add memo"
|
||||||
|
},
|
||||||
"addNFT": {
|
"addNFT": {
|
||||||
"message": "Add NFT"
|
"message": "Add NFT"
|
||||||
},
|
},
|
||||||
@ -100,6 +109,9 @@
|
|||||||
"addToken": {
|
"addToken": {
|
||||||
"message": "Add Token"
|
"message": "Add Token"
|
||||||
},
|
},
|
||||||
|
"address": {
|
||||||
|
"message": "Address"
|
||||||
|
},
|
||||||
"addressBookIcon": {
|
"addressBookIcon": {
|
||||||
"message": "Address book icon"
|
"message": "Address book icon"
|
||||||
},
|
},
|
||||||
@ -167,6 +179,14 @@
|
|||||||
"message": "MetaMask",
|
"message": "MetaMask",
|
||||||
"description": "The name of the application"
|
"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)"
|
||||||
|
},
|
||||||
"approvalAndAggregatorTxFeeCost": {
|
"approvalAndAggregatorTxFeeCost": {
|
||||||
"message": "Approval and aggregator network fee"
|
"message": "Approval and aggregator network fee"
|
||||||
},
|
},
|
||||||
@ -549,6 +569,9 @@
|
|||||||
"currentlyUnavailable": {
|
"currentlyUnavailable": {
|
||||||
"message": "Unavailable on this network"
|
"message": "Unavailable on this network"
|
||||||
},
|
},
|
||||||
|
"custom": {
|
||||||
|
"message": "Advanced"
|
||||||
|
},
|
||||||
"customGas": {
|
"customGas": {
|
||||||
"message": "Customize Gas"
|
"message": "Customize Gas"
|
||||||
},
|
},
|
||||||
@ -561,6 +584,16 @@
|
|||||||
"customToken": {
|
"customToken": {
|
||||||
"message": "Custom Token"
|
"message": "Custom Token"
|
||||||
},
|
},
|
||||||
|
"dappSuggested": {
|
||||||
|
"message": "Site suggested"
|
||||||
|
},
|
||||||
|
"dappSuggestedShortLabel": {
|
||||||
|
"message": "Site"
|
||||||
|
},
|
||||||
|
"dappSuggestedTooltip": {
|
||||||
|
"message": "$1 has recommended this price.",
|
||||||
|
"description": "$1 represents the Dapp's origin"
|
||||||
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"message": "Data"
|
"message": "Data"
|
||||||
},
|
},
|
||||||
@ -665,6 +698,9 @@
|
|||||||
"edit": {
|
"edit": {
|
||||||
"message": "Edit"
|
"message": "Edit"
|
||||||
},
|
},
|
||||||
|
"editAddressNickname": {
|
||||||
|
"message": "Edit address nickname"
|
||||||
|
},
|
||||||
"editContact": {
|
"editContact": {
|
||||||
"message": "Edit Contact"
|
"message": "Edit Contact"
|
||||||
},
|
},
|
||||||
@ -686,6 +722,9 @@
|
|||||||
"editGasEducationModalTitle": {
|
"editGasEducationModalTitle": {
|
||||||
"message": "How to choose?"
|
"message": "How to choose?"
|
||||||
},
|
},
|
||||||
|
"editGasFeeModalTitle": {
|
||||||
|
"message": "Edit gas fee"
|
||||||
|
},
|
||||||
"editGasHigh": {
|
"editGasHigh": {
|
||||||
"message": "High"
|
"message": "High"
|
||||||
},
|
},
|
||||||
@ -891,6 +930,9 @@
|
|||||||
"etherscanView": {
|
"etherscanView": {
|
||||||
"message": "View account on Etherscan"
|
"message": "View account on Etherscan"
|
||||||
},
|
},
|
||||||
|
"etherscanViewOn": {
|
||||||
|
"message": "View on Etherscan"
|
||||||
|
},
|
||||||
"expandView": {
|
"expandView": {
|
||||||
"message": "Expand view"
|
"message": "Expand view"
|
||||||
},
|
},
|
||||||
@ -984,6 +1026,9 @@
|
|||||||
"message": "Gas limit must be at least $1",
|
"message": "Gas limit must be at least $1",
|
||||||
"description": "$1 is the custom gas limit, in decimal."
|
"description": "$1 is the custom gas limit, in decimal."
|
||||||
},
|
},
|
||||||
|
"gasOption": {
|
||||||
|
"message": "Gas option"
|
||||||
|
},
|
||||||
"gasPrice": {
|
"gasPrice": {
|
||||||
"message": "Gas Price (GWEI)"
|
"message": "Gas Price (GWEI)"
|
||||||
},
|
},
|
||||||
@ -1002,10 +1047,18 @@
|
|||||||
"gasPriceInfoTooltipContent": {
|
"gasPriceInfoTooltipContent": {
|
||||||
"message": "Gas price specifies the amount of Ether you are willing to pay for each unit of gas."
|
"message": "Gas price specifies the amount of Ether you are willing to pay for each unit of gas."
|
||||||
},
|
},
|
||||||
|
"gasTimingHoursShort": {
|
||||||
|
"message": "$1 hrs",
|
||||||
|
"description": "$1 represents a number of hours"
|
||||||
|
},
|
||||||
"gasTimingMinutes": {
|
"gasTimingMinutes": {
|
||||||
"message": "$1 minutes",
|
"message": "$1 minutes",
|
||||||
"description": "$1 represents a number of minutes"
|
"description": "$1 represents a number of minutes"
|
||||||
},
|
},
|
||||||
|
"gasTimingMinutesShort": {
|
||||||
|
"message": "$1 min",
|
||||||
|
"description": "$1 represents a number of minutes"
|
||||||
|
},
|
||||||
"gasTimingNegative": {
|
"gasTimingNegative": {
|
||||||
"message": "Maybe in $1",
|
"message": "Maybe in $1",
|
||||||
"description": "$1 represents an amount of time"
|
"description": "$1 represents an amount of time"
|
||||||
@ -1018,6 +1071,10 @@
|
|||||||
"message": "$1 seconds",
|
"message": "$1 seconds",
|
||||||
"description": "$1 represents a number of seconds"
|
"description": "$1 represents a number of seconds"
|
||||||
},
|
},
|
||||||
|
"gasTimingSecondsShort": {
|
||||||
|
"message": "$1 sec",
|
||||||
|
"description": "$1 represents a number of seconds"
|
||||||
|
},
|
||||||
"gasTimingVeryPositive": {
|
"gasTimingVeryPositive": {
|
||||||
"message": "Very likely in < $1",
|
"message": "Very likely in < $1",
|
||||||
"description": "$1 represents an amount of time"
|
"description": "$1 represents an amount of time"
|
||||||
@ -1100,9 +1157,15 @@
|
|||||||
"hideZeroBalanceTokens": {
|
"hideZeroBalanceTokens": {
|
||||||
"message": "Hide Tokens Without Balance"
|
"message": "Hide Tokens Without Balance"
|
||||||
},
|
},
|
||||||
|
"high": {
|
||||||
|
"message": "Aggressive"
|
||||||
|
},
|
||||||
"history": {
|
"history": {
|
||||||
"message": "History"
|
"message": "History"
|
||||||
},
|
},
|
||||||
|
"id": {
|
||||||
|
"message": "ID"
|
||||||
|
},
|
||||||
"import": {
|
"import": {
|
||||||
"message": "Import",
|
"message": "Import",
|
||||||
"description": "Button to import an account from a selected file"
|
"description": "Button to import an account from a selected file"
|
||||||
@ -1117,7 +1180,7 @@
|
|||||||
"message": "import using Secret Recovery Phrase"
|
"message": "import using Secret Recovery Phrase"
|
||||||
},
|
},
|
||||||
"importAccountMsg": {
|
"importAccountMsg": {
|
||||||
"message": " Imported accounts will not be associated with your originally created MetaMask account Secret Recovery Phrase. Learn more about imported accounts "
|
"message": "Imported accounts will not be associated with your originally created MetaMask account Secret Recovery Phrase. Learn more about imported accounts"
|
||||||
},
|
},
|
||||||
"importAccountSeedPhrase": {
|
"importAccountSeedPhrase": {
|
||||||
"message": "Import a wallet with Secret Recovery Phrase"
|
"message": "Import a wallet with Secret Recovery Phrase"
|
||||||
@ -1336,6 +1399,12 @@
|
|||||||
"lockTimeTooGreat": {
|
"lockTimeTooGreat": {
|
||||||
"message": "Lock time is too great"
|
"message": "Lock time is too great"
|
||||||
},
|
},
|
||||||
|
"low": {
|
||||||
|
"message": "Low"
|
||||||
|
},
|
||||||
|
"lowPriorityMessage": {
|
||||||
|
"message": "Future transactions will queue after this one. This price was last seen was some time ago."
|
||||||
|
},
|
||||||
"mainnet": {
|
"mainnet": {
|
||||||
"message": "Ethereum Mainnet"
|
"message": "Ethereum Mainnet"
|
||||||
},
|
},
|
||||||
@ -1349,12 +1418,18 @@
|
|||||||
"max": {
|
"max": {
|
||||||
"message": "Max"
|
"message": "Max"
|
||||||
},
|
},
|
||||||
|
"maxBaseFee": {
|
||||||
|
"message": "Max base fee"
|
||||||
|
},
|
||||||
"maxFee": {
|
"maxFee": {
|
||||||
"message": "Max fee"
|
"message": "Max fee"
|
||||||
},
|
},
|
||||||
"maxPriorityFee": {
|
"maxPriorityFee": {
|
||||||
"message": "Max priority fee"
|
"message": "Max priority fee"
|
||||||
},
|
},
|
||||||
|
"medium": {
|
||||||
|
"message": "Market"
|
||||||
|
},
|
||||||
"memo": {
|
"memo": {
|
||||||
"message": "memo"
|
"message": "memo"
|
||||||
},
|
},
|
||||||
@ -1538,6 +1613,12 @@
|
|||||||
"newContract": {
|
"newContract": {
|
||||||
"message": "New Contract"
|
"message": "New Contract"
|
||||||
},
|
},
|
||||||
|
"newNFTsDetected": {
|
||||||
|
"message": "New NFTs detected"
|
||||||
|
},
|
||||||
|
"newNFTsDetectedInfo": {
|
||||||
|
"message": "One or more new NFTs were detected in your wallet."
|
||||||
|
},
|
||||||
"newNetworkAdded": {
|
"newNetworkAdded": {
|
||||||
"message": "“$1” was successfully added!"
|
"message": "“$1” was successfully added!"
|
||||||
},
|
},
|
||||||
@ -1560,9 +1641,15 @@
|
|||||||
"message": "Nonce is higher than suggested nonce of $1",
|
"message": "Nonce is higher than suggested nonce of $1",
|
||||||
"description": "The next nonce according to MetaMask's internal logic"
|
"description": "The next nonce according to MetaMask's internal logic"
|
||||||
},
|
},
|
||||||
|
"nftTokenIdPlaceholder": {
|
||||||
|
"message": "Enter the collectible ID"
|
||||||
|
},
|
||||||
"nfts": {
|
"nfts": {
|
||||||
"message": "NFTs"
|
"message": "NFTs"
|
||||||
},
|
},
|
||||||
|
"nickname": {
|
||||||
|
"message": "Nickname"
|
||||||
|
},
|
||||||
"noAccountsFound": {
|
"noAccountsFound": {
|
||||||
"message": "No accounts found for the given search query"
|
"message": "No accounts found for the given search query"
|
||||||
},
|
},
|
||||||
@ -2129,6 +2216,9 @@
|
|||||||
"selectHdPath": {
|
"selectHdPath": {
|
||||||
"message": "Select HD Path"
|
"message": "Select HD Path"
|
||||||
},
|
},
|
||||||
|
"selectNFTPrivacyPreference": {
|
||||||
|
"message": "Select NFT privacy preference"
|
||||||
|
},
|
||||||
"selectPathHelp": {
|
"selectPathHelp": {
|
||||||
"message": "If you don't see the accounts you expect, try switching the HD path."
|
"message": "If you don't see the accounts you expect, try switching the HD path."
|
||||||
},
|
},
|
||||||
@ -2236,6 +2326,9 @@
|
|||||||
"signed": {
|
"signed": {
|
||||||
"message": "Signed"
|
"message": "Signed"
|
||||||
},
|
},
|
||||||
|
"simulationErrorMessage": {
|
||||||
|
"message": "This transaction is expected to fail. Trying to execute it is expected to be expensive but fail, and is not recommended."
|
||||||
|
},
|
||||||
"skip": {
|
"skip": {
|
||||||
"message": "Skip"
|
"message": "Skip"
|
||||||
},
|
},
|
||||||
@ -2742,6 +2835,9 @@
|
|||||||
"thisWillCreate": {
|
"thisWillCreate": {
|
||||||
"message": "This will create a new wallet and Secret Recovery Phrase"
|
"message": "This will create a new wallet and Secret Recovery Phrase"
|
||||||
},
|
},
|
||||||
|
"time": {
|
||||||
|
"message": "Time"
|
||||||
|
},
|
||||||
"tips": {
|
"tips": {
|
||||||
"message": "Tips"
|
"message": "Tips"
|
||||||
},
|
},
|
||||||
@ -2896,6 +2992,9 @@
|
|||||||
"tryAgain": {
|
"tryAgain": {
|
||||||
"message": "Try again"
|
"message": "Try again"
|
||||||
},
|
},
|
||||||
|
"tryAnywayOption": {
|
||||||
|
"message": "I will try anyway"
|
||||||
|
},
|
||||||
"turnOnTokenDetection": {
|
"turnOnTokenDetection": {
|
||||||
"message": "Turn on enhanced token detection"
|
"message": "Turn on enhanced token detection"
|
||||||
},
|
},
|
||||||
|
@ -503,7 +503,7 @@
|
|||||||
"message": "编辑权限"
|
"message": "编辑权限"
|
||||||
},
|
},
|
||||||
"encryptionPublicKeyNotice": {
|
"encryptionPublicKeyNotice": {
|
||||||
"message": "$1 希望得到您的加密公钥。同意后该网站将可以想您发送加密信息。",
|
"message": "$1 希望得到您的加密公钥。同意后该网站将可以向您发送加密信息。",
|
||||||
"description": "$1 is the web3 site name"
|
"description": "$1 is the web3 site name"
|
||||||
},
|
},
|
||||||
"encryptionPublicKeyRequest": {
|
"encryptionPublicKeyRequest": {
|
||||||
|
@ -21,6 +21,6 @@
|
|||||||
"128": "images/icon-128.png",
|
"128": "images/icon-128.png",
|
||||||
"512": "images/icon-512.png"
|
"512": "images/icon-512.png"
|
||||||
},
|
},
|
||||||
"name": "__MSG_appName__ Beta",
|
"name": "__MSG_appNameBeta__",
|
||||||
"short_name": "__MSG_appName__ Beta"
|
"short_name": "__MSG_appNameBeta__"
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,6 @@
|
|||||||
"128": "images/icon-128.png",
|
"128": "images/icon-128.png",
|
||||||
"512": "images/icon-512.png"
|
"512": "images/icon-512.png"
|
||||||
},
|
},
|
||||||
"name": "__MSG_appName__ Flask",
|
"name": "__MSG_appNameFlask__",
|
||||||
"short_name": "__MSG_appName__ Flask"
|
"short_name": "__MSG_appNameFlask__"
|
||||||
}
|
}
|
||||||
|
@ -17,13 +17,19 @@ import {
|
|||||||
ENVIRONMENT_TYPE_FULLSCREEN,
|
ENVIRONMENT_TYPE_FULLSCREEN,
|
||||||
} from '../../shared/constants/app';
|
} from '../../shared/constants/app';
|
||||||
import { SECOND } from '../../shared/constants/time';
|
import { SECOND } from '../../shared/constants/time';
|
||||||
|
import {
|
||||||
|
REJECT_NOTFICIATION_CLOSE,
|
||||||
|
REJECT_NOTFICIATION_CLOSE_SIG,
|
||||||
|
} from '../../shared/constants/metametrics';
|
||||||
import migrations from './migrations';
|
import migrations from './migrations';
|
||||||
import Migrator from './lib/migrator';
|
import Migrator from './lib/migrator';
|
||||||
import ExtensionPlatform from './platforms/extension';
|
import ExtensionPlatform from './platforms/extension';
|
||||||
import LocalStore from './lib/local-store';
|
import LocalStore from './lib/local-store';
|
||||||
import ReadOnlyNetworkStore from './lib/network-store';
|
import ReadOnlyNetworkStore from './lib/network-store';
|
||||||
import createStreamSink from './lib/createStreamSink';
|
import createStreamSink from './lib/createStreamSink';
|
||||||
import NotificationManager from './lib/notification-manager';
|
import NotificationManager, {
|
||||||
|
NOTIFICATION_MANAGER_EVENTS,
|
||||||
|
} from './lib/notification-manager';
|
||||||
import MetamaskController, {
|
import MetamaskController, {
|
||||||
METAMASK_CONTROLLER_EVENTS,
|
METAMASK_CONTROLLER_EVENTS,
|
||||||
} from './metamask-controller';
|
} from './metamask-controller';
|
||||||
@ -475,6 +481,69 @@ function setupController(initState, initLangCode) {
|
|||||||
extension.browserAction.setBadgeBackgroundColor({ color: '#037DD6' });
|
extension.browserAction.setBadgeBackgroundColor({ color: '#037DD6' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notificationManager.on(
|
||||||
|
NOTIFICATION_MANAGER_EVENTS.POPUP_CLOSED,
|
||||||
|
rejectUnapprovedNotifications,
|
||||||
|
);
|
||||||
|
|
||||||
|
function rejectUnapprovedNotifications() {
|
||||||
|
Object.keys(
|
||||||
|
controller.txController.txStateManager.getUnapprovedTxList(),
|
||||||
|
).forEach((txId) =>
|
||||||
|
controller.txController.txStateManager.setTxStatusRejected(txId),
|
||||||
|
);
|
||||||
|
controller.messageManager.messages
|
||||||
|
.filter((msg) => msg.status === 'unapproved')
|
||||||
|
.forEach((tx) =>
|
||||||
|
controller.messageManager.rejectMsg(
|
||||||
|
tx.id,
|
||||||
|
REJECT_NOTFICIATION_CLOSE_SIG,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
controller.personalMessageManager.messages
|
||||||
|
.filter((msg) => msg.status === 'unapproved')
|
||||||
|
.forEach((tx) =>
|
||||||
|
controller.personalMessageManager.rejectMsg(
|
||||||
|
tx.id,
|
||||||
|
REJECT_NOTFICIATION_CLOSE_SIG,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
controller.typedMessageManager.messages
|
||||||
|
.filter((msg) => msg.status === 'unapproved')
|
||||||
|
.forEach((tx) =>
|
||||||
|
controller.typedMessageManager.rejectMsg(
|
||||||
|
tx.id,
|
||||||
|
REJECT_NOTFICIATION_CLOSE_SIG,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
controller.decryptMessageManager.messages
|
||||||
|
.filter((msg) => msg.status === 'unapproved')
|
||||||
|
.forEach((tx) =>
|
||||||
|
controller.decryptMessageManager.rejectMsg(
|
||||||
|
tx.id,
|
||||||
|
REJECT_NOTFICIATION_CLOSE,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
controller.encryptionPublicKeyManager.messages
|
||||||
|
.filter((msg) => msg.status === 'unapproved')
|
||||||
|
.forEach((tx) =>
|
||||||
|
controller.encryptionPublicKeyManager.rejectMsg(
|
||||||
|
tx.id,
|
||||||
|
REJECT_NOTFICIATION_CLOSE,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// We're specifcally avoid using approvalController directly for better
|
||||||
|
// Error support during rejection
|
||||||
|
Object.keys(
|
||||||
|
controller.permissionsController.approvals.state.pendingApprovals,
|
||||||
|
).forEach((approvalId) =>
|
||||||
|
controller.permissionsController.rejectPermissionsRequest(approvalId),
|
||||||
|
);
|
||||||
|
|
||||||
|
updateBadge();
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import { createScaffoldMiddleware, mergeMiddleware } from 'json-rpc-engine';
|
import { createScaffoldMiddleware, mergeMiddleware } from 'json-rpc-engine';
|
||||||
import createBlockRefMiddleware from 'eth-json-rpc-middleware/block-ref';
|
import {
|
||||||
import createRetryOnEmptyMiddleware from 'eth-json-rpc-middleware/retryOnEmpty';
|
createBlockRefMiddleware,
|
||||||
import createBlockCacheMiddleware from 'eth-json-rpc-middleware/block-cache';
|
createRetryOnEmptyMiddleware,
|
||||||
import createInflightCacheMiddleware from 'eth-json-rpc-middleware/inflight-cache';
|
createBlockCacheMiddleware,
|
||||||
import createBlockTrackerInspectorMiddleware from 'eth-json-rpc-middleware/block-tracker-inspector';
|
createInflightCacheMiddleware,
|
||||||
import providerFromMiddleware from 'eth-json-rpc-middleware/providerFromMiddleware';
|
createBlockTrackerInspectorMiddleware,
|
||||||
|
providerFromMiddleware,
|
||||||
|
} from 'eth-json-rpc-middleware';
|
||||||
|
|
||||||
import createInfuraMiddleware from 'eth-json-rpc-infura';
|
import createInfuraMiddleware from 'eth-json-rpc-infura';
|
||||||
import { PollingBlockTracker } from 'eth-block-tracker';
|
import { PollingBlockTracker } from 'eth-block-tracker';
|
||||||
|
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { createAsyncMiddleware, mergeMiddleware } from 'json-rpc-engine';
|
import { createAsyncMiddleware, mergeMiddleware } from 'json-rpc-engine';
|
||||||
import createFetchMiddleware from 'eth-json-rpc-middleware/fetch';
|
import {
|
||||||
import createBlockRefRewriteMiddleware from 'eth-json-rpc-middleware/block-ref-rewrite';
|
createFetchMiddleware,
|
||||||
import createBlockCacheMiddleware from 'eth-json-rpc-middleware/block-cache';
|
createBlockRefRewriteMiddleware,
|
||||||
import createInflightMiddleware from 'eth-json-rpc-middleware/inflight-cache';
|
createBlockCacheMiddleware,
|
||||||
import createBlockTrackerInspectorMiddleware from 'eth-json-rpc-middleware/block-tracker-inspector';
|
createInflightCacheMiddleware,
|
||||||
import providerFromMiddleware from 'eth-json-rpc-middleware/providerFromMiddleware';
|
createBlockTrackerInspectorMiddleware,
|
||||||
|
providerFromMiddleware,
|
||||||
|
} from 'eth-json-rpc-middleware';
|
||||||
import { PollingBlockTracker } from 'eth-block-tracker';
|
import { PollingBlockTracker } from 'eth-block-tracker';
|
||||||
import { SECOND } from '../../../../shared/constants/time';
|
import { SECOND } from '../../../../shared/constants/time';
|
||||||
|
|
||||||
@ -27,7 +29,7 @@ export default function createJsonRpcClient({ rpcUrl, chainId }) {
|
|||||||
createChainIdMiddleware(chainId),
|
createChainIdMiddleware(chainId),
|
||||||
createBlockRefRewriteMiddleware({ blockTracker }),
|
createBlockRefRewriteMiddleware({ blockTracker }),
|
||||||
createBlockCacheMiddleware({ blockTracker }),
|
createBlockCacheMiddleware({ blockTracker }),
|
||||||
createInflightMiddleware(),
|
createInflightCacheMiddleware(),
|
||||||
createBlockTrackerInspectorMiddleware({ blockTracker }),
|
createBlockTrackerInspectorMiddleware({ blockTracker }),
|
||||||
fetchMiddleware,
|
fetchMiddleware,
|
||||||
]);
|
]);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { createScaffoldMiddleware, mergeMiddleware } from 'json-rpc-engine';
|
import { createScaffoldMiddleware, mergeMiddleware } from 'json-rpc-engine';
|
||||||
import createWalletSubprovider from 'eth-json-rpc-middleware/wallet';
|
import { createWalletMiddleware } from 'eth-json-rpc-middleware';
|
||||||
import {
|
import {
|
||||||
createPendingNonceMiddleware,
|
createPendingNonceMiddleware,
|
||||||
createPendingTxMiddleware,
|
createPendingTxMiddleware,
|
||||||
@ -21,11 +21,10 @@ export default function createMetamaskMiddleware({
|
|||||||
}) {
|
}) {
|
||||||
const metamaskMiddleware = mergeMiddleware([
|
const metamaskMiddleware = mergeMiddleware([
|
||||||
createScaffoldMiddleware({
|
createScaffoldMiddleware({
|
||||||
// staticSubprovider
|
|
||||||
eth_syncing: false,
|
eth_syncing: false,
|
||||||
web3_clientVersion: `MetaMask/v${version}`,
|
web3_clientVersion: `MetaMask/v${version}`,
|
||||||
}),
|
}),
|
||||||
createWalletSubprovider({
|
createWalletMiddleware({
|
||||||
getAccounts,
|
getAccounts,
|
||||||
processTransaction,
|
processTransaction,
|
||||||
processEthSignMessage,
|
processEthSignMessage,
|
||||||
|
@ -2,7 +2,7 @@ import { strict as assert } from 'assert';
|
|||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import { ComposedStore, ObservableStore } from '@metamask/obs-store';
|
import { ComposedStore, ObservableStore } from '@metamask/obs-store';
|
||||||
import { JsonRpcEngine } from 'json-rpc-engine';
|
import { JsonRpcEngine } from 'json-rpc-engine';
|
||||||
import providerFromEngine from 'eth-json-rpc-middleware/providerFromEngine';
|
import { providerFromEngine } from 'eth-json-rpc-middleware';
|
||||||
import log from 'loglevel';
|
import log from 'loglevel';
|
||||||
import {
|
import {
|
||||||
createSwappableProxy,
|
createSwappableProxy,
|
||||||
@ -430,7 +430,7 @@ export default class NetworkController extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_setProviderAndBlockTracker({ provider, blockTracker }) {
|
_setProviderAndBlockTracker({ provider, blockTracker }) {
|
||||||
// update or intialize proxies
|
// update or initialize proxies
|
||||||
if (this._providerProxy) {
|
if (this._providerProxy) {
|
||||||
this._providerProxy.setTarget(provider);
|
this._providerProxy.setTarget(provider);
|
||||||
} else {
|
} else {
|
||||||
|
@ -37,6 +37,7 @@ export default class PreferencesController {
|
|||||||
// set to true means the dynamic list from the API is being used
|
// 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
|
// set to false will be using the static list from contract-metadata
|
||||||
useTokenDetection: false,
|
useTokenDetection: false,
|
||||||
|
advancedGasFee: null,
|
||||||
|
|
||||||
// WARNING: Do not use feature flags for security-sensitive things.
|
// WARNING: Do not use feature flags for security-sensitive things.
|
||||||
// Feature flag toggling is available in the global namespace
|
// Feature flag toggling is available in the global namespace
|
||||||
@ -129,6 +130,16 @@ export default class PreferencesController {
|
|||||||
this.store.updateState({ useTokenDetection: val });
|
this.store.updateState({ useTokenDetection: val });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setter for the `advancedGasFee` property
|
||||||
|
*
|
||||||
|
* @param {object} val - holds the maxBaseFee and PriorityFee that the user set as default advanced settings.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
setAdvancedGasFee(val) {
|
||||||
|
this.store.updateState({ advancedGasFee: val });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add new methodData to state, to avoid requesting this information again through Infura
|
* Add new methodData to state, to avoid requesting this information again through Infura
|
||||||
*
|
*
|
||||||
|
@ -266,4 +266,28 @@ describe('preferences controller', function () {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('setAdvancedGasFee', function () {
|
||||||
|
it('should default to null', function () {
|
||||||
|
const state = preferencesController.store.getState();
|
||||||
|
assert.equal(state.advancedGasFee, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the setAdvancedGasFee property in state', function () {
|
||||||
|
const state = preferencesController.store.getState();
|
||||||
|
assert.equal(state.advancedGasFee, null);
|
||||||
|
preferencesController.setAdvancedGasFee({
|
||||||
|
maxBaseFee: '1.5',
|
||||||
|
priorityFee: '2',
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
preferencesController.store.getState().advancedGasFee.maxBaseFee,
|
||||||
|
'1.5',
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
preferencesController.store.getState().advancedGasFee.priorityFee,
|
||||||
|
'2',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -28,6 +28,7 @@ import {
|
|||||||
} from '../../../ui/pages/swaps/swaps.util';
|
} from '../../../ui/pages/swaps/swaps.util';
|
||||||
import fetchWithCache from '../../../ui/helpers/utils/fetch-with-cache';
|
import fetchWithCache from '../../../ui/helpers/utils/fetch-with-cache';
|
||||||
import { MINUTE, SECOND } from '../../../shared/constants/time';
|
import { MINUTE, SECOND } from '../../../shared/constants/time';
|
||||||
|
import { isEqualCaseInsensitive } from '../../../ui/helpers/utils/util';
|
||||||
import { NETWORK_EVENTS } from './network';
|
import { NETWORK_EVENTS } from './network';
|
||||||
|
|
||||||
// The MAX_GAS_LIMIT is a number that is higher than the maximum gas costs we have observed on any aggregator
|
// The MAX_GAS_LIMIT is a number that is higher than the maximum gas costs we have observed on any aggregator
|
||||||
@ -91,7 +92,7 @@ export default class SwapsController {
|
|||||||
networkController,
|
networkController,
|
||||||
provider,
|
provider,
|
||||||
getProviderConfig,
|
getProviderConfig,
|
||||||
tokenRatesStore,
|
getTokenRatesState,
|
||||||
fetchTradesInfo = defaultFetchTradesInfo,
|
fetchTradesInfo = defaultFetchTradesInfo,
|
||||||
getCurrentChainId,
|
getCurrentChainId,
|
||||||
getEIP1559GasFeeEstimates,
|
getEIP1559GasFeeEstimates,
|
||||||
@ -105,7 +106,7 @@ export default class SwapsController {
|
|||||||
this._getEIP1559GasFeeEstimates = getEIP1559GasFeeEstimates;
|
this._getEIP1559GasFeeEstimates = getEIP1559GasFeeEstimates;
|
||||||
|
|
||||||
this.getBufferedGasLimit = getBufferedGasLimit;
|
this.getBufferedGasLimit = getBufferedGasLimit;
|
||||||
this.tokenRatesStore = tokenRatesStore;
|
this.getTokenRatesState = getTokenRatesState;
|
||||||
|
|
||||||
this.pollCount = 0;
|
this.pollCount = 0;
|
||||||
this.getProviderConfig = getProviderConfig;
|
this.getProviderConfig = getProviderConfig;
|
||||||
@ -280,7 +281,7 @@ export default class SwapsController {
|
|||||||
|
|
||||||
// For a user to be able to swap a token, they need to have approved the MetaSwap contract to withdraw that token.
|
// For a user to be able to swap a token, they need to have approved the MetaSwap contract to withdraw that token.
|
||||||
// _getERC20Allowance() returns the amount of the token they have approved for withdrawal. If that amount is greater
|
// _getERC20Allowance() returns the amount of the token they have approved for withdrawal. If that amount is greater
|
||||||
// than 0, it means that approval has already occured and is not needed. Otherwise, for tokens to be swapped, a new
|
// than 0, it means that approval has already occurred and is not needed. Otherwise, for tokens to be swapped, a new
|
||||||
// call of the ERC-20 approve method is required.
|
// call of the ERC-20 approve method is required.
|
||||||
approvalRequired =
|
approvalRequired =
|
||||||
allowance.eq(0) &&
|
allowance.eq(0) &&
|
||||||
@ -610,7 +611,9 @@ export default class SwapsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async _findTopQuoteAndCalculateSavings(quotes = {}) {
|
async _findTopQuoteAndCalculateSavings(quotes = {}) {
|
||||||
const tokenConversionRates = this.tokenRatesStore.contractExchangeRates;
|
const {
|
||||||
|
contractExchangeRates: tokenConversionRates,
|
||||||
|
} = this.getTokenRatesState();
|
||||||
const {
|
const {
|
||||||
swapsState: { customGasPrice, customMaxPriorityFeePerGas },
|
swapsState: { customGasPrice, customMaxPriorityFeePerGas },
|
||||||
} = this.store.getState();
|
} = this.store.getState();
|
||||||
@ -734,7 +737,12 @@ export default class SwapsController {
|
|||||||
decimalAdjustedDestinationAmount,
|
decimalAdjustedDestinationAmount,
|
||||||
);
|
);
|
||||||
|
|
||||||
const tokenConversionRate = tokenConversionRates[destinationToken];
|
const tokenConversionRate =
|
||||||
|
tokenConversionRates[
|
||||||
|
Object.keys(tokenConversionRates).find((tokenAddress) =>
|
||||||
|
isEqualCaseInsensitive(tokenAddress, destinationToken),
|
||||||
|
)
|
||||||
|
];
|
||||||
const conversionRateForSorting = tokenConversionRate || 1;
|
const conversionRateForSorting = tokenConversionRate || 1;
|
||||||
|
|
||||||
const ethValueOfTokens = decimalAdjustedDestinationAmount.times(
|
const ethValueOfTokens = decimalAdjustedDestinationAmount.times(
|
||||||
@ -777,7 +785,17 @@ export default class SwapsController {
|
|||||||
isSwapsDefaultTokenAddress(
|
isSwapsDefaultTokenAddress(
|
||||||
newQuotes[topAggId].destinationToken,
|
newQuotes[topAggId].destinationToken,
|
||||||
chainId,
|
chainId,
|
||||||
) || Boolean(tokenConversionRates[newQuotes[topAggId]?.destinationToken]);
|
) ||
|
||||||
|
Boolean(
|
||||||
|
tokenConversionRates[
|
||||||
|
Object.keys(tokenConversionRates).find((tokenAddress) =>
|
||||||
|
isEqualCaseInsensitive(
|
||||||
|
tokenAddress,
|
||||||
|
newQuotes[topAggId]?.destinationToken,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
let savings = null;
|
let savings = null;
|
||||||
|
|
||||||
|
@ -82,12 +82,12 @@ const MOCK_FETCH_METADATA = {
|
|||||||
chainId: MAINNET_CHAIN_ID,
|
chainId: MAINNET_CHAIN_ID,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MOCK_TOKEN_RATES_STORE = {
|
const MOCK_TOKEN_RATES_STORE = () => ({
|
||||||
contractExchangeRates: {
|
contractExchangeRates: {
|
||||||
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': 2,
|
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': 2,
|
||||||
'0x1111111111111111111111111111111111111111': 0.1,
|
'0x1111111111111111111111111111111111111111': 0.1,
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
const MOCK_GET_PROVIDER_CONFIG = () => ({ type: 'FAKE_NETWORK' });
|
const MOCK_GET_PROVIDER_CONFIG = () => ({ type: 'FAKE_NETWORK' });
|
||||||
|
|
||||||
@ -161,7 +161,7 @@ describe('SwapsController', function () {
|
|||||||
networkController: getMockNetworkController(),
|
networkController: getMockNetworkController(),
|
||||||
provider,
|
provider,
|
||||||
getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
|
getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
|
||||||
tokenRatesStore: MOCK_TOKEN_RATES_STORE,
|
getTokenRatesState: MOCK_TOKEN_RATES_STORE,
|
||||||
fetchTradesInfo: fetchTradesInfoStub,
|
fetchTradesInfo: fetchTradesInfoStub,
|
||||||
getCurrentChainId: getCurrentChainIdStub,
|
getCurrentChainId: getCurrentChainIdStub,
|
||||||
getEIP1559GasFeeEstimates: getEIP1559GasFeeEstimatesStub,
|
getEIP1559GasFeeEstimates: getEIP1559GasFeeEstimatesStub,
|
||||||
@ -211,7 +211,7 @@ describe('SwapsController', function () {
|
|||||||
networkController,
|
networkController,
|
||||||
provider,
|
provider,
|
||||||
getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
|
getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
|
||||||
tokenRatesStore: MOCK_TOKEN_RATES_STORE,
|
getTokenRatesState: MOCK_TOKEN_RATES_STORE,
|
||||||
fetchTradesInfo: fetchTradesInfoStub,
|
fetchTradesInfo: fetchTradesInfoStub,
|
||||||
getCurrentChainId: getCurrentChainIdStub,
|
getCurrentChainId: getCurrentChainIdStub,
|
||||||
});
|
});
|
||||||
@ -235,7 +235,7 @@ describe('SwapsController', function () {
|
|||||||
networkController,
|
networkController,
|
||||||
provider,
|
provider,
|
||||||
getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
|
getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
|
||||||
tokenRatesStore: MOCK_TOKEN_RATES_STORE,
|
getTokenRatesState: MOCK_TOKEN_RATES_STORE,
|
||||||
fetchTradesInfo: fetchTradesInfoStub,
|
fetchTradesInfo: fetchTradesInfoStub,
|
||||||
getCurrentChainId: getCurrentChainIdStub,
|
getCurrentChainId: getCurrentChainIdStub,
|
||||||
});
|
});
|
||||||
@ -259,7 +259,7 @@ describe('SwapsController', function () {
|
|||||||
networkController,
|
networkController,
|
||||||
provider,
|
provider,
|
||||||
getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
|
getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
|
||||||
tokenRatesStore: MOCK_TOKEN_RATES_STORE,
|
getTokenRatesState: MOCK_TOKEN_RATES_STORE,
|
||||||
fetchTradesInfo: fetchTradesInfoStub,
|
fetchTradesInfo: fetchTradesInfoStub,
|
||||||
getCurrentChainId: getCurrentChainIdStub,
|
getCurrentChainId: getCurrentChainIdStub,
|
||||||
});
|
});
|
||||||
@ -816,9 +816,10 @@ describe('SwapsController', function () {
|
|||||||
.stub(swapsController, '_getERC20Allowance')
|
.stub(swapsController, '_getERC20Allowance')
|
||||||
.resolves(ethers.BigNumber.from(1));
|
.resolves(ethers.BigNumber.from(1));
|
||||||
|
|
||||||
swapsController.tokenRatesStore = {
|
swapsController.getTokenRatesState = () => ({
|
||||||
contractExchangeRates: {},
|
contractExchangeRates: {},
|
||||||
};
|
});
|
||||||
|
|
||||||
const [newQuotes, topAggId] = await swapsController.fetchAndSetQuotes(
|
const [newQuotes, topAggId] = await swapsController.fetchAndSetQuotes(
|
||||||
MOCK_FETCH_PARAMS,
|
MOCK_FETCH_PARAMS,
|
||||||
MOCK_FETCH_METADATA,
|
MOCK_FETCH_METADATA,
|
||||||
|
@ -8,7 +8,7 @@ const Box = process.env.IN_TEST
|
|||||||
|
|
||||||
import log from 'loglevel';
|
import log from 'loglevel';
|
||||||
import { JsonRpcEngine } from 'json-rpc-engine';
|
import { JsonRpcEngine } from 'json-rpc-engine';
|
||||||
import providerFromEngine from 'eth-json-rpc-middleware/providerFromEngine';
|
import { providerFromEngine } from 'eth-json-rpc-middleware';
|
||||||
import Migrator from '../lib/migrator';
|
import Migrator from '../lib/migrator';
|
||||||
import migrations from '../migrations';
|
import migrations from '../migrations';
|
||||||
import createOriginMiddleware from '../lib/createOriginMiddleware';
|
import createOriginMiddleware from '../lib/createOriginMiddleware';
|
||||||
|
@ -26,6 +26,7 @@ import {
|
|||||||
TRANSACTION_TYPES,
|
TRANSACTION_TYPES,
|
||||||
TRANSACTION_ENVELOPE_TYPES,
|
TRANSACTION_ENVELOPE_TYPES,
|
||||||
} from '../../../../shared/constants/transaction';
|
} from '../../../../shared/constants/transaction';
|
||||||
|
import { TRANSACTION_ENVELOPE_TYPE_NAMES } from '../../../../ui/helpers/constants/transactions';
|
||||||
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
|
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
|
||||||
import {
|
import {
|
||||||
GAS_LIMITS,
|
GAS_LIMITS,
|
||||||
@ -1447,8 +1448,8 @@ export default class TransactionController extends EventEmitter {
|
|||||||
sensitiveProperties: {
|
sensitiveProperties: {
|
||||||
status,
|
status,
|
||||||
transaction_envelope_type: isEIP1559Transaction(txMeta)
|
transaction_envelope_type: isEIP1559Transaction(txMeta)
|
||||||
? 'fee-market'
|
? TRANSACTION_ENVELOPE_TYPE_NAMES.FEE_MARKET
|
||||||
: 'legacy',
|
: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
|
||||||
first_seen: time,
|
first_seen: time,
|
||||||
gas_limit: gasLimit,
|
gas_limit: gasLimit,
|
||||||
...gasParamsInGwei,
|
...gasParamsInGwei,
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
GAS_ESTIMATE_TYPES,
|
GAS_ESTIMATE_TYPES,
|
||||||
GAS_RECOMMENDATIONS,
|
GAS_RECOMMENDATIONS,
|
||||||
} from '../../../../shared/constants/gas';
|
} from '../../../../shared/constants/gas';
|
||||||
|
import { TRANSACTION_ENVELOPE_TYPE_NAMES } from '../../../../ui/helpers/constants/transactions';
|
||||||
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
|
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
|
||||||
import TransactionController, { TRANSACTION_EVENTS } from '.';
|
import TransactionController, { TRANSACTION_EVENTS } from '.';
|
||||||
|
|
||||||
@ -774,7 +775,7 @@ describe('Transaction Controller', function () {
|
|||||||
nonce: '0x4b',
|
nonce: '0x4b',
|
||||||
},
|
},
|
||||||
type: TRANSACTION_TYPES.SIMPLE_SEND,
|
type: TRANSACTION_TYPES.SIMPLE_SEND,
|
||||||
transaction_envelope_type: 'legacy',
|
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
|
||||||
origin: 'metamask',
|
origin: 'metamask',
|
||||||
chainId: currentChainId,
|
chainId: currentChainId,
|
||||||
time: 1624408066355,
|
time: 1624408066355,
|
||||||
@ -1578,7 +1579,7 @@ describe('Transaction Controller', function () {
|
|||||||
gas_price: '2',
|
gas_price: '2',
|
||||||
gas_limit: '0x7b0d',
|
gas_limit: '0x7b0d',
|
||||||
first_seen: 1624408066355,
|
first_seen: 1624408066355,
|
||||||
transaction_envelope_type: 'legacy',
|
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
|
||||||
status: 'unapproved',
|
status: 'unapproved',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -1625,7 +1626,7 @@ describe('Transaction Controller', function () {
|
|||||||
gas_price: '2',
|
gas_price: '2',
|
||||||
gas_limit: '0x7b0d',
|
gas_limit: '0x7b0d',
|
||||||
first_seen: 1624408066355,
|
first_seen: 1624408066355,
|
||||||
transaction_envelope_type: 'legacy',
|
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
|
||||||
status: 'unapproved',
|
status: 'unapproved',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -1674,7 +1675,7 @@ describe('Transaction Controller', function () {
|
|||||||
gas_price: '2',
|
gas_price: '2',
|
||||||
gas_limit: '0x7b0d',
|
gas_limit: '0x7b0d',
|
||||||
first_seen: 1624408066355,
|
first_seen: 1624408066355,
|
||||||
transaction_envelope_type: 'legacy',
|
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
|
||||||
status: 'unapproved',
|
status: 'unapproved',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -1731,7 +1732,7 @@ describe('Transaction Controller', function () {
|
|||||||
max_priority_fee_per_gas: '2',
|
max_priority_fee_per_gas: '2',
|
||||||
gas_limit: '0x7b0d',
|
gas_limit: '0x7b0d',
|
||||||
first_seen: 1624408066355,
|
first_seen: 1624408066355,
|
||||||
transaction_envelope_type: 'fee-market',
|
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.FEE_MARKET,
|
||||||
status: 'unapproved',
|
status: 'unapproved',
|
||||||
estimate_suggested: GAS_RECOMMENDATIONS.MEDIUM,
|
estimate_suggested: GAS_RECOMMENDATIONS.MEDIUM,
|
||||||
estimate_used: GAS_RECOMMENDATIONS.HIGH,
|
estimate_used: GAS_RECOMMENDATIONS.HIGH,
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import log from 'loglevel';
|
import log from 'loglevel';
|
||||||
|
|
||||||
import { METASWAP_CHAINID_API_HOST_MAP } from '../../../shared/constants/swaps';
|
import { SWAPS_API_V2_BASE_URL } from '../../../shared/constants/swaps';
|
||||||
import {
|
import {
|
||||||
GOERLI_CHAIN_ID,
|
GOERLI_CHAIN_ID,
|
||||||
KOVAN_CHAIN_ID,
|
KOVAN_CHAIN_ID,
|
||||||
MAINNET_CHAIN_ID,
|
MAINNET_CHAIN_ID,
|
||||||
RINKEBY_CHAIN_ID,
|
RINKEBY_CHAIN_ID,
|
||||||
ROPSTEN_CHAIN_ID,
|
ROPSTEN_CHAIN_ID,
|
||||||
|
MAINNET_NETWORK_ID,
|
||||||
} from '../../../shared/constants/network';
|
} from '../../../shared/constants/network';
|
||||||
import { SECOND } from '../../../shared/constants/time';
|
import { SECOND } from '../../../shared/constants/time';
|
||||||
import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout';
|
import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout';
|
||||||
@ -20,7 +21,7 @@ const fetchWithTimeout = getFetchWithTimeout(SECOND * 30);
|
|||||||
* @returns String
|
* @returns String
|
||||||
*/
|
*/
|
||||||
const createWyrePurchaseUrl = async (address) => {
|
const createWyrePurchaseUrl = async (address) => {
|
||||||
const fiatOnRampUrlApi = `${METASWAP_CHAINID_API_HOST_MAP[MAINNET_CHAIN_ID]}/fiatOnRampUrl?serviceName=wyre&destinationAddress=${address}`;
|
const fiatOnRampUrlApi = `${SWAPS_API_V2_BASE_URL}/networks/${MAINNET_NETWORK_ID}/fiatOnRampUrl?serviceName=wyre&destinationAddress=${address}`;
|
||||||
const wyrePurchaseUrlFallback = `https://pay.sendwyre.com/purchase?dest=ethereum:${address}&destCurrency=ETH&accountId=AC-7AG3W4XH4N2&paymentMethod=debit-card`;
|
const wyrePurchaseUrlFallback = `https://pay.sendwyre.com/purchase?dest=ethereum:${address}&destCurrency=ETH&accountId=AC-7AG3W4XH4N2&paymentMethod=debit-card`;
|
||||||
try {
|
try {
|
||||||
const response = await fetchWithTimeout(fiatOnRampUrlApi, {
|
const response = await fetchWithTimeout(fiatOnRampUrlApi, {
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
ROPSTEN_CHAIN_ID,
|
ROPSTEN_CHAIN_ID,
|
||||||
} from '../../../shared/constants/network';
|
} from '../../../shared/constants/network';
|
||||||
import { TRANSAK_API_KEY } from '../constants/on-ramp';
|
import { TRANSAK_API_KEY } from '../constants/on-ramp';
|
||||||
|
import { SWAPS_API_V2_BASE_URL } from '../../../shared/constants/swaps';
|
||||||
import getBuyEthUrl from './buy-eth-url';
|
import getBuyEthUrl from './buy-eth-url';
|
||||||
|
|
||||||
const WYRE_ACCOUNT_ID = 'AC-7AG3W4XH4N2';
|
const WYRE_ACCOUNT_ID = 'AC-7AG3W4XH4N2';
|
||||||
@ -28,8 +29,10 @@ const KOVAN = {
|
|||||||
|
|
||||||
describe('buy-eth-url', function () {
|
describe('buy-eth-url', function () {
|
||||||
it('returns Wyre url with an ETH address for Ethereum mainnet', async function () {
|
it('returns Wyre url with an ETH address for Ethereum mainnet', async function () {
|
||||||
nock('https://api.metaswap.codefi.network')
|
nock(SWAPS_API_V2_BASE_URL)
|
||||||
.get(`/fiatOnRampUrl?serviceName=wyre&destinationAddress=${ETH_ADDRESS}`)
|
.get(
|
||||||
|
`/networks/1/fiatOnRampUrl?serviceName=wyre&destinationAddress=${ETH_ADDRESS}`,
|
||||||
|
)
|
||||||
.reply(200, {
|
.reply(200, {
|
||||||
url: `https://pay.sendwyre.com/purchase?accountId=${WYRE_ACCOUNT_ID}&utm_campaign=${WYRE_ACCOUNT_ID}&destCurrency=ETH&utm_medium=widget&paymentMethod=debit-card&reservation=MLZVUF8FMXZUMARJC23B&dest=ethereum%3A${ETH_ADDRESS}&utm_source=checkout`,
|
url: `https://pay.sendwyre.com/purchase?accountId=${WYRE_ACCOUNT_ID}&utm_campaign=${WYRE_ACCOUNT_ID}&destCurrency=ETH&utm_medium=widget&paymentMethod=debit-card&reservation=MLZVUF8FMXZUMARJC23B&dest=ethereum%3A${ETH_ADDRESS}&utm_source=checkout`,
|
||||||
});
|
});
|
||||||
|
@ -38,13 +38,14 @@ export default class DecryptMessageManager extends EventEmitter {
|
|||||||
* @property {Array} messages Holds all messages that have been created by this DecryptMessageManager
|
* @property {Array} messages Holds all messages that have been created by this DecryptMessageManager
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor(opts) {
|
||||||
super();
|
super();
|
||||||
this.memStore = new ObservableStore({
|
this.memStore = new ObservableStore({
|
||||||
unapprovedDecryptMsgs: {},
|
unapprovedDecryptMsgs: {},
|
||||||
unapprovedDecryptMsgCount: 0,
|
unapprovedDecryptMsgCount: 0,
|
||||||
});
|
});
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
|
this.metricsEvent = opts.metricsEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -237,7 +238,16 @@ export default class DecryptMessageManager extends EventEmitter {
|
|||||||
* @param {number} msgId The id of the DecryptMessage to reject.
|
* @param {number} msgId The id of the DecryptMessage to reject.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
rejectMsg(msgId) {
|
rejectMsg(msgId, reason = undefined) {
|
||||||
|
if (reason) {
|
||||||
|
this.metricsEvent({
|
||||||
|
event: reason,
|
||||||
|
category: 'Messages',
|
||||||
|
properties: {
|
||||||
|
action: 'Decrypt Message Request',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
this._setMsgStatus(msgId, 'rejected');
|
this._setMsgStatus(msgId, 'rejected');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,13 +34,14 @@ export default class EncryptionPublicKeyManager extends EventEmitter {
|
|||||||
* @property {Array} messages Holds all messages that have been created by this EncryptionPublicKeyManager
|
* @property {Array} messages Holds all messages that have been created by this EncryptionPublicKeyManager
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor(opts) {
|
||||||
super();
|
super();
|
||||||
this.memStore = new ObservableStore({
|
this.memStore = new ObservableStore({
|
||||||
unapprovedEncryptionPublicKeyMsgs: {},
|
unapprovedEncryptionPublicKeyMsgs: {},
|
||||||
unapprovedEncryptionPublicKeyMsgCount: 0,
|
unapprovedEncryptionPublicKeyMsgCount: 0,
|
||||||
});
|
});
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
|
this.metricsEvent = opts.metricsEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -226,7 +227,16 @@ export default class EncryptionPublicKeyManager extends EventEmitter {
|
|||||||
* @param {number} msgId The id of the EncryptionPublicKey to reject.
|
* @param {number} msgId The id of the EncryptionPublicKey to reject.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
rejectMsg(msgId) {
|
rejectMsg(msgId, reason = undefined) {
|
||||||
|
if (reason) {
|
||||||
|
this.metricsEvent({
|
||||||
|
event: reason,
|
||||||
|
category: 'Messages',
|
||||||
|
properties: {
|
||||||
|
action: 'Encryption public key Request',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
this._setMsgStatus(msgId, 'rejected');
|
this._setMsgStatus(msgId, 'rejected');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,13 +35,14 @@ export default class MessageManager extends EventEmitter {
|
|||||||
* @property {Array} messages Holds all messages that have been created by this MessageManager
|
* @property {Array} messages Holds all messages that have been created by this MessageManager
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor({ metricsEvent }) {
|
||||||
super();
|
super();
|
||||||
this.memStore = new ObservableStore({
|
this.memStore = new ObservableStore({
|
||||||
unapprovedMsgs: {},
|
unapprovedMsgs: {},
|
||||||
unapprovedMsgCount: 0,
|
unapprovedMsgCount: 0,
|
||||||
});
|
});
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
|
this.metricsEvent = metricsEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -78,9 +79,9 @@ export default class MessageManager extends EventEmitter {
|
|||||||
* @returns {promise} after signature has been
|
* @returns {promise} after signature has been
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
addUnapprovedMessageAsync(msgParams, req) {
|
async addUnapprovedMessageAsync(msgParams, req) {
|
||||||
return new Promise((resolve, reject) => {
|
const msgId = this.addUnapprovedMessage(msgParams, req);
|
||||||
const msgId = this.addUnapprovedMessage(msgParams, req);
|
return await new Promise((resolve, reject) => {
|
||||||
// await finished
|
// await finished
|
||||||
this.once(`${msgId}:finished`, (data) => {
|
this.once(`${msgId}:finished`, (data) => {
|
||||||
switch (data.status) {
|
switch (data.status) {
|
||||||
@ -92,6 +93,10 @@ export default class MessageManager extends EventEmitter {
|
|||||||
'MetaMask Message Signature: User denied message signature.',
|
'MetaMask Message Signature: User denied message signature.',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
case 'errored':
|
||||||
|
return reject(
|
||||||
|
new Error(`MetaMask Message Signature: ${data.error}`),
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return reject(
|
return reject(
|
||||||
new Error(
|
new Error(
|
||||||
@ -217,10 +222,34 @@ export default class MessageManager extends EventEmitter {
|
|||||||
* @param {number} msgId - The id of the Message to reject.
|
* @param {number} msgId - The id of the Message to reject.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
rejectMsg(msgId) {
|
rejectMsg(msgId, reason = undefined) {
|
||||||
|
if (reason) {
|
||||||
|
const msg = this.getMsg(msgId);
|
||||||
|
this.metricsEvent({
|
||||||
|
event: reason,
|
||||||
|
category: 'Transactions',
|
||||||
|
properties: {
|
||||||
|
action: 'Sign Request',
|
||||||
|
type: msg.type,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
this._setMsgStatus(msgId, 'rejected');
|
this._setMsgStatus(msgId, 'rejected');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a Message status to 'errored' via a call to this._setMsgStatus.
|
||||||
|
*
|
||||||
|
* @param {number} msgId - The id of the Message to error
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
errorMessage(msgId, error) {
|
||||||
|
const msg = this.getMsg(msgId);
|
||||||
|
msg.error = error;
|
||||||
|
this._updateMsg(msg);
|
||||||
|
this._setMsgStatus(msgId, 'errored');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears all unapproved messages from memory.
|
* Clears all unapproved messages from memory.
|
||||||
*/
|
*/
|
||||||
@ -292,7 +321,7 @@ export default class MessageManager extends EventEmitter {
|
|||||||
* @returns {string} A hex string conversion of the buffer data
|
* @returns {string} A hex string conversion of the buffer data
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
function normalizeMsgData(data) {
|
export function normalizeMsgData(data) {
|
||||||
if (data.slice(0, 2) === '0x') {
|
if (data.slice(0, 2) === '0x') {
|
||||||
// data is already hex
|
// data is already hex
|
||||||
return data;
|
return data;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { strict as assert } from 'assert';
|
import { strict as assert } from 'assert';
|
||||||
|
import sinon from 'sinon';
|
||||||
import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction';
|
import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction';
|
||||||
import MessageManager from './message-manager';
|
import MessageManager from './message-manager';
|
||||||
|
|
||||||
@ -6,7 +7,9 @@ describe('Message Manager', function () {
|
|||||||
let messageManager;
|
let messageManager;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
messageManager = new MessageManager();
|
messageManager = new MessageManager({
|
||||||
|
metricsEvent: sinon.fake(),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#getMsgList', function () {
|
describe('#getMsgList', function () {
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
|
import EventEmitter from 'safe-event-emitter';
|
||||||
import ExtensionPlatform from '../platforms/extension';
|
import ExtensionPlatform from '../platforms/extension';
|
||||||
|
|
||||||
const NOTIFICATION_HEIGHT = 620;
|
const NOTIFICATION_HEIGHT = 620;
|
||||||
const NOTIFICATION_WIDTH = 360;
|
const NOTIFICATION_WIDTH = 360;
|
||||||
|
|
||||||
export default class NotificationManager {
|
export const NOTIFICATION_MANAGER_EVENTS = {
|
||||||
|
POPUP_CLOSED: 'onPopupClosed',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class NotificationManager extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
* A collection of methods for controlling the showing and hiding of the notification popup.
|
* A collection of methods for controlling the showing and hiding of the notification popup.
|
||||||
*
|
*
|
||||||
@ -12,7 +17,9 @@ export default class NotificationManager {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
super();
|
||||||
this.platform = new ExtensionPlatform();
|
this.platform = new ExtensionPlatform();
|
||||||
|
this.platform.addOnRemovedListener(this._onWindowClosed.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -62,6 +69,13 @@ export default class NotificationManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onWindowClosed(windowId) {
|
||||||
|
if (windowId === this._popupId) {
|
||||||
|
this._popupId = undefined;
|
||||||
|
this.emit(NOTIFICATION_MANAGER_EVENTS.POPUP_CLOSED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks all open MetaMask windows, and returns the first one it finds that is a notification window (i.e. has the
|
* Checks all open MetaMask windows, and returns the first one it finds that is a notification window (i.e. has the
|
||||||
* type 'popup')
|
* type 'popup')
|
||||||
|
@ -40,13 +40,14 @@ export default class PersonalMessageManager extends EventEmitter {
|
|||||||
* @property {Array} messages Holds all messages that have been created by this PersonalMessageManager
|
* @property {Array} messages Holds all messages that have been created by this PersonalMessageManager
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor({ metricsEvent }) {
|
||||||
super();
|
super();
|
||||||
this.memStore = new ObservableStore({
|
this.memStore = new ObservableStore({
|
||||||
unapprovedPersonalMsgs: {},
|
unapprovedPersonalMsgs: {},
|
||||||
unapprovedPersonalMsgCount: 0,
|
unapprovedPersonalMsgCount: 0,
|
||||||
});
|
});
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
|
this.metricsEvent = metricsEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -106,6 +107,9 @@ export default class PersonalMessageManager extends EventEmitter {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
case 'errored':
|
||||||
|
reject(new Error(`MetaMask Message Signature: ${data.error}`));
|
||||||
|
return;
|
||||||
default:
|
default:
|
||||||
reject(
|
reject(
|
||||||
new Error(
|
new Error(
|
||||||
@ -238,10 +242,34 @@ export default class PersonalMessageManager extends EventEmitter {
|
|||||||
* @param {number} msgId - The id of the PersonalMessage to reject.
|
* @param {number} msgId - The id of the PersonalMessage to reject.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
rejectMsg(msgId) {
|
rejectMsg(msgId, reason = undefined) {
|
||||||
|
if (reason) {
|
||||||
|
const msg = this.getMsg(msgId);
|
||||||
|
this.metricsEvent({
|
||||||
|
event: reason,
|
||||||
|
category: 'Transactions',
|
||||||
|
properties: {
|
||||||
|
action: 'Sign Request',
|
||||||
|
type: msg.type,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
this._setMsgStatus(msgId, 'rejected');
|
this._setMsgStatus(msgId, 'rejected');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a Message status to 'errored' via a call to this._setMsgStatus.
|
||||||
|
*
|
||||||
|
* @param {number} msgId - The id of the Message to error
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
errorMessage(msgId, error) {
|
||||||
|
const msg = this.getMsg(msgId);
|
||||||
|
msg.error = error;
|
||||||
|
this._updateMsg(msg);
|
||||||
|
this._setMsgStatus(msgId, 'errored');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears all unapproved messages from memory.
|
* Clears all unapproved messages from memory.
|
||||||
*/
|
*/
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { strict as assert } from 'assert';
|
import { strict as assert } from 'assert';
|
||||||
|
import sinon from 'sinon';
|
||||||
import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction';
|
import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction';
|
||||||
import PersonalMessageManager from './personal-message-manager';
|
import PersonalMessageManager from './personal-message-manager';
|
||||||
|
|
||||||
@ -6,7 +7,7 @@ describe('Personal Message Manager', function () {
|
|||||||
let messageManager;
|
let messageManager;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
messageManager = new PersonalMessageManager();
|
messageManager = new PersonalMessageManager({ metricsEvent: sinon.fake() });
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#getMsgList', function () {
|
describe('#getMsgList', function () {
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
|
import { ethErrors } from 'eth-rpc-errors';
|
||||||
|
import { UNSUPPORTED_RPC_METHODS } from '../../../../shared/constants/network';
|
||||||
import handlers from './handlers';
|
import handlers from './handlers';
|
||||||
|
|
||||||
const handlerMap = handlers.reduce((map, handler) => {
|
const handlerMap = handlers.reduce((map, handler) => {
|
||||||
for (const methodName of handler.methodNames) {
|
for (const methodName of handler.methodNames) {
|
||||||
map.set(methodName, handler.implementation);
|
map.set(methodName, handler);
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}, new Map());
|
}, new Map());
|
||||||
@ -21,14 +23,41 @@ const handlerMap = handlers.reduce((map, handler) => {
|
|||||||
* Eventually, we'll want to extract this middleware into its own package.
|
* Eventually, we'll want to extract this middleware into its own package.
|
||||||
*
|
*
|
||||||
* @param {Object} opts - The middleware options
|
* @param {Object} opts - The middleware options
|
||||||
* @param {Function} opts.sendMetrics - A function for sending a metrics event
|
|
||||||
* @returns {(req: Object, res: Object, next: Function, end: Function) => void}
|
* @returns {(req: Object, res: Object, next: Function, end: Function) => void}
|
||||||
*/
|
*/
|
||||||
export default function createMethodMiddleware(opts) {
|
export default function createMethodMiddleware(opts) {
|
||||||
return function methodMiddleware(req, res, next, end) {
|
return function methodMiddleware(req, res, next, end) {
|
||||||
if (handlerMap.has(req.method)) {
|
// Reject unsupported methods.
|
||||||
return handlerMap.get(req.method)(req, res, next, end, opts);
|
if (UNSUPPORTED_RPC_METHODS.has(req.method)) {
|
||||||
|
return end(ethErrors.rpc.methodNotSupported());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handler = handlerMap.get(req.method);
|
||||||
|
if (handler) {
|
||||||
|
const { implementation, hookNames } = handler;
|
||||||
|
return implementation(req, res, next, end, selectHooks(opts, hookNames));
|
||||||
|
}
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the subset of the specified `hooks` that are included in the
|
||||||
|
* `hookNames` object. This is a Principle of Least Authority (POLA) measure
|
||||||
|
* to ensure that each RPC method implementation only has access to the
|
||||||
|
* API "hooks" it needs to do its job.
|
||||||
|
*
|
||||||
|
* @param {Record<string, unknown>} hooks - The hooks to select from.
|
||||||
|
* @param {Record<string, true>} hookNames - The names of the hooks to select.
|
||||||
|
* @returns {Record<string, unknown> | undefined} The selected hooks.
|
||||||
|
*/
|
||||||
|
function selectHooks(hooks, hookNames) {
|
||||||
|
if (hookNames) {
|
||||||
|
return Object.keys(hookNames).reduce((hookSubset, hookName) => {
|
||||||
|
hookSubset[hookName] = hooks[hookName];
|
||||||
|
return hookSubset;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
@ -12,6 +12,14 @@ import { CHAIN_ID_TO_NETWORK_ID_MAP } from '../../../../../shared/constants/netw
|
|||||||
const addEthereumChain = {
|
const addEthereumChain = {
|
||||||
methodNames: [MESSAGE_TYPE.ADD_ETHEREUM_CHAIN],
|
methodNames: [MESSAGE_TYPE.ADD_ETHEREUM_CHAIN],
|
||||||
implementation: addEthereumChainHandler,
|
implementation: addEthereumChainHandler,
|
||||||
|
hookNames: {
|
||||||
|
addCustomRpc: true,
|
||||||
|
getCurrentChainId: true,
|
||||||
|
findCustomRpcBy: true,
|
||||||
|
updateRpcTarget: true,
|
||||||
|
requestUserApproval: true,
|
||||||
|
sendMetrics: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
export default addEthereumChain;
|
export default addEthereumChain;
|
||||||
|
|
||||||
|
@ -9,6 +9,9 @@ import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
|
|||||||
const getProviderState = {
|
const getProviderState = {
|
||||||
methodNames: [MESSAGE_TYPE.GET_PROVIDER_STATE],
|
methodNames: [MESSAGE_TYPE.GET_PROVIDER_STATE],
|
||||||
implementation: getProviderStateHandler,
|
implementation: getProviderStateHandler,
|
||||||
|
hookNames: {
|
||||||
|
getProviderState: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
export default getProviderState;
|
export default getProviderState;
|
||||||
|
|
||||||
|
@ -10,6 +10,11 @@ import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
|
|||||||
const logWeb3ShimUsage = {
|
const logWeb3ShimUsage = {
|
||||||
methodNames: [MESSAGE_TYPE.LOG_WEB3_SHIM_USAGE],
|
methodNames: [MESSAGE_TYPE.LOG_WEB3_SHIM_USAGE],
|
||||||
implementation: logWeb3ShimUsageHandler,
|
implementation: logWeb3ShimUsageHandler,
|
||||||
|
hookNames: {
|
||||||
|
sendMetrics: true,
|
||||||
|
getWeb3ShimUsageState: true,
|
||||||
|
setWeb3ShimUsageRecorded: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
export default logWeb3ShimUsage;
|
export default logWeb3ShimUsage;
|
||||||
|
|
||||||
|
@ -15,6 +15,13 @@ import {
|
|||||||
const switchEthereumChain = {
|
const switchEthereumChain = {
|
||||||
methodNames: [MESSAGE_TYPE.SWITCH_ETHEREUM_CHAIN],
|
methodNames: [MESSAGE_TYPE.SWITCH_ETHEREUM_CHAIN],
|
||||||
implementation: switchEthereumChainHandler,
|
implementation: switchEthereumChainHandler,
|
||||||
|
hookNames: {
|
||||||
|
getCurrentChainId: true,
|
||||||
|
findCustomRpcBy: true,
|
||||||
|
setProviderType: true,
|
||||||
|
updateRpcTarget: true,
|
||||||
|
requestUserApproval: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
export default switchEthereumChain;
|
export default switchEthereumChain;
|
||||||
|
|
||||||
|
@ -3,6 +3,9 @@ import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
|
|||||||
const watchAsset = {
|
const watchAsset = {
|
||||||
methodNames: [MESSAGE_TYPE.WATCH_ASSET, MESSAGE_TYPE.WATCH_ASSET_LEGACY],
|
methodNames: [MESSAGE_TYPE.WATCH_ASSET, MESSAGE_TYPE.WATCH_ASSET_LEGACY],
|
||||||
implementation: watchAssetHandler,
|
implementation: watchAssetHandler,
|
||||||
|
hookNames: {
|
||||||
|
handleWatchAssetRequest: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
export default watchAsset;
|
export default watchAsset;
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ export default class TypedMessageManager extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* Controller in charge of managing - storing, adding, removing, updating - TypedMessage.
|
* Controller in charge of managing - storing, adding, removing, updating - TypedMessage.
|
||||||
*/
|
*/
|
||||||
constructor({ getCurrentChainId }) {
|
constructor({ getCurrentChainId, metricEvents }) {
|
||||||
super();
|
super();
|
||||||
this._getCurrentChainId = getCurrentChainId;
|
this._getCurrentChainId = getCurrentChainId;
|
||||||
this.memStore = new ObservableStore({
|
this.memStore = new ObservableStore({
|
||||||
@ -40,6 +40,7 @@ export default class TypedMessageManager extends EventEmitter {
|
|||||||
unapprovedTypedMessagesCount: 0,
|
unapprovedTypedMessagesCount: 0,
|
||||||
});
|
});
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
|
this.metricEvents = metricEvents;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -301,7 +302,19 @@ export default class TypedMessageManager extends EventEmitter {
|
|||||||
* @param {number} msgId - The id of the TypedMessage to reject.
|
* @param {number} msgId - The id of the TypedMessage to reject.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
rejectMsg(msgId) {
|
rejectMsg(msgId, reason = undefined) {
|
||||||
|
if (reason) {
|
||||||
|
const msg = this.getMsg(msgId);
|
||||||
|
this.metricsEvent({
|
||||||
|
event: reason,
|
||||||
|
category: 'Transactions',
|
||||||
|
properties: {
|
||||||
|
action: 'Sign Request',
|
||||||
|
version: msg.msgParams.version,
|
||||||
|
type: msg.type,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
this._setMsgStatus(msgId, 'rejected');
|
this._setMsgStatus(msgId, 'rejected');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ describe('Typed Message Manager', function () {
|
|||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
typedMessageManager = new TypedMessageManager({
|
typedMessageManager = new TypedMessageManager({
|
||||||
getCurrentChainId: sinon.fake.returns('0x1'),
|
getCurrentChainId: sinon.fake.returns('0x1'),
|
||||||
|
metricsEvent: sinon.fake(),
|
||||||
});
|
});
|
||||||
|
|
||||||
msgParamsV1 = {
|
msgParamsV1 = {
|
||||||
|
@ -7,7 +7,7 @@ import { debounce } from 'lodash';
|
|||||||
import createEngineStream from 'json-rpc-middleware-stream/engineStream';
|
import createEngineStream from 'json-rpc-middleware-stream/engineStream';
|
||||||
import createFilterMiddleware from 'eth-json-rpc-filters';
|
import createFilterMiddleware from 'eth-json-rpc-filters';
|
||||||
import createSubscriptionManager from 'eth-json-rpc-filters/subscriptionManager';
|
import createSubscriptionManager from 'eth-json-rpc-filters/subscriptionManager';
|
||||||
import providerAsMiddleware from 'eth-json-rpc-middleware/providerAsMiddleware';
|
import { providerAsMiddleware } from 'eth-json-rpc-middleware';
|
||||||
import KeyringController from 'eth-keyring-controller';
|
import KeyringController from 'eth-keyring-controller';
|
||||||
import { Mutex } from 'await-semaphore';
|
import { Mutex } from 'await-semaphore';
|
||||||
import { stripHexPrefix } from 'ethereumjs-util';
|
import { stripHexPrefix } from 'ethereumjs-util';
|
||||||
@ -17,6 +17,7 @@ import LedgerBridgeKeyring from '@metamask/eth-ledger-bridge-keyring';
|
|||||||
import LatticeKeyring from 'eth-lattice-keyring';
|
import LatticeKeyring from 'eth-lattice-keyring';
|
||||||
import EthQuery from 'eth-query';
|
import EthQuery from 'eth-query';
|
||||||
import nanoid from 'nanoid';
|
import nanoid from 'nanoid';
|
||||||
|
import { ethErrors } from 'eth-rpc-errors';
|
||||||
import { captureException } from '@sentry/browser';
|
import { captureException } from '@sentry/browser';
|
||||||
import {
|
import {
|
||||||
AddressBookController,
|
AddressBookController,
|
||||||
@ -29,6 +30,9 @@ import {
|
|||||||
TokenListController,
|
TokenListController,
|
||||||
TokensController,
|
TokensController,
|
||||||
TokenRatesController,
|
TokenRatesController,
|
||||||
|
CollectiblesController,
|
||||||
|
AssetsContractController,
|
||||||
|
CollectibleDetectionController,
|
||||||
} from '@metamask/controllers';
|
} from '@metamask/controllers';
|
||||||
import { TRANSACTION_STATUSES } from '../../shared/constants/transaction';
|
import { TRANSACTION_STATUSES } from '../../shared/constants/transaction';
|
||||||
import {
|
import {
|
||||||
@ -61,7 +65,7 @@ import AlertController from './controllers/alert';
|
|||||||
import OnboardingController from './controllers/onboarding';
|
import OnboardingController from './controllers/onboarding';
|
||||||
import ThreeBoxController from './controllers/threebox';
|
import ThreeBoxController from './controllers/threebox';
|
||||||
import IncomingTransactionsController from './controllers/incoming-transactions';
|
import IncomingTransactionsController from './controllers/incoming-transactions';
|
||||||
import MessageManager from './lib/message-manager';
|
import MessageManager, { normalizeMsgData } from './lib/message-manager';
|
||||||
import DecryptMessageManager from './lib/decrypt-message-manager';
|
import DecryptMessageManager from './lib/decrypt-message-manager';
|
||||||
import EncryptionPublicKeyManager from './lib/encryption-public-key-manager';
|
import EncryptionPublicKeyManager from './lib/encryption-public-key-manager';
|
||||||
import PersonalMessageManager from './lib/personal-message-manager';
|
import PersonalMessageManager from './lib/personal-message-manager';
|
||||||
@ -175,6 +179,57 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
state: initState.TokensController,
|
state: initState.TokensController,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.assetsContractController = new AssetsContractController();
|
||||||
|
|
||||||
|
this.collectiblesController = new CollectiblesController({
|
||||||
|
onPreferencesStateChange: this.preferencesController.store.subscribe.bind(
|
||||||
|
this.preferencesController.store,
|
||||||
|
),
|
||||||
|
onNetworkStateChange: this.networkController.store.subscribe.bind(
|
||||||
|
this.networkController.store,
|
||||||
|
),
|
||||||
|
getAssetName: this.assetsContractController.getAssetName.bind(
|
||||||
|
this.assetsContractController,
|
||||||
|
),
|
||||||
|
getAssetSymbol: this.assetsContractController.getAssetSymbol.bind(
|
||||||
|
this.assetsContractController,
|
||||||
|
),
|
||||||
|
getCollectibleTokenURI: this.assetsContractController.getCollectibleTokenURI.bind(
|
||||||
|
this.assetsContractController,
|
||||||
|
),
|
||||||
|
getOwnerOf: this.assetsContractController.getOwnerOf.bind(
|
||||||
|
this.assetsContractController,
|
||||||
|
),
|
||||||
|
balanceOfERC1155Collectible: this.assetsContractController.balanceOfERC1155Collectible.bind(
|
||||||
|
this.assetsContractController,
|
||||||
|
),
|
||||||
|
uriERC1155Collectible: this.assetsContractController.uriERC1155Collectible.bind(
|
||||||
|
this.assetsContractController,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
getOpenSeaApiKey: () => this.collectiblesController.openSeaApiKey,
|
||||||
|
getBalancesInSingleCall: this.assetsContractController.getBalancesInSingleCall.bind(
|
||||||
|
this.assetsContractController,
|
||||||
|
),
|
||||||
|
addCollectible: this.collectiblesController.addCollectible.bind(
|
||||||
|
this.collectiblesController,
|
||||||
|
),
|
||||||
|
getCollectiblesState: () => this.collectiblesController.state,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
this.metaMetricsController = new MetaMetricsController({
|
this.metaMetricsController = new MetaMetricsController({
|
||||||
segment,
|
segment,
|
||||||
preferencesStore: this.preferencesController.store,
|
preferencesStore: this.preferencesController.store,
|
||||||
@ -526,14 +581,33 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.networkController.lookupNetwork();
|
this.networkController.lookupNetwork();
|
||||||
this.messageManager = new MessageManager();
|
this.messageManager = new MessageManager({
|
||||||
this.personalMessageManager = new PersonalMessageManager();
|
metricsEvent: this.metaMetricsController.trackEvent.bind(
|
||||||
this.decryptMessageManager = new DecryptMessageManager();
|
this.metaMetricsController,
|
||||||
this.encryptionPublicKeyManager = new EncryptionPublicKeyManager();
|
),
|
||||||
|
});
|
||||||
|
this.personalMessageManager = new PersonalMessageManager({
|
||||||
|
metricsEvent: this.metaMetricsController.trackEvent.bind(
|
||||||
|
this.metaMetricsController,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
this.decryptMessageManager = new DecryptMessageManager({
|
||||||
|
metricsEvent: this.metaMetricsController.trackEvent.bind(
|
||||||
|
this.metaMetricsController,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
this.encryptionPublicKeyManager = new EncryptionPublicKeyManager({
|
||||||
|
metricsEvent: this.metaMetricsController.trackEvent.bind(
|
||||||
|
this.metaMetricsController,
|
||||||
|
),
|
||||||
|
});
|
||||||
this.typedMessageManager = new TypedMessageManager({
|
this.typedMessageManager = new TypedMessageManager({
|
||||||
getCurrentChainId: this.networkController.getCurrentChainId.bind(
|
getCurrentChainId: this.networkController.getCurrentChainId.bind(
|
||||||
this.networkController,
|
this.networkController,
|
||||||
),
|
),
|
||||||
|
metricsEvent: this.metaMetricsController.trackEvent.bind(
|
||||||
|
this.metaMetricsController,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.swapsController = new SwapsController({
|
this.swapsController = new SwapsController({
|
||||||
@ -545,7 +619,7 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
getProviderConfig: this.networkController.getProviderConfig.bind(
|
getProviderConfig: this.networkController.getProviderConfig.bind(
|
||||||
this.networkController,
|
this.networkController,
|
||||||
),
|
),
|
||||||
tokenRatesStore: this.tokenRatesController.state,
|
getTokenRatesState: () => this.tokenRatesController.state,
|
||||||
getCurrentChainId: this.networkController.getCurrentChainId.bind(
|
getCurrentChainId: this.networkController.getCurrentChainId.bind(
|
||||||
this.networkController,
|
this.networkController,
|
||||||
),
|
),
|
||||||
@ -592,6 +666,7 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
GasFeeController: this.gasFeeController,
|
GasFeeController: this.gasFeeController,
|
||||||
TokenListController: this.tokenListController,
|
TokenListController: this.tokenListController,
|
||||||
TokensController: this.tokensController,
|
TokensController: this.tokensController,
|
||||||
|
CollectiblesController: this.collectiblesController,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.memStore = new ComposableObservableStore({
|
this.memStore = new ComposableObservableStore({
|
||||||
@ -626,6 +701,7 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
GasFeeController: this.gasFeeController,
|
GasFeeController: this.gasFeeController,
|
||||||
TokenListController: this.tokenListController,
|
TokenListController: this.tokenListController,
|
||||||
TokensController: this.tokensController,
|
TokensController: this.tokensController,
|
||||||
|
CollectiblesController: this.collectiblesController,
|
||||||
},
|
},
|
||||||
controllerMessenger: this.controllerMessenger,
|
controllerMessenger: this.controllerMessenger,
|
||||||
});
|
});
|
||||||
@ -807,6 +883,7 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
threeBoxController,
|
threeBoxController,
|
||||||
txController,
|
txController,
|
||||||
tokensController,
|
tokensController,
|
||||||
|
collectiblesController,
|
||||||
} = this;
|
} = this;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -924,6 +1001,26 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
this.preferencesController.setDismissSeedBackUpReminder,
|
this.preferencesController.setDismissSeedBackUpReminder,
|
||||||
this.preferencesController,
|
this.preferencesController,
|
||||||
),
|
),
|
||||||
|
setAdvancedGasFee: nodeify(
|
||||||
|
preferencesController.setAdvancedGasFee,
|
||||||
|
preferencesController,
|
||||||
|
),
|
||||||
|
|
||||||
|
// CollectiblesController
|
||||||
|
addCollectible: nodeify(
|
||||||
|
collectiblesController.addCollectible,
|
||||||
|
collectiblesController,
|
||||||
|
),
|
||||||
|
|
||||||
|
removeAndIgnoreCollectible: nodeify(
|
||||||
|
collectiblesController.removeAndIgnoreCollectible,
|
||||||
|
collectiblesController,
|
||||||
|
),
|
||||||
|
|
||||||
|
removeCollectible: nodeify(
|
||||||
|
collectiblesController.removeCollectible,
|
||||||
|
collectiblesController,
|
||||||
|
),
|
||||||
|
|
||||||
// AddressController
|
// AddressController
|
||||||
setAddressBook: nodeify(
|
setAddressBook: nodeify(
|
||||||
@ -985,9 +1082,7 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
),
|
),
|
||||||
createCancelTransaction: nodeify(this.createCancelTransaction, this),
|
createCancelTransaction: nodeify(this.createCancelTransaction, this),
|
||||||
createSpeedUpTransaction: nodeify(this.createSpeedUpTransaction, this),
|
createSpeedUpTransaction: nodeify(this.createSpeedUpTransaction, this),
|
||||||
isNonceTaken: nodeify(txController.isNonceTaken, txController),
|
|
||||||
estimateGas: nodeify(this.estimateGas, this),
|
estimateGas: nodeify(this.estimateGas, this),
|
||||||
getPendingNonce: nodeify(this.getPendingNonce, this),
|
|
||||||
getNextNonce: nodeify(this.getNextNonce, this),
|
getNextNonce: nodeify(this.getNextNonce, this),
|
||||||
addUnapprovedTransaction: nodeify(
|
addUnapprovedTransaction: nodeify(
|
||||||
txController.addUnapprovedTransaction,
|
txController.addUnapprovedTransaction,
|
||||||
@ -1071,13 +1166,6 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
permissionsController.approvePermissionsRequest,
|
permissionsController.approvePermissionsRequest,
|
||||||
permissionsController,
|
permissionsController,
|
||||||
),
|
),
|
||||||
clearPermissions: permissionsController.clearPermissions.bind(
|
|
||||||
permissionsController,
|
|
||||||
),
|
|
||||||
getApprovedAccounts: nodeify(
|
|
||||||
permissionsController.getAccounts,
|
|
||||||
permissionsController,
|
|
||||||
),
|
|
||||||
rejectPermissionsRequest: nodeify(
|
rejectPermissionsRequest: nodeify(
|
||||||
permissionsController.rejectPermissionsRequest,
|
permissionsController.rejectPermissionsRequest,
|
||||||
permissionsController,
|
permissionsController,
|
||||||
@ -1232,6 +1320,14 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
this.detectTokensController.detectNewTokens,
|
this.detectTokensController.detectNewTokens,
|
||||||
this.detectTokensController,
|
this.detectTokensController,
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// DetectCollectibleController
|
||||||
|
detectCollectibles: process.env.COLLECTIBLES_V1
|
||||||
|
? nodeify(
|
||||||
|
this.collectibleDetectionController.detectCollectibles,
|
||||||
|
this.collectibleDetectionController,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1829,14 +1925,22 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
* @param {Object} msgParams - The params passed to eth_sign.
|
* @param {Object} msgParams - The params passed to eth_sign.
|
||||||
* @param {Function} cb - The callback function called with the signature.
|
* @param {Function} cb - The callback function called with the signature.
|
||||||
*/
|
*/
|
||||||
newUnsignedMessage(msgParams, req) {
|
async newUnsignedMessage(msgParams, req) {
|
||||||
const promise = this.messageManager.addUnapprovedMessageAsync(
|
const data = normalizeMsgData(msgParams.data);
|
||||||
msgParams,
|
let promise;
|
||||||
req,
|
// 64 hex + "0x" at the beginning
|
||||||
);
|
// This is needed because Ethereum's EcSign works only on 32 byte numbers
|
||||||
this.sendUpdate();
|
// For 67 length see: https://github.com/MetaMask/metamask-extension/pull/12679/files#r749479607
|
||||||
this.opts.showUserConfirmation();
|
if (data.length === 66 || data.length === 67) {
|
||||||
return promise;
|
promise = this.messageManager.addUnapprovedMessageAsync(msgParams, req);
|
||||||
|
this.sendUpdate();
|
||||||
|
this.opts.showUserConfirmation();
|
||||||
|
} else {
|
||||||
|
throw ethErrors.rpc.invalidParams(
|
||||||
|
'eth_sign requires 32 byte message hash',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return await promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1845,24 +1949,23 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
* @param {Object} msgParams - The params passed to eth_call.
|
* @param {Object} msgParams - The params passed to eth_call.
|
||||||
* @returns {Promise<Object>} Full state update.
|
* @returns {Promise<Object>} Full state update.
|
||||||
*/
|
*/
|
||||||
signMessage(msgParams) {
|
async signMessage(msgParams) {
|
||||||
log.info('MetaMaskController - signMessage');
|
log.info('MetaMaskController - signMessage');
|
||||||
const msgId = msgParams.metamaskId;
|
const msgId = msgParams.metamaskId;
|
||||||
|
try {
|
||||||
// sets the status op the message to 'approved'
|
// sets the status op the message to 'approved'
|
||||||
// and removes the metamaskId for signing
|
// and removes the metamaskId for signing
|
||||||
return this.messageManager
|
const cleanMsgParams = await this.messageManager.approveMessage(
|
||||||
.approveMessage(msgParams)
|
msgParams,
|
||||||
.then((cleanMsgParams) => {
|
);
|
||||||
// signs the message
|
const rawSig = await this.keyringController.signMessage(cleanMsgParams);
|
||||||
return this.keyringController.signMessage(cleanMsgParams);
|
this.messageManager.setMsgStatusSigned(msgId, rawSig);
|
||||||
})
|
return this.getState();
|
||||||
.then((rawSig) => {
|
} catch (error) {
|
||||||
// tells the listener that the message has been signed
|
log.info('MetaMaskController - eth_sign failed', error);
|
||||||
// and can be returned to the dapp
|
this.messageManager.errorMessage(msgId, error);
|
||||||
this.messageManager.setMsgStatusSigned(msgId, rawSig);
|
throw error;
|
||||||
return this.getState();
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1909,23 +2012,27 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
* @param {Object} msgParams - The params of the message to sign & return to the Dapp.
|
* @param {Object} msgParams - The params of the message to sign & return to the Dapp.
|
||||||
* @returns {Promise<Object>} A full state update.
|
* @returns {Promise<Object>} A full state update.
|
||||||
*/
|
*/
|
||||||
signPersonalMessage(msgParams) {
|
async signPersonalMessage(msgParams) {
|
||||||
log.info('MetaMaskController - signPersonalMessage');
|
log.info('MetaMaskController - signPersonalMessage');
|
||||||
const msgId = msgParams.metamaskId;
|
const msgId = msgParams.metamaskId;
|
||||||
// sets the status op the message to 'approved'
|
// sets the status op the message to 'approved'
|
||||||
// and removes the metamaskId for signing
|
// and removes the metamaskId for signing
|
||||||
return this.personalMessageManager
|
try {
|
||||||
.approveMessage(msgParams)
|
const cleanMsgParams = await this.personalMessageManager.approveMessage(
|
||||||
.then((cleanMsgParams) => {
|
msgParams,
|
||||||
// signs the message
|
);
|
||||||
return this.keyringController.signPersonalMessage(cleanMsgParams);
|
const rawSig = await this.keyringController.signPersonalMessage(
|
||||||
})
|
cleanMsgParams,
|
||||||
.then((rawSig) => {
|
);
|
||||||
// tells the listener that the message has been signed
|
// tells the listener that the message has been signed
|
||||||
// and can be returned to the dapp
|
// and can be returned to the dapp
|
||||||
this.personalMessageManager.setMsgStatusSigned(msgId, rawSig);
|
this.personalMessageManager.setMsgStatusSigned(msgId, rawSig);
|
||||||
return this.getState();
|
return this.getState();
|
||||||
});
|
} catch (error) {
|
||||||
|
log.info('MetaMaskController - eth_personalSign failed', error);
|
||||||
|
this.personalMessageManager.errorMessage(msgId, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -835,7 +835,8 @@ describe('MetaMaskController', function () {
|
|||||||
let msgParams, metamaskMsgs, messages, msgId;
|
let msgParams, metamaskMsgs, messages, msgId;
|
||||||
|
|
||||||
const address = '0xc42edfcc21ed14dda456aa0756c153f7985d8813';
|
const address = '0xc42edfcc21ed14dda456aa0756c153f7985d8813';
|
||||||
const data = '0x43727970746f6b697474696573';
|
const data =
|
||||||
|
'0x0000000000000000000000000000000000000043727970746f6b697474696573';
|
||||||
|
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
sandbox.stub(metamaskController, 'getBalance');
|
sandbox.stub(metamaskController, 'getBalance');
|
||||||
@ -885,6 +886,19 @@ describe('MetaMaskController', function () {
|
|||||||
assert.equal(messages[0].status, TRANSACTION_STATUSES.REJECTED);
|
assert.equal(messages[0].status, TRANSACTION_STATUSES.REJECTED);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('checks message length', async function () {
|
||||||
|
msgParams = {
|
||||||
|
from: address,
|
||||||
|
data: '0xDEADBEEF',
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await metamaskController.newUnsignedMessage(msgParams);
|
||||||
|
} catch (error) {
|
||||||
|
assert.equal(error.message, 'eth_sign requires 32 byte message hash');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('errors when signing a message', async function () {
|
it('errors when signing a message', async function () {
|
||||||
try {
|
try {
|
||||||
await metamaskController.signMessage(messages[0].msgParams);
|
await metamaskController.signMessage(messages[0].msgParams);
|
||||||
|
@ -162,6 +162,10 @@ export default class ExtensionPlatform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addOnRemovedListener(listener) {
|
||||||
|
extension.windows.onRemoved.addListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
getAllWindows() {
|
getAllWindows() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
extension.windows.getAll((windows) => {
|
extension.windows.getAll((windows) => {
|
||||||
|
@ -358,10 +358,14 @@ function createFactoredBuild({
|
|||||||
// lavamoat will add lavapack but it will be removed by bify-module-groups
|
// lavamoat will add lavapack but it will be removed by bify-module-groups
|
||||||
// we will re-add it later by installing a lavapack runtime
|
// we will re-add it later by installing a lavapack runtime
|
||||||
const lavamoatOpts = {
|
const lavamoatOpts = {
|
||||||
policy: path.resolve(__dirname, '../../lavamoat/browserify/policy.json'),
|
policy: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
`../../lavamoat/browserify/${buildType}/policy.json`,
|
||||||
|
),
|
||||||
|
policyName: buildType,
|
||||||
policyOverride: path.resolve(
|
policyOverride: path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
'../../lavamoat/browserify/policy-override.json',
|
`../../lavamoat/browserify/${buildType}/policy-override.json`,
|
||||||
),
|
),
|
||||||
writeAutoPolicy: process.env.WRITE_AUTO_POLICY,
|
writeAutoPolicy: process.env.WRITE_AUTO_POLICY,
|
||||||
};
|
};
|
||||||
@ -456,7 +460,7 @@ function createFactoredBuild({
|
|||||||
groupSet,
|
groupSet,
|
||||||
commonSet,
|
commonSet,
|
||||||
browserPlatforms,
|
browserPlatforms,
|
||||||
useLavamoat: false,
|
useLavamoat: true,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -41,11 +41,16 @@ class RemoveFencedCodeTransform extends Transform {
|
|||||||
// stream, immediately before the "end" event is emitted.
|
// stream, immediately before the "end" event is emitted.
|
||||||
// It applies the transform to the concatenated file contents.
|
// It applies the transform to the concatenated file contents.
|
||||||
_flush(end) {
|
_flush(end) {
|
||||||
const [fileContent, didModify] = removeFencedCode(
|
let fileContent, didModify;
|
||||||
this.filePath,
|
try {
|
||||||
this.buildType,
|
[fileContent, didModify] = removeFencedCode(
|
||||||
Buffer.concat(this._fileBuffers).toString('utf8'),
|
this.filePath,
|
||||||
);
|
this.buildType,
|
||||||
|
Buffer.concat(this._fileBuffers).toString('utf8'),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
return end(error);
|
||||||
|
}
|
||||||
|
|
||||||
const pushAndEnd = () => {
|
const pushAndEnd = () => {
|
||||||
this.push(fileContent);
|
this.push(fileContent);
|
||||||
@ -53,12 +58,11 @@ class RemoveFencedCodeTransform extends Transform {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (this.shouldLintTransformedFiles && didModify) {
|
if (this.shouldLintTransformedFiles && didModify) {
|
||||||
lintTransformedFile(fileContent, this.filePath)
|
return lintTransformedFile(fileContent, this.filePath)
|
||||||
.then(pushAndEnd)
|
.then(pushAndEnd)
|
||||||
.catch((error) => end(error));
|
.catch((error) => end(error));
|
||||||
} else {
|
|
||||||
pushAndEnd();
|
|
||||||
}
|
}
|
||||||
|
return pushAndEnd();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,6 +161,28 @@ describe('build/transforms/remove-fenced-code', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handles error during code fence removal or parsing', async () => {
|
||||||
|
const fileContent = getMinimalFencedCode().concat(
|
||||||
|
'///: END:ONLY_INCLUDE_IN',
|
||||||
|
);
|
||||||
|
|
||||||
|
const stream = createRemoveFencedCodeTransform('main')(mockJsFileName);
|
||||||
|
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
stream.on('error', (error) => {
|
||||||
|
expect(error.message).toStrictEqual(
|
||||||
|
expect.stringContaining(
|
||||||
|
'A valid fence consists of two fence lines, but the file contains an uneven number, "3", of fence lines.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(lintTransformedFileMock).toHaveBeenCalledTimes(0);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.end(fileContent);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('handles transformed file lint failure', async () => {
|
it('handles transformed file lint failure', async () => {
|
||||||
lintTransformedFileMock.mockImplementationOnce(() =>
|
lintTransformedFileMock.mockImplementationOnce(() =>
|
||||||
Promise.reject(new Error('lint failure')),
|
Promise.reject(new Error('lint failure')),
|
||||||
|
13
development/generate-lavamoat-policies.sh
Executable file
13
development/generate-lavamoat-policies.sh
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
set -u
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
# Generate LavaMoat policies for the extension background script for each build
|
||||||
|
# type.
|
||||||
|
# ATTN: This may tax your device when running it locally.
|
||||||
|
concurrently --kill-others-on-fail -n main,beta,flask \
|
||||||
|
"WRITE_AUTO_POLICY=1 yarn dist" \
|
||||||
|
"WRITE_AUTO_POLICY=1 yarn dist --build-type beta" \
|
||||||
|
"WRITE_AUTO_POLICY=1 yarn dist --build-type flask"
|
@ -216,7 +216,12 @@ async function verifyEnglishLocale() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// never consider these messages as unused
|
// never consider these messages as unused
|
||||||
const messageExceptions = ['appName', 'appDescription'];
|
const messageExceptions = [
|
||||||
|
'appName',
|
||||||
|
'appNameBeta',
|
||||||
|
'appNameFlask',
|
||||||
|
'appDescription',
|
||||||
|
];
|
||||||
|
|
||||||
const englishMessages = Object.keys(englishLocale);
|
const englishMessages = Object.keys(englishLocale);
|
||||||
const unusedMessages = englishMessages.filter(
|
const unusedMessages = englishMessages.filter(
|
||||||
|
@ -325,6 +325,7 @@
|
|||||||
"@ethersproject/bignumber": true,
|
"@ethersproject/bignumber": true,
|
||||||
"@ethersproject/bytes": true,
|
"@ethersproject/bytes": true,
|
||||||
"@ethersproject/keccak256": true,
|
"@ethersproject/keccak256": true,
|
||||||
|
"@ethersproject/logger": true,
|
||||||
"@ethersproject/sha2": true,
|
"@ethersproject/sha2": true,
|
||||||
"@ethersproject/strings": true
|
"@ethersproject/strings": true
|
||||||
}
|
}
|
||||||
@ -525,6 +526,7 @@
|
|||||||
"ethjs-util": true,
|
"ethjs-util": true,
|
||||||
"events": true,
|
"events": true,
|
||||||
"human-standard-collectible-abi": true,
|
"human-standard-collectible-abi": true,
|
||||||
|
"human-standard-multi-collectible-abi": true,
|
||||||
"human-standard-token-abi": true,
|
"human-standard-token-abi": true,
|
||||||
"immer": true,
|
"immer": true,
|
||||||
"isomorphic-fetch": true,
|
"isomorphic-fetch": true,
|
||||||
@ -1555,11 +1557,15 @@
|
|||||||
},
|
},
|
||||||
"eth-json-rpc-middleware": {
|
"eth-json-rpc-middleware": {
|
||||||
"globals": {
|
"globals": {
|
||||||
|
"URL": true,
|
||||||
|
"btoa": true,
|
||||||
"console.error": true,
|
"console.error": true,
|
||||||
"fetch": true,
|
"fetch": true,
|
||||||
"setTimeout": true
|
"setTimeout": true
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
|
"@metamask/safe-event-emitter": true,
|
||||||
|
"browser-resolve": true,
|
||||||
"btoa": true,
|
"btoa": true,
|
||||||
"clone": true,
|
"clone": true,
|
||||||
"eth-rpc-errors": true,
|
"eth-rpc-errors": true,
|
55
lavamoat/browserify/flask/policy-override.json
Normal file
55
lavamoat/browserify/flask/policy-override.json
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"resources": {
|
||||||
|
"browser-resolve": {
|
||||||
|
"packages": {
|
||||||
|
"core-js": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"babel-runtime": {
|
||||||
|
"packages": {
|
||||||
|
"@babel/runtime": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node-fetch": {
|
||||||
|
"globals": {
|
||||||
|
"fetch": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lodash": {
|
||||||
|
"globals": {
|
||||||
|
"setTimeout": true,
|
||||||
|
"clearTimeout": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@ethersproject/random": {
|
||||||
|
"globals": {
|
||||||
|
"crypto.getRandomValues": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"browser-passworder": {
|
||||||
|
"globals": {
|
||||||
|
"crypto": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"randombytes": {
|
||||||
|
"globals": {
|
||||||
|
"crypto.getRandomValues": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extensionizer": {
|
||||||
|
"globals": {
|
||||||
|
"console": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"web3": {
|
||||||
|
"globals": {
|
||||||
|
"XMLHttpRequest": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"globals": {
|
||||||
|
"localStorage": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4773
lavamoat/browserify/flask/policy.json
Normal file
4773
lavamoat/browserify/flask/policy.json
Normal file
File diff suppressed because it is too large
Load Diff
55
lavamoat/browserify/main/policy-override.json
Normal file
55
lavamoat/browserify/main/policy-override.json
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"resources": {
|
||||||
|
"browser-resolve": {
|
||||||
|
"packages": {
|
||||||
|
"core-js": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"babel-runtime": {
|
||||||
|
"packages": {
|
||||||
|
"@babel/runtime": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node-fetch": {
|
||||||
|
"globals": {
|
||||||
|
"fetch": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lodash": {
|
||||||
|
"globals": {
|
||||||
|
"setTimeout": true,
|
||||||
|
"clearTimeout": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@ethersproject/random": {
|
||||||
|
"globals": {
|
||||||
|
"crypto.getRandomValues": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"browser-passworder": {
|
||||||
|
"globals": {
|
||||||
|
"crypto": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"randombytes": {
|
||||||
|
"globals": {
|
||||||
|
"crypto.getRandomValues": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extensionizer": {
|
||||||
|
"globals": {
|
||||||
|
"console": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"web3": {
|
||||||
|
"globals": {
|
||||||
|
"XMLHttpRequest": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"globals": {
|
||||||
|
"localStorage": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4773
lavamoat/browserify/main/policy.json
Normal file
4773
lavamoat/browserify/main/policy.json
Normal file
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
@ -12,7 +12,7 @@
|
|||||||
"start": "yarn build:dev dev",
|
"start": "yarn build:dev dev",
|
||||||
"start:lavamoat": "yarn build dev",
|
"start:lavamoat": "yarn build dev",
|
||||||
"dist": "yarn build prod",
|
"dist": "yarn build prod",
|
||||||
"build": "lavamoat development/build/index.js",
|
"build": "yarn lavamoat:build",
|
||||||
"build:dev": "node development/build/index.js",
|
"build:dev": "node development/build/index.js",
|
||||||
"start:test": "yarn build testDev",
|
"start:test": "yarn build testDev",
|
||||||
"benchmark:chrome": "SELENIUM_BROWSER=chrome node test/e2e/benchmark.js",
|
"benchmark:chrome": "SELENIUM_BROWSER=chrome node test/e2e/benchmark.js",
|
||||||
@ -41,8 +41,9 @@
|
|||||||
"test:coverage:path": "nyc --check-coverage yarn test:unit:path",
|
"test:coverage:path": "nyc --check-coverage yarn test:unit:path",
|
||||||
"ganache:start": "./development/run-ganache.sh",
|
"ganache:start": "./development/run-ganache.sh",
|
||||||
"sentry:publish": "node ./development/sentry-publish.js",
|
"sentry:publish": "node ./development/sentry-publish.js",
|
||||||
"lint": "prettier --check '**/*.json' && eslint . --ext js,snap --cache && yarn lint:styles",
|
"lint:prettier": "prettier '**/*.json'",
|
||||||
"lint:fix": "prettier --write '**/*.json' && eslint . --ext js --cache --fix",
|
"lint": "yarn lint:prettier --check '**/*.json' && eslint . --ext js,snap --cache && yarn lint:styles",
|
||||||
|
"lint:fix": "yarn lint:prettier --write '**/*.json' && eslint . --ext js --cache --fix",
|
||||||
"lint:changed": "{ git ls-files --others --exclude-standard ; git diff-index --name-only --diff-filter=d HEAD ; } | grep --regexp='[.]js$' | tr '\\n' '\\0' | xargs -0 eslint",
|
"lint:changed": "{ git ls-files --others --exclude-standard ; git diff-index --name-only --diff-filter=d HEAD ; } | grep --regexp='[.]js$' | tr '\\n' '\\0' | xargs -0 eslint",
|
||||||
"lint:changed:fix": "{ git ls-files --others --exclude-standard ; git diff-index --name-only --diff-filter=d HEAD ; } | grep --regexp='[.]js$' | tr '\\n' '\\0' | xargs -0 eslint --fix",
|
"lint:changed:fix": "{ git ls-files --others --exclude-standard ; git diff-index --name-only --diff-filter=d HEAD ; } | grep --regexp='[.]js$' | tr '\\n' '\\0' | xargs -0 eslint --fix",
|
||||||
"lint:changelog": "auto-changelog validate",
|
"lint:changelog": "auto-changelog validate",
|
||||||
@ -63,9 +64,10 @@
|
|||||||
"storybook:deploy": "storybook-to-ghpages --existing-output-dir storybook-build --remote storybook --branch master",
|
"storybook:deploy": "storybook-to-ghpages --existing-output-dir storybook-build --remote storybook --branch master",
|
||||||
"update-changelog": "auto-changelog update",
|
"update-changelog": "auto-changelog update",
|
||||||
"generate:migration": "./development/generate-migration.sh",
|
"generate:migration": "./development/generate-migration.sh",
|
||||||
"lavamoat:build:auto": "lavamoat ./development/build/index.js --writeAutoPolicy",
|
"lavamoat:build": "lavamoat development/build/index.js --policy lavamoat/build-system/policy.json --policyOverride lavamoat/build-system/policy-override.json",
|
||||||
"lavamoat:debug:build": "lavamoat ./development/build/index.js --writeAutoPolicyDebug",
|
"lavamoat:build:auto": "yarn lavamoat:build --writeAutoPolicy",
|
||||||
"lavamoat:background:auto": "WRITE_AUTO_POLICY=1 yarn build prod",
|
"lavamoat:debug:build": "yarn lavamoat:build --writeAutoPolicyDebug --policydebug lavamoat/build-system/policy-debug.json",
|
||||||
|
"lavamoat:background:auto": "./development/generate-lavamoat-policies.sh",
|
||||||
"lavamoat:auto": "yarn lavamoat:build:auto && yarn lavamoat:background:auto"
|
"lavamoat:auto": "yarn lavamoat:build:auto && yarn lavamoat:background:auto"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
@ -91,7 +93,8 @@
|
|||||||
"netmask": "^2.0.1",
|
"netmask": "^2.0.1",
|
||||||
"pubnub/superagent-proxy": "^3.0.0",
|
"pubnub/superagent-proxy": "^3.0.0",
|
||||||
"pull-ws": "^3.3.2",
|
"pull-ws": "^3.3.2",
|
||||||
"ws": "^7.4.6"
|
"ws": "^7.4.6",
|
||||||
|
"json-schema": "^0.4.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"3box": "^1.10.2",
|
"3box": "^1.10.2",
|
||||||
@ -105,7 +108,7 @@
|
|||||||
"@fortawesome/fontawesome-free": "^5.13.0",
|
"@fortawesome/fontawesome-free": "^5.13.0",
|
||||||
"@material-ui/core": "^4.11.0",
|
"@material-ui/core": "^4.11.0",
|
||||||
"@metamask/contract-metadata": "^1.28.0",
|
"@metamask/contract-metadata": "^1.28.0",
|
||||||
"@metamask/controllers": "^17.0.0",
|
"@metamask/controllers": "^20.0.0",
|
||||||
"@metamask/eth-ledger-bridge-keyring": "^0.10.0",
|
"@metamask/eth-ledger-bridge-keyring": "^0.10.0",
|
||||||
"@metamask/eth-token-tracker": "^3.0.1",
|
"@metamask/eth-token-tracker": "^3.0.1",
|
||||||
"@metamask/etherscan-link": "^2.1.0",
|
"@metamask/etherscan-link": "^2.1.0",
|
||||||
@ -135,7 +138,7 @@
|
|||||||
"eth-ens-namehash": "^2.0.8",
|
"eth-ens-namehash": "^2.0.8",
|
||||||
"eth-json-rpc-filters": "^4.2.1",
|
"eth-json-rpc-filters": "^4.2.1",
|
||||||
"eth-json-rpc-infura": "^5.1.0",
|
"eth-json-rpc-infura": "^5.1.0",
|
||||||
"eth-json-rpc-middleware": "^6.0.0",
|
"eth-json-rpc-middleware": "^8.0.0",
|
||||||
"eth-keyring-controller": "^6.2.0",
|
"eth-keyring-controller": "^6.2.0",
|
||||||
"eth-lattice-keyring": "^0.4.0",
|
"eth-lattice-keyring": "^0.4.0",
|
||||||
"eth-method-registry": "^2.0.0",
|
"eth-method-registry": "^2.0.0",
|
||||||
@ -221,7 +224,7 @@
|
|||||||
"@babel/preset-react": "^7.0.0",
|
"@babel/preset-react": "^7.0.0",
|
||||||
"@babel/register": "^7.5.5",
|
"@babel/register": "^7.5.5",
|
||||||
"@lavamoat/allow-scripts": "^1.0.6",
|
"@lavamoat/allow-scripts": "^1.0.6",
|
||||||
"@lavamoat/lavapack": "^2.0.3",
|
"@lavamoat/lavapack": "^2.0.4",
|
||||||
"@metamask/auto-changelog": "^2.1.0",
|
"@metamask/auto-changelog": "^2.1.0",
|
||||||
"@metamask/eslint-config": "^6.0.0",
|
"@metamask/eslint-config": "^6.0.0",
|
||||||
"@metamask/eslint-config-jest": "^6.0.0",
|
"@metamask/eslint-config-jest": "^6.0.0",
|
||||||
|
@ -30,6 +30,17 @@ export const GAS_RECOMMENDATIONS = {
|
|||||||
HIGH: 'high',
|
HIGH: 'high',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These represent types of gas estimation
|
||||||
|
*/
|
||||||
|
export const PRIORITY_LEVELS = {
|
||||||
|
LOW: 'low',
|
||||||
|
MEDIUM: 'medium',
|
||||||
|
HIGH: 'high',
|
||||||
|
CUSTOM: 'custom',
|
||||||
|
DAPP_SUGGESTED: 'dappSuggested',
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the user customizing their gas preference
|
* Represents the user customizing their gas preference
|
||||||
*/
|
*/
|
||||||
|
@ -140,3 +140,7 @@ export const METAMETRICS_BACKGROUND_PAGE_OBJECT = {
|
|||||||
* @property {() => void} identify - Identify an anonymous user. We do not
|
* @property {() => void} identify - Identify an anonymous user. We do not
|
||||||
* currently use this method.
|
* currently use this method.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export const REJECT_NOTFICIATION_CLOSE = 'Cancel Via Notification Close';
|
||||||
|
export const REJECT_NOTFICIATION_CLOSE_SIG =
|
||||||
|
'Cancel Sig Request Via Notification Close';
|
||||||
|
@ -161,3 +161,13 @@ export const CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP = {
|
|||||||
[OPTIMISM_CHAIN_ID]: 1,
|
[OPTIMISM_CHAIN_ID]: 1,
|
||||||
[OPTIMISM_TESTNET_CHAIN_ID]: 1,
|
[OPTIMISM_TESTNET_CHAIN_ID]: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ethereum JSON-RPC methods that are known to exist but that we intentionally
|
||||||
|
* do not support.
|
||||||
|
*/
|
||||||
|
export const UNSUPPORTED_RPC_METHODS = new Set([
|
||||||
|
// This is implemented later in our middleware stack – specifically, in
|
||||||
|
// eth-json-rpc-middleware – but our UI does not support it.
|
||||||
|
'eth_signTransaction',
|
||||||
|
]);
|
||||||
|
@ -160,6 +160,10 @@
|
|||||||
"toNickname": ""
|
"toNickname": ""
|
||||||
},
|
},
|
||||||
"useTokenDetection": true,
|
"useTokenDetection": true,
|
||||||
|
"advancedGasFee": {
|
||||||
|
"maxBaseFee": "1.5",
|
||||||
|
"priorityFee": "2"
|
||||||
|
},
|
||||||
"tokenList": {
|
"tokenList": {
|
||||||
"0x2260fac5e5542a773aa44fbcfedf7c193bc2c599": {
|
"0x2260fac5e5542a773aa44fbcfedf7c193bc2c599": {
|
||||||
"address": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599",
|
"address": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599",
|
||||||
|
@ -27,38 +27,72 @@ describe('Metamask Import UI', function () {
|
|||||||
async ({ driver }) => {
|
async ({ driver }) => {
|
||||||
await driver.navigate();
|
await driver.navigate();
|
||||||
|
|
||||||
// clicks the continue button on the welcome screen
|
if (process.env.ONBOARDING_V2 === '1') {
|
||||||
await driver.findElement('.welcome-page__header');
|
// welcome
|
||||||
await driver.clickElement({
|
await driver.clickElement('[data-testid="onboarding-import-wallet"]');
|
||||||
text: enLocaleMessages.getStarted.message,
|
|
||||||
tag: 'button',
|
|
||||||
});
|
|
||||||
|
|
||||||
// clicks the "Import Wallet" option
|
// metrics
|
||||||
await driver.clickElement({ text: 'Import wallet', tag: 'button' });
|
await driver.clickElement('[data-testid="metametrics-no-thanks"]');
|
||||||
|
|
||||||
// clicks the "No thanks" option on the metametrics opt-in screen
|
// import with recovery phrase
|
||||||
await driver.clickElement('.btn-secondary');
|
await driver.fill('[data-testid="import-srp-text"]', testSeedPhrase);
|
||||||
|
await driver.clickElement('[data-testid="import-srp-confirm"]');
|
||||||
|
|
||||||
// Import Secret Recovery Phrase
|
// create password
|
||||||
await driver.fill(
|
await driver.fill(
|
||||||
'input[placeholder="Paste Secret Recovery Phrase from clipboard"]',
|
'[data-testid="create-password-new"]',
|
||||||
testSeedPhrase,
|
'correct horse battery staple',
|
||||||
);
|
);
|
||||||
|
await driver.fill(
|
||||||
|
'[data-testid="create-password-confirm"]',
|
||||||
|
'correct horse battery staple',
|
||||||
|
);
|
||||||
|
await driver.clickElement('[data-testid="create-password-terms"]');
|
||||||
|
await driver.clickElement('[data-testid="create-password-import"]');
|
||||||
|
|
||||||
await driver.fill('#password', 'correct horse battery staple');
|
// complete
|
||||||
await driver.fill('#confirm-password', 'correct horse battery staple');
|
await driver.clickElement('[data-testid="onboarding-complete-done"]');
|
||||||
|
|
||||||
await driver.clickElement('.first-time-flow__terms');
|
// pin extension
|
||||||
|
await driver.clickElement('[data-testid="pin-extension-next"]');
|
||||||
|
await driver.clickElement('[data-testid="pin-extension-done"]');
|
||||||
|
} else {
|
||||||
|
// clicks the continue button on the welcome screen
|
||||||
|
await driver.findElement('.welcome-page__header');
|
||||||
|
await driver.clickElement({
|
||||||
|
text: enLocaleMessages.getStarted.message,
|
||||||
|
tag: 'button',
|
||||||
|
});
|
||||||
|
|
||||||
await driver.clickElement({ text: 'Import', tag: 'button' });
|
// clicks the "Import Wallet" option
|
||||||
|
await driver.clickElement({ text: 'Import wallet', tag: 'button' });
|
||||||
|
|
||||||
// clicks through the success screen
|
// clicks the "No thanks" option on the metametrics opt-in screen
|
||||||
await driver.findElement({ text: 'Congratulations', tag: 'div' });
|
await driver.clickElement('.btn-secondary');
|
||||||
await driver.clickElement({
|
|
||||||
text: enLocaleMessages.endOfFlowMessage10.message,
|
// Import Secret Recovery Phrase
|
||||||
tag: 'button',
|
await driver.fill(
|
||||||
});
|
'input[placeholder="Paste Secret Recovery Phrase from clipboard"]',
|
||||||
|
testSeedPhrase,
|
||||||
|
);
|
||||||
|
|
||||||
|
await driver.fill('#password', 'correct horse battery staple');
|
||||||
|
await driver.fill(
|
||||||
|
'#confirm-password',
|
||||||
|
'correct horse battery staple',
|
||||||
|
);
|
||||||
|
|
||||||
|
await driver.clickElement('.first-time-flow__terms');
|
||||||
|
|
||||||
|
await driver.clickElement({ text: 'Import', tag: 'button' });
|
||||||
|
|
||||||
|
// clicks through the success screen
|
||||||
|
await driver.findElement({ text: 'Congratulations', tag: 'div' });
|
||||||
|
await driver.clickElement({
|
||||||
|
text: enLocaleMessages.endOfFlowMessage10.message,
|
||||||
|
tag: 'button',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Show account information
|
// Show account information
|
||||||
await driver.clickElement(
|
await driver.clickElement(
|
||||||
@ -233,10 +267,15 @@ describe('Metamask Import UI', function () {
|
|||||||
// should remove the account
|
// should remove the account
|
||||||
await driver.clickElement({ text: 'Remove', tag: 'button' });
|
await driver.clickElement({ text: 'Remove', tag: 'button' });
|
||||||
|
|
||||||
const currentActiveAccountName = await driver.findElement(
|
// Wait until selected account switches away from removed account to first account
|
||||||
'.selected-account__name',
|
await driver.waitForSelector(
|
||||||
|
{
|
||||||
|
css: '.selected-account__name',
|
||||||
|
text: 'Account 1',
|
||||||
|
},
|
||||||
|
{ timeout: 10000 },
|
||||||
);
|
);
|
||||||
assert.equal(await currentActiveAccountName.getText(), 'Account 1');
|
|
||||||
await driver.delay(regularDelayMs);
|
await driver.delay(regularDelayMs);
|
||||||
await driver.clickElement('.account-menu__icon');
|
await driver.clickElement('.account-menu__icon');
|
||||||
|
|
||||||
|
@ -16,50 +16,6 @@ describe('Metamask Responsive UI', function () {
|
|||||||
async ({ driver }) => {
|
async ({ driver }) => {
|
||||||
await driver.navigate();
|
await driver.navigate();
|
||||||
|
|
||||||
// clicks the continue button on the welcome screen
|
|
||||||
await driver.findElement('.welcome-page__header');
|
|
||||||
await driver.clickElement({
|
|
||||||
text: enLocaleMessages.getStarted.message,
|
|
||||||
tag: 'button',
|
|
||||||
});
|
|
||||||
await driver.delay(tinyDelayMs);
|
|
||||||
|
|
||||||
// clicks the "Create New Wallet" option
|
|
||||||
await driver.clickElement({ text: 'Create a Wallet', tag: 'button' });
|
|
||||||
|
|
||||||
// clicks the "I Agree" option on the metametrics opt-in screen
|
|
||||||
await driver.clickElement('.btn-primary');
|
|
||||||
|
|
||||||
// accepts a secure password
|
|
||||||
await driver.fill(
|
|
||||||
'.first-time-flow__form #create-password',
|
|
||||||
'correct horse battery staple',
|
|
||||||
);
|
|
||||||
await driver.fill(
|
|
||||||
'.first-time-flow__form #confirm-password',
|
|
||||||
'correct horse battery staple',
|
|
||||||
);
|
|
||||||
await driver.clickElement('.first-time-flow__checkbox');
|
|
||||||
await driver.clickElement('.first-time-flow__form button');
|
|
||||||
|
|
||||||
// renders the Secret Recovery Phrase intro screen
|
|
||||||
await driver.clickElement('.seed-phrase-intro__left button');
|
|
||||||
|
|
||||||
// reveals the Secret Recovery Phrase
|
|
||||||
await driver.clickElement(
|
|
||||||
'.reveal-seed-phrase__secret-blocker .reveal-seed-phrase__reveal-button',
|
|
||||||
);
|
|
||||||
const revealedSeedPhrase = await driver.findElement(
|
|
||||||
'.reveal-seed-phrase__secret-words',
|
|
||||||
);
|
|
||||||
const seedPhrase = await revealedSeedPhrase.getText();
|
|
||||||
assert.equal(seedPhrase.split(' ').length, 12);
|
|
||||||
|
|
||||||
await driver.clickElement({
|
|
||||||
text: enLocaleMessages.next.message,
|
|
||||||
tag: 'button',
|
|
||||||
});
|
|
||||||
|
|
||||||
async function clickWordAndWait(word) {
|
async function clickWordAndWait(word) {
|
||||||
await driver.clickElement(
|
await driver.clickElement(
|
||||||
`[data-testid="seed-phrase-sorted"] [data-testid="draggable-seed-${word}"]`,
|
`[data-testid="seed-phrase-sorted"] [data-testid="draggable-seed-${word}"]`,
|
||||||
@ -67,26 +23,126 @@ describe('Metamask Responsive UI', function () {
|
|||||||
await driver.delay(tinyDelayMs);
|
await driver.delay(tinyDelayMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// can retype the Secret Recovery Phrase
|
if (process.env.ONBOARDING_V2 === '1') {
|
||||||
const words = seedPhrase.split(' ');
|
// welcome
|
||||||
for (const word of words) {
|
await driver.clickElement('[data-testid="onboarding-create-wallet"]');
|
||||||
await clickWordAndWait(word);
|
|
||||||
|
// metrics
|
||||||
|
await driver.clickElement('[data-testid="metametrics-no-thanks"]');
|
||||||
|
|
||||||
|
// create password
|
||||||
|
await driver.fill(
|
||||||
|
'[data-testid="create-password-new"]',
|
||||||
|
'correct horse battery staple',
|
||||||
|
);
|
||||||
|
await driver.fill(
|
||||||
|
'[data-testid="create-password-confirm"]',
|
||||||
|
'correct horse battery staple',
|
||||||
|
);
|
||||||
|
await driver.clickElement('[data-testid="create-password-terms"]');
|
||||||
|
await driver.clickElement('[data-testid="create-password-wallet"]');
|
||||||
|
|
||||||
|
// secure wallet
|
||||||
|
await driver.clickElement(
|
||||||
|
'[data-testid="secure-wallet-recommended"]',
|
||||||
|
);
|
||||||
|
|
||||||
|
// review
|
||||||
|
await driver.clickElement('[data-testid="recovery-phrase-reveal"]');
|
||||||
|
const chipTwo = await (
|
||||||
|
await driver.findElement('[data-testid="recovery-phrase-chip-2"]')
|
||||||
|
).getText();
|
||||||
|
const chipThree = await (
|
||||||
|
await driver.findElement('[data-testid="recovery-phrase-chip-3"]')
|
||||||
|
).getText();
|
||||||
|
const chipSeven = await (
|
||||||
|
await driver.findElement('[data-testid="recovery-phrase-chip-7"]')
|
||||||
|
).getText();
|
||||||
|
await driver.clickElement('[data-testid="recovery-phrase-next"]');
|
||||||
|
|
||||||
|
// confirm
|
||||||
|
await driver.fill('[data-testid="recovery-phrase-input-2"]', chipTwo);
|
||||||
|
await driver.fill(
|
||||||
|
'[data-testid="recovery-phrase-input-3"]',
|
||||||
|
chipThree,
|
||||||
|
);
|
||||||
|
await driver.fill(
|
||||||
|
'[data-testid="recovery-phrase-input-7"]',
|
||||||
|
chipSeven,
|
||||||
|
);
|
||||||
|
await driver.clickElement('[data-testid="recovery-phrase-confirm"]');
|
||||||
|
|
||||||
|
// complete
|
||||||
|
await driver.clickElement('[data-testid="onboarding-complete-done"]');
|
||||||
|
|
||||||
|
// pin extension
|
||||||
|
await driver.clickElement('[data-testid="pin-extension-next"]');
|
||||||
|
await driver.clickElement('[data-testid="pin-extension-done"]');
|
||||||
|
} else {
|
||||||
|
// clicks the continue button on the welcome screen
|
||||||
|
await driver.findElement('.welcome-page__header');
|
||||||
|
await driver.clickElement({
|
||||||
|
text: enLocaleMessages.getStarted.message,
|
||||||
|
tag: 'button',
|
||||||
|
});
|
||||||
|
await driver.delay(tinyDelayMs);
|
||||||
|
|
||||||
|
// clicks the "Create New Wallet" option
|
||||||
|
await driver.clickElement({ text: 'Create a Wallet', tag: 'button' });
|
||||||
|
|
||||||
|
// clicks the "I Agree" option on the metametrics opt-in screen
|
||||||
|
await driver.clickElement('.btn-primary');
|
||||||
|
|
||||||
|
// accepts a secure password
|
||||||
|
await driver.fill(
|
||||||
|
'.first-time-flow__form #create-password',
|
||||||
|
'correct horse battery staple',
|
||||||
|
);
|
||||||
|
await driver.fill(
|
||||||
|
'.first-time-flow__form #confirm-password',
|
||||||
|
'correct horse battery staple',
|
||||||
|
);
|
||||||
|
await driver.clickElement('.first-time-flow__checkbox');
|
||||||
|
await driver.clickElement('.first-time-flow__form button');
|
||||||
|
|
||||||
|
// renders the Secret Recovery Phrase intro screen
|
||||||
|
await driver.clickElement('.seed-phrase-intro__left button');
|
||||||
|
|
||||||
|
// reveals the Secret Recovery Phrase
|
||||||
|
await driver.clickElement(
|
||||||
|
'.reveal-seed-phrase__secret-blocker .reveal-seed-phrase__reveal-button',
|
||||||
|
);
|
||||||
|
const revealedSeedPhrase = await driver.findElement(
|
||||||
|
'.reveal-seed-phrase__secret-words',
|
||||||
|
);
|
||||||
|
const seedPhrase = await revealedSeedPhrase.getText();
|
||||||
|
assert.equal(seedPhrase.split(' ').length, 12);
|
||||||
|
|
||||||
|
await driver.clickElement({
|
||||||
|
text: enLocaleMessages.next.message,
|
||||||
|
tag: 'button',
|
||||||
|
});
|
||||||
|
|
||||||
|
// can retype the Secret Recovery Phrase
|
||||||
|
const words = seedPhrase.split(' ');
|
||||||
|
for (const word of words) {
|
||||||
|
await clickWordAndWait(word);
|
||||||
|
}
|
||||||
|
await driver.clickElement({ text: 'Confirm', tag: 'button' });
|
||||||
|
|
||||||
|
// clicks through the success screen
|
||||||
|
await driver.findElement({ text: 'Congratulations', tag: 'div' });
|
||||||
|
await driver.clickElement({
|
||||||
|
text: enLocaleMessages.endOfFlowMessage10.message,
|
||||||
|
tag: 'button',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
await driver.clickElement({ text: 'Confirm', tag: 'button' });
|
|
||||||
|
|
||||||
// clicks through the success screen
|
// assert balance
|
||||||
await driver.findElement({ text: 'Congratulations', tag: 'div' });
|
const balance = await driver.findElement(
|
||||||
await driver.clickElement({
|
'[data-testid="wallet-balance"]',
|
||||||
text: enLocaleMessages.endOfFlowMessage10.message,
|
);
|
||||||
tag: 'button',
|
assert.ok(/^0\sETH$/u.test(await balance.getText()));
|
||||||
});
|
|
||||||
|
|
||||||
// Show account information
|
|
||||||
// balance renders
|
|
||||||
await driver.waitForSelector({
|
|
||||||
css: '[data-testid="eth-overview__primary-currency"]',
|
|
||||||
text: '0 ETH',
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
const { strict: assert } = require('assert');
|
const { strict: assert } = require('assert');
|
||||||
|
const { errorCodes } = require('eth-rpc-errors');
|
||||||
const { withFixtures } = require('../helpers');
|
const { withFixtures } = require('../helpers');
|
||||||
|
|
||||||
describe('MetaMask', function () {
|
describe('MetaMask', function () {
|
||||||
|
const ganacheOptions = {
|
||||||
|
accounts: [
|
||||||
|
{
|
||||||
|
secretKey:
|
||||||
|
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC',
|
||||||
|
balance: 25000000000000000000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
it('provider should inform dapp when switching networks', async function () {
|
it('provider should inform dapp when switching networks', async function () {
|
||||||
const ganacheOptions = {
|
|
||||||
accounts: [
|
|
||||||
{
|
|
||||||
secretKey:
|
|
||||||
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC',
|
|
||||||
balance: 25000000000000000000,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
await withFixtures(
|
await withFixtures(
|
||||||
{
|
{
|
||||||
dapp: true,
|
dapp: true,
|
||||||
@ -62,4 +64,48 @@ describe('MetaMask', function () {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should reject unsupported methods', async function () {
|
||||||
|
await withFixtures(
|
||||||
|
{
|
||||||
|
dapp: true,
|
||||||
|
failOnConsoleError: false,
|
||||||
|
fixtures: 'connected-state',
|
||||||
|
ganacheOptions,
|
||||||
|
title: this.test.title,
|
||||||
|
},
|
||||||
|
async ({ driver }) => {
|
||||||
|
await driver.navigate();
|
||||||
|
await driver.fill('#password', 'correct horse battery staple');
|
||||||
|
await driver.press('#password', driver.Key.ENTER);
|
||||||
|
|
||||||
|
await driver.openNewPage('http://127.0.0.1:8080/');
|
||||||
|
for (const unsupportedMethod of ['eth_signTransaction']) {
|
||||||
|
assert.equal(
|
||||||
|
await driver.executeAsyncScript(`
|
||||||
|
const webDriverCallback = arguments[arguments.length - 1];
|
||||||
|
window.ethereum.request({ method: '${unsupportedMethod}' })
|
||||||
|
.then(() => {
|
||||||
|
console.error('The unsupported method "${unsupportedMethod}" was not rejected.');
|
||||||
|
webDriverCallback(false);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
if (error.code === ${errorCodes.rpc.methodNotSupported}) {
|
||||||
|
webDriverCallback(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(
|
||||||
|
'The unsupported method "${unsupportedMethod}" was rejected with an unexpected error.',
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
webDriverCallback(false);
|
||||||
|
})
|
||||||
|
`),
|
||||||
|
true,
|
||||||
|
`The unsupported method "${unsupportedMethod}" should be rejected by the provider.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
@ -29,6 +29,10 @@ function wrapElementWithAPI(element, driver) {
|
|||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For Selenium WebDriver API documentation, see:
|
||||||
|
* https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html
|
||||||
|
*/
|
||||||
class Driver {
|
class Driver {
|
||||||
/**
|
/**
|
||||||
* @param {!ThenableWebDriver} driver - A {@code WebDriver} instance
|
* @param {!ThenableWebDriver} driver - A {@code WebDriver} instance
|
||||||
@ -49,6 +53,10 @@ class Driver {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async executeAsyncScript(script, ...args) {
|
||||||
|
return this.driver.executeAsyncScript(script, args);
|
||||||
|
}
|
||||||
|
|
||||||
async executeScript(script, ...args) {
|
async executeScript(script, ...args) {
|
||||||
return this.driver.executeScript(script, args);
|
return this.driver.executeScript(script, args);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { JsonRpcEngine } from 'json-rpc-engine';
|
import { JsonRpcEngine, createScaffoldMiddleware } from 'json-rpc-engine';
|
||||||
import scaffoldMiddleware from 'eth-json-rpc-middleware/scaffold';
|
import { providerAsMiddleware } from 'eth-json-rpc-middleware';
|
||||||
import providerAsMiddleware from 'eth-json-rpc-middleware/providerAsMiddleware';
|
|
||||||
import GanacheCore from 'ganache-core';
|
import GanacheCore from 'ganache-core';
|
||||||
|
|
||||||
export function getTestSeed() {
|
export function getTestSeed() {
|
||||||
@ -45,7 +44,7 @@ export function providerFromEngine(engine) {
|
|||||||
export function createTestProviderTools(opts = {}) {
|
export function createTestProviderTools(opts = {}) {
|
||||||
const engine = createEngineForTestData();
|
const engine = createEngineForTestData();
|
||||||
// handle provided hooks
|
// handle provided hooks
|
||||||
engine.push(scaffoldMiddleware(opts.scaffold || {}));
|
engine.push(createScaffoldMiddleware(opts.scaffold || {}));
|
||||||
// handle block tracker methods
|
// handle block tracker methods
|
||||||
engine.push(
|
engine.push(
|
||||||
providerAsMiddleware(
|
providerAsMiddleware(
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
@import 'connected-status-indicator/index';
|
@import 'connected-status-indicator/index';
|
||||||
@import 'edit-gas-display/index';
|
@import 'edit-gas-display/index';
|
||||||
@import 'edit-gas-display-education/index';
|
@import 'edit-gas-display-education/index';
|
||||||
|
@import 'edit-gas-fee-popover/index';
|
||||||
|
@import 'edit-gas-fee-popover/edit-gas-item/index';
|
||||||
@import 'gas-customization/gas-modal-page-container/index';
|
@import 'gas-customization/gas-modal-page-container/index';
|
||||||
@import 'gas-customization/gas-price-button-group/index';
|
@import 'gas-customization/gas-price-button-group/index';
|
||||||
@import 'gas-customization/index';
|
@import 'gas-customization/index';
|
||||||
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
import Box from '../../ui/box';
|
import Box from '../../ui/box';
|
||||||
import Button from '../../ui/button';
|
import Button from '../../ui/button';
|
||||||
import Typography from '../../ui/typography/typography';
|
import Typography from '../../ui/typography/typography';
|
||||||
|
import NewCollectiblesNotice from '../new-collectibles-notice';
|
||||||
import {
|
import {
|
||||||
COLORS,
|
COLORS,
|
||||||
TYPOGRAPHY,
|
TYPOGRAPHY,
|
||||||
@ -15,6 +16,7 @@ import { useI18nContext } from '../../../hooks/useI18nContext';
|
|||||||
|
|
||||||
export default function CollectiblesList({ onAddNFT }) {
|
export default function CollectiblesList({ onAddNFT }) {
|
||||||
const collectibles = [];
|
const collectibles = [];
|
||||||
|
const newNFTsDetected = true;
|
||||||
const t = useI18nContext();
|
const t = useI18nContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -22,7 +24,8 @@ export default function CollectiblesList({ onAddNFT }) {
|
|||||||
{collectibles.length > 0 ? (
|
{collectibles.length > 0 ? (
|
||||||
<span>{JSON.stringify(collectibles)}</span>
|
<span>{JSON.stringify(collectibles)}</span>
|
||||||
) : (
|
) : (
|
||||||
<Box padding={[4, 0, 4, 0]}>
|
<Box padding={[6, 12, 6, 12]}>
|
||||||
|
{newNFTsDetected ? <NewCollectiblesNotice /> : null}
|
||||||
<Box justifyContent={JUSTIFY_CONTENT.CENTER}>
|
<Box justifyContent={JUSTIFY_CONTENT.CENTER}>
|
||||||
<img src="./images/no-nfts.svg" />
|
<img src="./images/no-nfts.svg" />
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -10,6 +10,14 @@ import ConfirmPageContainer, {
|
|||||||
ConfirmPageContainerNavigation,
|
ConfirmPageContainerNavigation,
|
||||||
} from '.';
|
} from '.';
|
||||||
|
|
||||||
|
jest.mock('../../../store/actions', () => ({
|
||||||
|
disconnectGasFeeEstimatePoller: jest.fn(),
|
||||||
|
getGasFeeEstimatesAndStartPolling: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(() => Promise.resolve()),
|
||||||
|
addPollingTokenToAppState: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('Confirm Page Container Container Test', () => {
|
describe('Confirm Page Container Container Test', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
@ -31,6 +39,8 @@ describe('Confirm Page Container Container Test', () => {
|
|||||||
selectedAddress: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5',
|
selectedAddress: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5',
|
||||||
addressBook: [],
|
addressBook: [],
|
||||||
chainId: 'test',
|
chainId: 'test',
|
||||||
|
identities: [],
|
||||||
|
featureFlags: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { Tabs, Tab } from '../../../ui/tabs';
|
import { Tabs, Tab } from '../../../ui/tabs';
|
||||||
import ErrorMessage from '../../../ui/error-message';
|
import ErrorMessage from '../../../ui/error-message';
|
||||||
|
import ActionableMessage from '../../../ui/actionable-message/actionable-message';
|
||||||
import { PageContainerFooter } from '../../../ui/page-container';
|
import { PageContainerFooter } from '../../../ui/page-container';
|
||||||
import { ConfirmPageContainerSummary, ConfirmPageContainerWarning } from '.';
|
import { ConfirmPageContainerSummary, ConfirmPageContainerWarning } from '.';
|
||||||
|
|
||||||
@ -17,6 +18,7 @@ export default class ConfirmPageContainerContent extends Component {
|
|||||||
detailsComponent: PropTypes.node,
|
detailsComponent: PropTypes.node,
|
||||||
errorKey: PropTypes.string,
|
errorKey: PropTypes.string,
|
||||||
errorMessage: PropTypes.string,
|
errorMessage: PropTypes.string,
|
||||||
|
hasSimulationError: PropTypes.bool,
|
||||||
hideSubtitle: PropTypes.bool,
|
hideSubtitle: PropTypes.bool,
|
||||||
identiconAddress: PropTypes.string,
|
identiconAddress: PropTypes.string,
|
||||||
nonce: PropTypes.string,
|
nonce: PropTypes.string,
|
||||||
@ -31,8 +33,10 @@ export default class ConfirmPageContainerContent extends Component {
|
|||||||
onCancel: PropTypes.func,
|
onCancel: PropTypes.func,
|
||||||
cancelText: PropTypes.string,
|
cancelText: PropTypes.string,
|
||||||
onSubmit: PropTypes.func,
|
onSubmit: PropTypes.func,
|
||||||
|
onConfirmAnyways: PropTypes.func,
|
||||||
submitText: PropTypes.string,
|
submitText: PropTypes.string,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
|
hideConfirmAnyways: PropTypes.bool,
|
||||||
unapprovedTxCount: PropTypes.number,
|
unapprovedTxCount: PropTypes.number,
|
||||||
rejectNText: PropTypes.string,
|
rejectNText: PropTypes.string,
|
||||||
hideTitle: PropTypes.boolean,
|
hideTitle: PropTypes.boolean,
|
||||||
@ -71,6 +75,7 @@ export default class ConfirmPageContainerContent extends Component {
|
|||||||
action,
|
action,
|
||||||
errorKey,
|
errorKey,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
|
hasSimulationError,
|
||||||
title,
|
title,
|
||||||
titleComponent,
|
titleComponent,
|
||||||
subtitleComponent,
|
subtitleComponent,
|
||||||
@ -91,14 +96,32 @@ export default class ConfirmPageContainerContent extends Component {
|
|||||||
origin,
|
origin,
|
||||||
ethGasPriceWarning,
|
ethGasPriceWarning,
|
||||||
hideTitle,
|
hideTitle,
|
||||||
|
onConfirmAnyways,
|
||||||
|
hideConfirmAnyways,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const primaryAction = hideConfirmAnyways
|
||||||
|
? null
|
||||||
|
: {
|
||||||
|
label: this.context.t('tryAnywayOption'),
|
||||||
|
onClick: onConfirmAnyways,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="confirm-page-container-content">
|
<div className="confirm-page-container-content">
|
||||||
{warning ? <ConfirmPageContainerWarning warning={warning} /> : null}
|
{warning ? <ConfirmPageContainerWarning warning={warning} /> : null}
|
||||||
{ethGasPriceWarning && (
|
{ethGasPriceWarning && (
|
||||||
<ConfirmPageContainerWarning warning={ethGasPriceWarning} />
|
<ConfirmPageContainerWarning warning={ethGasPriceWarning} />
|
||||||
)}
|
)}
|
||||||
|
{hasSimulationError && (
|
||||||
|
<div className="confirm-page-container-content__error-container">
|
||||||
|
<ActionableMessage
|
||||||
|
type="danger"
|
||||||
|
primaryAction={primaryAction}
|
||||||
|
message={this.context.t('simulationErrorMessage')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<ConfirmPageContainerSummary
|
<ConfirmPageContainerSummary
|
||||||
className={classnames({
|
className={classnames({
|
||||||
'confirm-page-container-summary--border':
|
'confirm-page-container-summary--border':
|
||||||
@ -115,7 +138,7 @@ export default class ConfirmPageContainerContent extends Component {
|
|||||||
hideTitle={hideTitle}
|
hideTitle={hideTitle}
|
||||||
/>
|
/>
|
||||||
{this.renderContent()}
|
{this.renderContent()}
|
||||||
{(errorKey || errorMessage) && (
|
{(errorKey || errorMessage) && !hasSimulationError && (
|
||||||
<div className="confirm-page-container-content__error-container">
|
<div className="confirm-page-container-content__error-container">
|
||||||
<ErrorMessage errorMessage={errorMessage} errorKey={errorKey} />
|
<ErrorMessage errorMessage={errorMessage} errorKey={errorKey} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,124 @@
|
|||||||
|
import { fireEvent } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
import configureMockStore from 'redux-mock-store';
|
||||||
|
import { renderWithProvider } from '../../../../../test/lib/render-helpers';
|
||||||
|
import { TRANSACTION_ERROR_KEY } from '../../../../helpers/constants/error-keys';
|
||||||
|
import ConfirmPageContainerContent from './confirm-page-container-content.component';
|
||||||
|
|
||||||
|
describe('Confirm Page Container Content', () => {
|
||||||
|
const mockStore = {
|
||||||
|
metamask: {
|
||||||
|
provider: {
|
||||||
|
type: 'test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = configureMockStore()(mockStore);
|
||||||
|
|
||||||
|
let props = {};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const mockOnCancel = jest.fn();
|
||||||
|
const mockOnCancelAll = jest.fn();
|
||||||
|
const mockOnSubmit = jest.fn();
|
||||||
|
const mockOnConfirmAnyways = jest.fn();
|
||||||
|
props = {
|
||||||
|
action: ' Withdraw Stake',
|
||||||
|
errorMessage: null,
|
||||||
|
errorKey: null,
|
||||||
|
hasSimulationError: true,
|
||||||
|
onCancelAll: mockOnCancelAll,
|
||||||
|
onCancel: mockOnCancel,
|
||||||
|
cancelText: 'Reject',
|
||||||
|
onSubmit: mockOnSubmit,
|
||||||
|
onConfirmAnyways: mockOnConfirmAnyways,
|
||||||
|
submitText: 'Confirm',
|
||||||
|
disabled: true,
|
||||||
|
origin: 'http://localhost:4200',
|
||||||
|
hideTitle: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('render ConfirmPageContainer component with simulation error', async () => {
|
||||||
|
const { queryByText, getByText } = renderWithProvider(
|
||||||
|
<ConfirmPageContainerContent {...props} />,
|
||||||
|
store,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
queryByText('Transaction Error. Exception thrown in contract code.'),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
queryByText(
|
||||||
|
'This transaction is expected to fail. Trying to execute it is expected to be expensive but fail, and is not recommended.',
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(queryByText('I will try anyway')).toBeInTheDocument();
|
||||||
|
|
||||||
|
const confirmButton = getByText('Confirm');
|
||||||
|
expect(getByText('Confirm').closest('button')).toBeDisabled();
|
||||||
|
fireEvent.click(confirmButton);
|
||||||
|
expect(props.onSubmit).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
const iWillTryButton = getByText('I will try anyway');
|
||||||
|
fireEvent.click(iWillTryButton);
|
||||||
|
expect(props.onConfirmAnyways).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
const cancelButton = getByText('Reject');
|
||||||
|
fireEvent.click(cancelButton);
|
||||||
|
expect(props.onCancel).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('render ConfirmPageContainer component with another error', async () => {
|
||||||
|
props.hasSimulationError = false;
|
||||||
|
props.disabled = true;
|
||||||
|
props.errorKey = TRANSACTION_ERROR_KEY;
|
||||||
|
const { queryByText, getByText } = renderWithProvider(
|
||||||
|
<ConfirmPageContainerContent {...props} />,
|
||||||
|
store,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
queryByText(
|
||||||
|
'This transaction is expected to fail. Trying to execute it is expected to be expensive but fail, and is not recommended.',
|
||||||
|
),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
expect(queryByText('I will try anyway')).not.toBeInTheDocument();
|
||||||
|
expect(getByText('Confirm').closest('button')).toBeDisabled();
|
||||||
|
expect(
|
||||||
|
getByText('Transaction Error. Exception thrown in contract code.'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
const cancelButton = getByText('Reject');
|
||||||
|
fireEvent.click(cancelButton);
|
||||||
|
expect(props.onCancel).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('render ConfirmPageContainer component with no errors', async () => {
|
||||||
|
props.hasSimulationError = false;
|
||||||
|
props.disabled = false;
|
||||||
|
const { queryByText, getByText } = renderWithProvider(
|
||||||
|
<ConfirmPageContainerContent {...props} />,
|
||||||
|
store,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
queryByText(
|
||||||
|
'This transaction is expected to fail. Trying to execute it is expected to be expensive but fail, and is not recommended.',
|
||||||
|
),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
queryByText('Transaction Error. Exception thrown in contract code.'),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
expect(queryByText('I will try anyway')).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
const confirmButton = getByText('Confirm');
|
||||||
|
fireEvent.click(confirmButton);
|
||||||
|
expect(props.onSubmit).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
const cancelButton = getByText('Reject');
|
||||||
|
fireEvent.click(cancelButton);
|
||||||
|
expect(props.onCancel).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
@ -4,15 +4,20 @@ import SenderToRecipient from '../../ui/sender-to-recipient';
|
|||||||
import { PageContainerFooter } from '../../ui/page-container';
|
import { PageContainerFooter } from '../../ui/page-container';
|
||||||
import EditGasPopover from '../edit-gas-popover';
|
import EditGasPopover from '../edit-gas-popover';
|
||||||
import { EDIT_GAS_MODES } from '../../../../shared/constants/gas';
|
import { EDIT_GAS_MODES } from '../../../../shared/constants/gas';
|
||||||
|
import { GasFeeContextProvider } from '../../../contexts/gasFee';
|
||||||
import ErrorMessage from '../../ui/error-message';
|
import ErrorMessage from '../../ui/error-message';
|
||||||
import { TRANSACTION_TYPES } from '../../../../shared/constants/transaction';
|
import { TRANSACTION_TYPES } from '../../../../shared/constants/transaction';
|
||||||
import Dialog from '../../ui/dialog';
|
import Dialog from '../../ui/dialog';
|
||||||
|
import EditGasFeePopover from '../edit-gas-fee-popover/edit-gas-fee-popover';
|
||||||
import {
|
import {
|
||||||
ConfirmPageContainerHeader,
|
ConfirmPageContainerHeader,
|
||||||
ConfirmPageContainerContent,
|
ConfirmPageContainerContent,
|
||||||
ConfirmPageContainerNavigation,
|
ConfirmPageContainerNavigation,
|
||||||
} from '.';
|
} from '.';
|
||||||
|
|
||||||
|
// eslint-disable-next-line prefer-destructuring
|
||||||
|
const EIP_1559_V2 = process.env.EIP_1559_V2;
|
||||||
|
|
||||||
export default class ConfirmPageContainer extends Component {
|
export default class ConfirmPageContainer extends Component {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
t: PropTypes.func,
|
t: PropTypes.func,
|
||||||
@ -135,102 +140,107 @@ export default class ConfirmPageContainer extends Component {
|
|||||||
currentTransaction.txParams?.value === '0x0';
|
currentTransaction.txParams?.value === '0x0';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-container">
|
<GasFeeContextProvider transaction={currentTransaction}>
|
||||||
<ConfirmPageContainerNavigation
|
<div className="page-container">
|
||||||
totalTx={totalTx}
|
<ConfirmPageContainerNavigation
|
||||||
positionOfCurrentTx={positionOfCurrentTx}
|
totalTx={totalTx}
|
||||||
nextTxId={nextTxId}
|
positionOfCurrentTx={positionOfCurrentTx}
|
||||||
prevTxId={prevTxId}
|
nextTxId={nextTxId}
|
||||||
showNavigation={showNavigation}
|
prevTxId={prevTxId}
|
||||||
onNextTx={(txId) => onNextTx(txId)}
|
showNavigation={showNavigation}
|
||||||
firstTx={firstTx}
|
onNextTx={(txId) => onNextTx(txId)}
|
||||||
lastTx={lastTx}
|
firstTx={firstTx}
|
||||||
ofText={ofText}
|
lastTx={lastTx}
|
||||||
requestsWaitingText={requestsWaitingText}
|
ofText={ofText}
|
||||||
/>
|
requestsWaitingText={requestsWaitingText}
|
||||||
<ConfirmPageContainerHeader
|
/>
|
||||||
showEdit={showEdit}
|
<ConfirmPageContainerHeader
|
||||||
onEdit={() => onEdit()}
|
showEdit={showEdit}
|
||||||
showAccountInHeader={showAccountInHeader}
|
onEdit={() => onEdit()}
|
||||||
accountAddress={fromAddress}
|
showAccountInHeader={showAccountInHeader}
|
||||||
>
|
accountAddress={fromAddress}
|
||||||
{hideSenderToRecipient ? null : (
|
>
|
||||||
<SenderToRecipient
|
{hideSenderToRecipient ? null : (
|
||||||
senderName={fromName}
|
<SenderToRecipient
|
||||||
senderAddress={fromAddress}
|
senderName={fromName}
|
||||||
recipientName={toName}
|
senderAddress={fromAddress}
|
||||||
recipientAddress={toAddress}
|
recipientName={toName}
|
||||||
recipientEns={toEns}
|
recipientAddress={toAddress}
|
||||||
recipientNickname={toNickname}
|
recipientEns={toEns}
|
||||||
|
recipientNickname={toNickname}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ConfirmPageContainerHeader>
|
||||||
|
<div>
|
||||||
|
{showAddToAddressDialog && (
|
||||||
|
<Dialog
|
||||||
|
type="message"
|
||||||
|
className="send__dialog"
|
||||||
|
onClick={() => showAddToAddressBookModal()}
|
||||||
|
>
|
||||||
|
{this.context.t('newAccountDetectedDialogMessage')}
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{contentComponent || (
|
||||||
|
<ConfirmPageContainerContent
|
||||||
|
action={action}
|
||||||
|
title={title}
|
||||||
|
titleComponent={titleComponent}
|
||||||
|
subtitleComponent={subtitleComponent}
|
||||||
|
hideSubtitle={hideSubtitle}
|
||||||
|
detailsComponent={detailsComponent}
|
||||||
|
dataComponent={dataComponent}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
errorKey={errorKey}
|
||||||
|
identiconAddress={identiconAddress}
|
||||||
|
nonce={nonce}
|
||||||
|
warning={warning}
|
||||||
|
onCancelAll={onCancelAll}
|
||||||
|
onCancel={onCancel}
|
||||||
|
cancelText={this.context.t('reject')}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
submitText={this.context.t('confirm')}
|
||||||
|
disabled={disabled}
|
||||||
|
unapprovedTxCount={unapprovedTxCount}
|
||||||
|
rejectNText={this.context.t('rejectTxsN', [unapprovedTxCount])}
|
||||||
|
origin={origin}
|
||||||
|
ethGasPriceWarning={ethGasPriceWarning}
|
||||||
|
hideTitle={hideTitle}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ConfirmPageContainerHeader>
|
{shouldDisplayWarning && (
|
||||||
<div>
|
<div className="confirm-approve-content__warning">
|
||||||
{showAddToAddressDialog && (
|
<ErrorMessage errorKey={errorKey} />
|
||||||
<Dialog
|
</div>
|
||||||
type="message"
|
)}
|
||||||
className="send__dialog"
|
{contentComponent && (
|
||||||
onClick={() => showAddToAddressBookModal()}
|
<PageContainerFooter
|
||||||
|
onCancel={onCancel}
|
||||||
|
cancelText={this.context.t('reject')}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
submitText={this.context.t('confirm')}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{this.context.t('newAccountDetectedDialogMessage')}
|
{unapprovedTxCount > 1 && (
|
||||||
</Dialog>
|
<a onClick={onCancelAll}>
|
||||||
|
{this.context.t('rejectTxsN', [unapprovedTxCount])}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</PageContainerFooter>
|
||||||
|
)}
|
||||||
|
{editingGas && !EIP_1559_V2 && (
|
||||||
|
<EditGasPopover
|
||||||
|
mode={EDIT_GAS_MODES.MODIFY_IN_PLACE}
|
||||||
|
onClose={handleCloseEditGas}
|
||||||
|
transaction={currentTransaction}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{editingGas && EIP_1559_V2 && (
|
||||||
|
<EditGasFeePopover onClose={handleCloseEditGas} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{contentComponent || (
|
</GasFeeContextProvider>
|
||||||
<ConfirmPageContainerContent
|
|
||||||
action={action}
|
|
||||||
title={title}
|
|
||||||
titleComponent={titleComponent}
|
|
||||||
subtitleComponent={subtitleComponent}
|
|
||||||
hideSubtitle={hideSubtitle}
|
|
||||||
detailsComponent={detailsComponent}
|
|
||||||
dataComponent={dataComponent}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
errorKey={errorKey}
|
|
||||||
identiconAddress={identiconAddress}
|
|
||||||
nonce={nonce}
|
|
||||||
warning={warning}
|
|
||||||
onCancelAll={onCancelAll}
|
|
||||||
onCancel={onCancel}
|
|
||||||
cancelText={this.context.t('reject')}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
submitText={this.context.t('confirm')}
|
|
||||||
disabled={disabled}
|
|
||||||
unapprovedTxCount={unapprovedTxCount}
|
|
||||||
rejectNText={this.context.t('rejectTxsN', [unapprovedTxCount])}
|
|
||||||
origin={origin}
|
|
||||||
ethGasPriceWarning={ethGasPriceWarning}
|
|
||||||
hideTitle={hideTitle}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{shouldDisplayWarning && (
|
|
||||||
<div className="confirm-approve-content__warning">
|
|
||||||
<ErrorMessage errorKey={errorKey} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{contentComponent && (
|
|
||||||
<PageContainerFooter
|
|
||||||
onCancel={onCancel}
|
|
||||||
cancelText={this.context.t('reject')}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
submitText={this.context.t('confirm')}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
{unapprovedTxCount > 1 && (
|
|
||||||
<a onClick={onCancelAll}>
|
|
||||||
{this.context.t('rejectTxsN', [unapprovedTxCount])}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</PageContainerFooter>
|
|
||||||
)}
|
|
||||||
{editingGas && (
|
|
||||||
<EditGasPopover
|
|
||||||
mode={EDIT_GAS_MODES.MODIFY_IN_PLACE}
|
|
||||||
onClose={handleCloseEditGas}
|
|
||||||
transaction={currentTransaction}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,68 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { PRIORITY_LEVELS } from '../../../../shared/constants/gas';
|
||||||
|
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||||
|
import Popover from '../../ui/popover';
|
||||||
|
import I18nValue from '../../ui/i18n-value';
|
||||||
|
import LoadingHeartBeat from '../../ui/loading-heartbeat';
|
||||||
|
|
||||||
|
import EditGasItem from './edit-gas-item';
|
||||||
|
|
||||||
|
const EditGasFeePopover = ({ onClose }) => {
|
||||||
|
const t = useI18nContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
title={t('editGasFeeModalTitle')}
|
||||||
|
onClose={onClose}
|
||||||
|
className="edit-gas-fee-popover"
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
{process.env.IN_TEST === 'true' ? null : <LoadingHeartBeat />}
|
||||||
|
<div className="edit-gas-fee-popover__wrapper">
|
||||||
|
<div className="edit-gas-fee-popover__content">
|
||||||
|
<div className="edit-gas-fee-popover__content__header">
|
||||||
|
<span className="edit-gas-fee-popover__content__header-option">
|
||||||
|
<I18nValue messageKey="gasOption" />
|
||||||
|
</span>
|
||||||
|
<span className="edit-gas-fee-popover__content__header-time">
|
||||||
|
<I18nValue messageKey="time" />
|
||||||
|
</span>
|
||||||
|
<span className="edit-gas-fee-popover__content__header-max-fee">
|
||||||
|
<I18nValue messageKey="maxFee" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<EditGasItem
|
||||||
|
priorityLevel={PRIORITY_LEVELS.LOW}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
<EditGasItem
|
||||||
|
priorityLevel={PRIORITY_LEVELS.MEDIUM}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
<EditGasItem
|
||||||
|
priorityLevel={PRIORITY_LEVELS.HIGH}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
<div className="edit-gas-fee-popover__content__separator" />
|
||||||
|
<EditGasItem
|
||||||
|
priorityLevel={PRIORITY_LEVELS.DAPP_SUGGESTED}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
<EditGasItem
|
||||||
|
priorityLevel={PRIORITY_LEVELS.CUSTOM}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
EditGasFeePopover.propTypes = {
|
||||||
|
onClose: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditGasFeePopover;
|
@ -0,0 +1,95 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { screen } from '@testing-library/react';
|
||||||
|
|
||||||
|
import { renderWithProvider } from '../../../../test/lib/render-helpers';
|
||||||
|
import { ETH } from '../../../helpers/constants/common';
|
||||||
|
import configureStore from '../../../store/store';
|
||||||
|
import { GasFeeContextProvider } from '../../../contexts/gasFee';
|
||||||
|
|
||||||
|
import EditGasFeePopover from './edit-gas-fee-popover';
|
||||||
|
|
||||||
|
jest.mock('../../../store/actions', () => ({
|
||||||
|
disconnectGasFeeEstimatePoller: jest.fn(),
|
||||||
|
getGasFeeEstimatesAndStartPolling: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(() => Promise.resolve()),
|
||||||
|
addPollingTokenToAppState: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const MOCK_FEE_ESTIMATE = {
|
||||||
|
low: {
|
||||||
|
minWaitTimeEstimate: 360000,
|
||||||
|
maxWaitTimeEstimate: 300000,
|
||||||
|
suggestedMaxPriorityFeePerGas: '3',
|
||||||
|
suggestedMaxFeePerGas: '53',
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
minWaitTimeEstimate: 30000,
|
||||||
|
maxWaitTimeEstimate: 60000,
|
||||||
|
suggestedMaxPriorityFeePerGas: '7',
|
||||||
|
suggestedMaxFeePerGas: '70',
|
||||||
|
},
|
||||||
|
high: {
|
||||||
|
minWaitTimeEstimate: 15000,
|
||||||
|
maxWaitTimeEstimate: 15000,
|
||||||
|
suggestedMaxPriorityFeePerGas: '10',
|
||||||
|
suggestedMaxFeePerGas: '100',
|
||||||
|
},
|
||||||
|
estimatedBaseFee: '50',
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderComponent = () => {
|
||||||
|
const store = configureStore({
|
||||||
|
metamask: {
|
||||||
|
nativeCurrency: ETH,
|
||||||
|
provider: {},
|
||||||
|
cachedBalances: {},
|
||||||
|
accounts: {
|
||||||
|
'0xAddress': {
|
||||||
|
address: '0xAddress',
|
||||||
|
balance: '0x176e5b6f173ebe66',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selectedAddress: '0xAddress',
|
||||||
|
featureFlags: { advancedInlineGas: true },
|
||||||
|
gasFeeEstimates: MOCK_FEE_ESTIMATE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return renderWithProvider(
|
||||||
|
<GasFeeContextProvider transaction={{ txParams: { gas: '0x5208' } }}>
|
||||||
|
<EditGasFeePopover />
|
||||||
|
</GasFeeContextProvider>,
|
||||||
|
store,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('EditGasFeePopover', () => {
|
||||||
|
it('should renders low / medium / high options', () => {
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
expect(screen.queryByText('🐢')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('🦊')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('🦍')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('🌐')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('⚙')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Low')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Market')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Aggressive')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Site')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Advanced')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show time estimates', () => {
|
||||||
|
renderComponent();
|
||||||
|
expect(screen.queryAllByText('5 min')).toHaveLength(2);
|
||||||
|
expect(screen.queryByText('15 sec')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show gas fee estimates', () => {
|
||||||
|
renderComponent();
|
||||||
|
expect(screen.queryByTitle('0.001113 ETH')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTitle('0.00147 ETH')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTitle('0.0021 ETH')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,150 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
import { getMaximumGasTotalInHexWei } from '../../../../../shared/modules/gas.utils';
|
||||||
|
import { PRIORITY_LEVELS } from '../../../../../shared/constants/gas';
|
||||||
|
import { PRIORITY_LEVEL_ICON_MAP } from '../../../../helpers/constants/gas';
|
||||||
|
import { PRIMARY } from '../../../../helpers/constants/common';
|
||||||
|
import {
|
||||||
|
decGWEIToHexWEI,
|
||||||
|
decimalToHex,
|
||||||
|
hexWEIToDecGWEI,
|
||||||
|
} from '../../../../helpers/utils/conversions.util';
|
||||||
|
import { getAdvancedGasFeeValues } from '../../../../selectors';
|
||||||
|
import { toHumanReadableTime } from '../../../../helpers/utils/util';
|
||||||
|
import { useGasFeeContext } from '../../../../contexts/gasFee';
|
||||||
|
import { useI18nContext } from '../../../../hooks/useI18nContext';
|
||||||
|
import I18nValue from '../../../ui/i18n-value';
|
||||||
|
import InfoTooltip from '../../../ui/info-tooltip';
|
||||||
|
import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display';
|
||||||
|
|
||||||
|
import { useCustomTimeEstimate } from './useCustomTimeEstimate';
|
||||||
|
|
||||||
|
const EditGasItem = ({ priorityLevel, onClose }) => {
|
||||||
|
const {
|
||||||
|
estimateUsed,
|
||||||
|
gasFeeEstimates,
|
||||||
|
gasLimit,
|
||||||
|
maxFeePerGas: maxFeePerGasValue,
|
||||||
|
maxPriorityFeePerGas: maxPriorityFeePerGasValue,
|
||||||
|
updateTransactionUsingGasFeeEstimates,
|
||||||
|
transaction: { dappSuggestedGasFees },
|
||||||
|
} = useGasFeeContext();
|
||||||
|
const t = useI18nContext();
|
||||||
|
const advancedGasFeeValues = useSelector(getAdvancedGasFeeValues);
|
||||||
|
let maxFeePerGas;
|
||||||
|
let maxPriorityFeePerGas;
|
||||||
|
let minWaitTime;
|
||||||
|
|
||||||
|
if (gasFeeEstimates[priorityLevel]) {
|
||||||
|
maxFeePerGas = gasFeeEstimates[priorityLevel].suggestedMaxFeePerGas;
|
||||||
|
} else if (
|
||||||
|
priorityLevel === PRIORITY_LEVELS.DAPP_SUGGESTED &&
|
||||||
|
dappSuggestedGasFees
|
||||||
|
) {
|
||||||
|
maxFeePerGas = hexWEIToDecGWEI(dappSuggestedGasFees.maxFeePerGas);
|
||||||
|
maxPriorityFeePerGas = hexWEIToDecGWEI(
|
||||||
|
dappSuggestedGasFees.maxPriorityFeePerGas,
|
||||||
|
);
|
||||||
|
} else if (priorityLevel === PRIORITY_LEVELS.CUSTOM) {
|
||||||
|
if (estimateUsed === PRIORITY_LEVELS.CUSTOM) {
|
||||||
|
maxFeePerGas = maxFeePerGasValue;
|
||||||
|
maxPriorityFeePerGas = maxPriorityFeePerGasValue;
|
||||||
|
} else if (advancedGasFeeValues) {
|
||||||
|
maxFeePerGas =
|
||||||
|
gasFeeEstimates.estimatedBaseFee *
|
||||||
|
parseFloat(advancedGasFeeValues.maxBaseFee);
|
||||||
|
maxPriorityFeePerGas = advancedGasFeeValues.priorityFee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { waitTimeEstimate } = useCustomTimeEstimate({
|
||||||
|
gasFeeEstimates,
|
||||||
|
maxFeePerGas,
|
||||||
|
maxPriorityFeePerGas,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (gasFeeEstimates[priorityLevel]) {
|
||||||
|
minWaitTime =
|
||||||
|
priorityLevel === PRIORITY_LEVELS.HIGH
|
||||||
|
? gasFeeEstimates?.high.minWaitTimeEstimate
|
||||||
|
: gasFeeEstimates?.low.maxWaitTimeEstimate;
|
||||||
|
} else {
|
||||||
|
minWaitTime = waitTimeEstimate;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hexMaximumTransactionFee = maxFeePerGas
|
||||||
|
? getMaximumGasTotalInHexWei({
|
||||||
|
gasLimit: decimalToHex(gasLimit),
|
||||||
|
maxFeePerGas: decGWEIToHexWEI(maxFeePerGas),
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const onOptionSelect = () => {
|
||||||
|
if (priorityLevel !== PRIORITY_LEVELS.CUSTOM) {
|
||||||
|
updateTransactionUsingGasFeeEstimates(priorityLevel);
|
||||||
|
}
|
||||||
|
// todo: open advance modal if priorityLevel is custom
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames('edit-gas-item', {
|
||||||
|
'edit-gas-item-selected': priorityLevel === estimateUsed,
|
||||||
|
'edit-gas-item-disabled':
|
||||||
|
priorityLevel === PRIORITY_LEVELS.DAPP_SUGGESTED &&
|
||||||
|
!dappSuggestedGasFees,
|
||||||
|
})}
|
||||||
|
role="button"
|
||||||
|
onClick={onOptionSelect}
|
||||||
|
>
|
||||||
|
<span className="edit-gas-item__name">
|
||||||
|
<span
|
||||||
|
className={`edit-gas-item__icon edit-gas-item__icon-${priorityLevel}`}
|
||||||
|
>
|
||||||
|
{PRIORITY_LEVEL_ICON_MAP[priorityLevel]}
|
||||||
|
</span>
|
||||||
|
<I18nValue
|
||||||
|
messageKey={
|
||||||
|
priorityLevel === PRIORITY_LEVELS.DAPP_SUGGESTED
|
||||||
|
? 'dappSuggestedShortLabel'
|
||||||
|
: priorityLevel
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`edit-gas-item__time-estimate edit-gas-item__time-estimate-${priorityLevel}`}
|
||||||
|
>
|
||||||
|
{minWaitTime
|
||||||
|
? minWaitTime && toHumanReadableTime(t, minWaitTime)
|
||||||
|
: '--'}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`edit-gas-item__fee-estimate edit-gas-item__fee-estimate-${priorityLevel}`}
|
||||||
|
>
|
||||||
|
{hexMaximumTransactionFee ? (
|
||||||
|
<UserPreferencedCurrencyDisplay
|
||||||
|
key="editGasSubTextFeeAmount"
|
||||||
|
type={PRIMARY}
|
||||||
|
value={hexMaximumTransactionFee}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
'--'
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span className="edit-gas-item__tooltip">
|
||||||
|
<InfoTooltip position="top" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
EditGasItem.propTypes = {
|
||||||
|
priorityLevel: PropTypes.string,
|
||||||
|
onClose: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditGasItem;
|
@ -0,0 +1,138 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { screen } from '@testing-library/react';
|
||||||
|
|
||||||
|
import { renderWithProvider } from '../../../../../test/lib/render-helpers';
|
||||||
|
import { ETH } from '../../../../helpers/constants/common';
|
||||||
|
import configureStore from '../../../../store/store';
|
||||||
|
import { GasFeeContextProvider } from '../../../../contexts/gasFee';
|
||||||
|
|
||||||
|
import EditGasItem from './edit-gas-item';
|
||||||
|
|
||||||
|
jest.mock('../../../../store/actions', () => ({
|
||||||
|
disconnectGasFeeEstimatePoller: jest.fn(),
|
||||||
|
getGasFeeEstimatesAndStartPolling: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(() => Promise.resolve()),
|
||||||
|
addPollingTokenToAppState: jest.fn(),
|
||||||
|
getGasFeeTimeEstimate: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(() => Promise.resolve('unknown')),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const MOCK_FEE_ESTIMATE = {
|
||||||
|
low: {
|
||||||
|
minWaitTimeEstimate: 360000,
|
||||||
|
maxWaitTimeEstimate: 300000,
|
||||||
|
suggestedMaxPriorityFeePerGas: '3',
|
||||||
|
suggestedMaxFeePerGas: '53',
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
minWaitTimeEstimate: 30000,
|
||||||
|
maxWaitTimeEstimate: 60000,
|
||||||
|
suggestedMaxPriorityFeePerGas: '7',
|
||||||
|
suggestedMaxFeePerGas: '70',
|
||||||
|
},
|
||||||
|
high: {
|
||||||
|
minWaitTimeEstimate: 15000,
|
||||||
|
maxWaitTimeEstimate: 15000,
|
||||||
|
suggestedMaxPriorityFeePerGas: '10',
|
||||||
|
suggestedMaxFeePerGas: '100',
|
||||||
|
},
|
||||||
|
estimatedBaseFee: '50',
|
||||||
|
};
|
||||||
|
|
||||||
|
const DAPP_SUGGESTED_ESTIMATE = {
|
||||||
|
maxFeePerGas: '0x59682f10',
|
||||||
|
maxPriorityFeePerGas: '0x59682f00',
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderComponent = (props, transactionProps, gasFeeContextProps) => {
|
||||||
|
const store = configureStore({
|
||||||
|
metamask: {
|
||||||
|
nativeCurrency: ETH,
|
||||||
|
provider: {},
|
||||||
|
cachedBalances: {},
|
||||||
|
accounts: {
|
||||||
|
'0xAddress': {
|
||||||
|
address: '0xAddress',
|
||||||
|
balance: '0x176e5b6f173ebe66',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selectedAddress: '0xAddress',
|
||||||
|
featureFlags: { advancedInlineGas: true },
|
||||||
|
gasFeeEstimates: MOCK_FEE_ESTIMATE,
|
||||||
|
advancedGasFee: {
|
||||||
|
maxBaseFee: '1.5',
|
||||||
|
priorityFee: '2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return renderWithProvider(
|
||||||
|
<GasFeeContextProvider
|
||||||
|
transaction={{ txParams: { gas: '0x5208' }, ...transactionProps }}
|
||||||
|
{...gasFeeContextProps}
|
||||||
|
>
|
||||||
|
<EditGasItem priorityLevel="low" {...props} />
|
||||||
|
</GasFeeContextProvider>,
|
||||||
|
store,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('EditGasItem', () => {
|
||||||
|
it('should renders low gas estimate option for priorityLevel low', () => {
|
||||||
|
renderComponent({ priorityLevel: 'low' });
|
||||||
|
expect(screen.queryByText('🐢')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Low')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('5 min')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTitle('0.001113 ETH')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should renders market gas estimate option for priorityLevel medium', () => {
|
||||||
|
renderComponent({ priorityLevel: 'medium' });
|
||||||
|
expect(screen.queryByText('🦊')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Market')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('5 min')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTitle('0.00147 ETH')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should renders aggressive gas estimate option for priorityLevel high', () => {
|
||||||
|
renderComponent({ priorityLevel: 'high' });
|
||||||
|
expect(screen.queryByText('🦍')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Aggressive')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('15 sec')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTitle('0.0021 ETH')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should highlight option is priorityLevel is currently selected', () => {
|
||||||
|
renderComponent({ priorityLevel: 'high' }, { userFeeLevel: 'high' });
|
||||||
|
expect(
|
||||||
|
document.getElementsByClassName('edit-gas-item-selected'),
|
||||||
|
).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should renders site gas estimate option for priorityLevel dappSuggested', () => {
|
||||||
|
renderComponent(
|
||||||
|
{ priorityLevel: 'dappSuggested' },
|
||||||
|
{ dappSuggestedGasFees: DAPP_SUGGESTED_ESTIMATE },
|
||||||
|
);
|
||||||
|
expect(screen.queryByText('🌐')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Site')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTitle('0.0000315 ETH')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disable site gas estimate option for is transaction does not have dappSuggestedGasFees', async () => {
|
||||||
|
renderComponent({ priorityLevel: 'dappSuggested' });
|
||||||
|
expect(
|
||||||
|
document.getElementsByClassName('edit-gas-item-disabled'),
|
||||||
|
).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should renders advance gas estimate option for priorityLevel custom', () => {
|
||||||
|
renderComponent({ priorityLevel: 'custom' });
|
||||||
|
expect(screen.queryByText('⚙')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Advanced')).toBeInTheDocument();
|
||||||
|
// below value of custom gas fee estimate is default obtained from state.metamask.advancedGasFee
|
||||||
|
expect(screen.queryByTitle('0.001575 ETH')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1 @@
|
|||||||
|
export { default } from './edit-gas-item';
|
@ -0,0 +1,68 @@
|
|||||||
|
.edit-gas-item {
|
||||||
|
border-radius: 24px;
|
||||||
|
color: $ui-4;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 12px 0;
|
||||||
|
padding: 4px 12px;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
&--selected {
|
||||||
|
background-color: $ui-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-disabled {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
color: $ui-black;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
|
||||||
|
&-custom {
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__time-estimate {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__fee-estimate {
|
||||||
|
display: inline-block;
|
||||||
|
width: 30%;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tooltip {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: right;
|
||||||
|
width: 10%;
|
||||||
|
|
||||||
|
.info-tooltip {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__time-estimate-low,
|
||||||
|
&__fee-estimate-high {
|
||||||
|
color: $secondary-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__time-estimate-medium,
|
||||||
|
&__time-estimate-high {
|
||||||
|
color: $success-3;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
|
||||||
|
import { GAS_ESTIMATE_TYPES } from '../../../../../shared/constants/gas';
|
||||||
|
import {
|
||||||
|
getGasEstimateType,
|
||||||
|
getIsGasEstimatesLoading,
|
||||||
|
} from '../../../../ducks/metamask/metamask';
|
||||||
|
import { getGasFeeTimeEstimate } from '../../../../store/actions';
|
||||||
|
|
||||||
|
export const useCustomTimeEstimate = ({
|
||||||
|
gasFeeEstimates,
|
||||||
|
maxFeePerGas,
|
||||||
|
maxPriorityFeePerGas,
|
||||||
|
}) => {
|
||||||
|
const gasEstimateType = useSelector(getGasEstimateType);
|
||||||
|
const isGasEstimatesLoading = useSelector(getIsGasEstimatesLoading);
|
||||||
|
|
||||||
|
const [customEstimatedTime, setCustomEstimatedTime] = useState(null);
|
||||||
|
|
||||||
|
const returnNoEstimates =
|
||||||
|
isGasEstimatesLoading ||
|
||||||
|
gasEstimateType !== GAS_ESTIMATE_TYPES.FEE_MARKET ||
|
||||||
|
!maxPriorityFeePerGas;
|
||||||
|
|
||||||
|
// If the user has chosen a value lower than the low gas fee estimate,
|
||||||
|
// We'll need to use the useEffect hook below to make a call to calculate
|
||||||
|
// the time to show
|
||||||
|
const isUnknownLow =
|
||||||
|
gasFeeEstimates?.low &&
|
||||||
|
Number(maxPriorityFeePerGas) <
|
||||||
|
Number(gasFeeEstimates.low.suggestedMaxPriorityFeePerGas);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
isGasEstimatesLoading ||
|
||||||
|
gasEstimateType !== GAS_ESTIMATE_TYPES.FEE_MARKET ||
|
||||||
|
!maxPriorityFeePerGas
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
if (isUnknownLow) {
|
||||||
|
// getGasFeeTimeEstimate requires parameters in string format
|
||||||
|
getGasFeeTimeEstimate(
|
||||||
|
new BigNumber(maxPriorityFeePerGas, 10).toString(10),
|
||||||
|
new BigNumber(maxFeePerGas, 10).toString(10),
|
||||||
|
).then((result) => {
|
||||||
|
setCustomEstimatedTime(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
gasEstimateType,
|
||||||
|
isUnknownLow,
|
||||||
|
isGasEstimatesLoading,
|
||||||
|
maxFeePerGas,
|
||||||
|
maxPriorityFeePerGas,
|
||||||
|
returnNoEstimates,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (returnNoEstimates) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { low = {}, medium = {}, high = {} } = gasFeeEstimates;
|
||||||
|
let waitTimeEstimate = '';
|
||||||
|
|
||||||
|
if (
|
||||||
|
isUnknownLow &&
|
||||||
|
customEstimatedTime &&
|
||||||
|
customEstimatedTime !== 'unknown' &&
|
||||||
|
customEstimatedTime?.upperTimeBound !== 'unknown'
|
||||||
|
) {
|
||||||
|
waitTimeEstimate = Number(customEstimatedTime?.upperTimeBound);
|
||||||
|
} else if (
|
||||||
|
Number(maxPriorityFeePerGas) >= Number(medium.suggestedMaxPriorityFeePerGas)
|
||||||
|
) {
|
||||||
|
waitTimeEstimate = high.minWaitTimeEstimate;
|
||||||
|
} else {
|
||||||
|
waitTimeEstimate = low.maxWaitTimeEstimate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { waitTimeEstimate };
|
||||||
|
};
|
1
ui/components/app/edit-gas-fee-popover/index.js
Normal file
1
ui/components/app/edit-gas-fee-popover/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './edit-gas-fee-popover';
|
40
ui/components/app/edit-gas-fee-popover/index.scss
Normal file
40
ui/components/app/edit-gas-fee-popover/index.scss
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
.edit-gas-fee-popover {
|
||||||
|
@media screen and (min-width: $break-large) {
|
||||||
|
max-height: 84vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__wrapper {
|
||||||
|
border-top: 1px solid $ui-grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
padding: 16px 12px;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
color: $ui-4;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 12px;
|
||||||
|
|
||||||
|
&-option {
|
||||||
|
display: inline-block;
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-time {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-max-fee {
|
||||||
|
display: inline-block;
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__separator {
|
||||||
|
border-top: 1px solid $ui-grey;
|
||||||
|
margin: 8px 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -24,6 +24,7 @@ import InfoTooltip from '../../ui/info-tooltip/info-tooltip';
|
|||||||
|
|
||||||
import { getGasFeeTimeEstimate } from '../../../store/actions';
|
import { getGasFeeTimeEstimate } from '../../../store/actions';
|
||||||
import { GAS_FORM_ERRORS } from '../../../helpers/constants/gas';
|
import { GAS_FORM_ERRORS } from '../../../helpers/constants/gas';
|
||||||
|
import { useGasFeeContext } from '../../../contexts/gasFee';
|
||||||
|
|
||||||
// Once we reach this second threshold, we switch to minutes as a unit
|
// Once we reach this second threshold, we switch to minutes as a unit
|
||||||
const SECOND_CUTOFF = 90;
|
const SECOND_CUTOFF = 90;
|
||||||
@ -49,6 +50,7 @@ export default function GasTiming({
|
|||||||
|
|
||||||
const [customEstimatedTime, setCustomEstimatedTime] = useState(null);
|
const [customEstimatedTime, setCustomEstimatedTime] = useState(null);
|
||||||
const t = useContext(I18nContext);
|
const t = useContext(I18nContext);
|
||||||
|
const { estimateUsed } = useGasFeeContext();
|
||||||
|
|
||||||
// If the user has chosen a value lower than the low gas fee estimate,
|
// If the user has chosen a value lower than the low gas fee estimate,
|
||||||
// We'll need to use the useEffect hook below to make a call to calculate
|
// We'll need to use the useEffect hook below to make a call to calculate
|
||||||
@ -94,12 +96,17 @@ export default function GasTiming({
|
|||||||
previousIsUnknownLow,
|
previousIsUnknownLow,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const unknownProcessingTimeText = (
|
let unknownProcessingTimeText;
|
||||||
<>
|
if (EIP_1559_V2) {
|
||||||
{t('editGasTooLow')}{' '}
|
unknownProcessingTimeText = t('editGasTooLow');
|
||||||
<InfoTooltip position="top" contentText={t('editGasTooLowTooltip')} />
|
} else {
|
||||||
</>
|
unknownProcessingTimeText = (
|
||||||
);
|
<>
|
||||||
|
{t('editGasTooLow')}{' '}
|
||||||
|
<InfoTooltip position="top" contentText={t('editGasTooLowTooltip')} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
gasWarnings?.maxPriorityFee === GAS_FORM_ERRORS.MAX_PRIORITY_FEE_TOO_LOW ||
|
gasWarnings?.maxPriorityFee === GAS_FORM_ERRORS.MAX_PRIORITY_FEE_TOO_LOW ||
|
||||||
@ -148,8 +155,9 @@ export default function GasTiming({
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
attitude = 'negative';
|
if (!EIP_1559_V2 || estimateUsed === 'low') {
|
||||||
|
attitude = 'negative';
|
||||||
|
}
|
||||||
// If the user has chosen a value less than our low estimate,
|
// If the user has chosen a value less than our low estimate,
|
||||||
// calculate a potential wait time
|
// calculate a potential wait time
|
||||||
if (isUnknownLow) {
|
if (isUnknownLow) {
|
||||||
@ -191,7 +199,8 @@ export default function GasTiming({
|
|||||||
<Typography
|
<Typography
|
||||||
variant={TYPOGRAPHY.H7}
|
variant={TYPOGRAPHY.H7}
|
||||||
className={classNames('gas-timing', {
|
className={classNames('gas-timing', {
|
||||||
[`gas-timing--${attitude}`]: attitude,
|
[`gas-timing--${attitude}`]: attitude && !EIP_1559_V2,
|
||||||
|
[`gas-timing--${attitude}-V2`]: attitude && EIP_1559_V2,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
|
@ -14,6 +14,11 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--negative-V2 {
|
||||||
|
color: $secondary-1;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
.info-tooltip {
|
.info-tooltip {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-inline-start: 4px;
|
margin-inline-start: 4px;
|
||||||
|
@ -84,9 +84,7 @@ export default class AccountDetailsModal extends Component {
|
|||||||
? this.context.t('blockExplorerView', [
|
? this.context.t('blockExplorerView', [
|
||||||
getURLHostName(rpcPrefs.blockExplorerUrl),
|
getURLHostName(rpcPrefs.blockExplorerUrl),
|
||||||
])
|
])
|
||||||
: this.context.t('viewOnEtherscan', [
|
: this.context.t('etherscanViewOn')}
|
||||||
this.context.t('blockExplorerAccountAction'),
|
|
||||||
])}
|
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{exportPrivateKeyFeatureEnabled ? (
|
{exportPrivateKeyFeatureEnabled ? (
|
||||||
|
@ -8,13 +8,13 @@
|
|||||||
& &__button {
|
& &__button {
|
||||||
margin-top: 17px;
|
margin-top: 17px;
|
||||||
padding: 10px 22px;
|
padding: 10px 22px;
|
||||||
width: 286px;
|
width: 284px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__divider {
|
&__divider {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
margin: 19px 0 8px 0;
|
margin: 16px 0 8px 0;
|
||||||
background-color: $alto;
|
background-color: $alto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ const accountModalStyle = {
|
|||||||
margin: '0 auto',
|
margin: '0 auto',
|
||||||
},
|
},
|
||||||
laptopModalStyle: {
|
laptopModalStyle: {
|
||||||
width: '360px',
|
width: '335px',
|
||||||
// top: 'calc(33% + 45px)',
|
// top: 'calc(33% + 45px)',
|
||||||
boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px',
|
boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
|
1
ui/components/app/new-collectibles-notice/index.js
Normal file
1
ui/components/app/new-collectibles-notice/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './new-collectibles-notice.component';
|
@ -0,0 +1,56 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Box from '../../ui/box';
|
||||||
|
import Dialog from '../../ui/dialog';
|
||||||
|
import Typography from '../../ui/typography/typography';
|
||||||
|
import {
|
||||||
|
COLORS,
|
||||||
|
TYPOGRAPHY,
|
||||||
|
TEXT_ALIGN,
|
||||||
|
FONT_WEIGHT,
|
||||||
|
DISPLAY,
|
||||||
|
} from '../../../helpers/constants/design-system';
|
||||||
|
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||||
|
|
||||||
|
export default function NewCollectiblesNotice() {
|
||||||
|
const t = useI18nContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box marginBottom={8}>
|
||||||
|
<Dialog type="message">
|
||||||
|
<Box display={DISPLAY.FLEX}>
|
||||||
|
<Box paddingTop={2}>
|
||||||
|
<i style={{ fontSize: '1rem' }} className="fa fa-info-circle" />
|
||||||
|
</Box>
|
||||||
|
<Box paddingLeft={4}>
|
||||||
|
<Typography
|
||||||
|
color={COLORS.BLACK}
|
||||||
|
align={TEXT_ALIGN.LEFT}
|
||||||
|
variant={TYPOGRAPHY.Paragraph}
|
||||||
|
fontWeight={FONT_WEIGHT.BOLD}
|
||||||
|
>
|
||||||
|
{t('newNFTsDetected')}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
color={COLORS.BLACK}
|
||||||
|
align={TEXT_ALIGN.LEFT}
|
||||||
|
variant={TYPOGRAPHY.Paragraph}
|
||||||
|
boxProps={{ marginBottom: 4 }}
|
||||||
|
>
|
||||||
|
{t('newNFTsDetectedInfo')}
|
||||||
|
</Typography>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
console.log('show preference popover');
|
||||||
|
}}
|
||||||
|
style={{ fontSize: '.9rem' }}
|
||||||
|
>
|
||||||
|
{t('selectNFTPrivacyPreference')}
|
||||||
|
</a>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Dialog>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
@ -5,11 +5,7 @@ import classnames from 'classnames';
|
|||||||
import { ObjectInspector } from 'react-inspector';
|
import { ObjectInspector } from 'react-inspector';
|
||||||
import LedgerInstructionField from '../ledger-instruction-field';
|
import LedgerInstructionField from '../ledger-instruction-field';
|
||||||
|
|
||||||
import {
|
import { MESSAGE_TYPE } from '../../../../shared/constants/app';
|
||||||
ENVIRONMENT_TYPE_NOTIFICATION,
|
|
||||||
MESSAGE_TYPE,
|
|
||||||
} from '../../../../shared/constants/app';
|
|
||||||
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
|
|
||||||
import Identicon from '../../ui/identicon';
|
import Identicon from '../../ui/identicon';
|
||||||
import AccountListItem from '../account-list-item';
|
import AccountListItem from '../account-list-item';
|
||||||
import { conversionUtil } from '../../../../shared/modules/conversion.utils';
|
import { conversionUtil } from '../../../../shared/modules/conversion.utils';
|
||||||
@ -39,42 +35,13 @@ export default class SignatureRequestOriginal extends Component {
|
|||||||
domainMetadata: PropTypes.object,
|
domainMetadata: PropTypes.object,
|
||||||
hardwareWalletRequiresConnection: PropTypes.bool,
|
hardwareWalletRequiresConnection: PropTypes.bool,
|
||||||
isLedgerWallet: PropTypes.bool,
|
isLedgerWallet: PropTypes.bool,
|
||||||
|
nativeCurrency: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
fromAccount: this.props.fromAccount,
|
fromAccount: this.props.fromAccount,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount = () => {
|
|
||||||
if (getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION) {
|
|
||||||
window.addEventListener('beforeunload', this._beforeUnload);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
componentWillUnmount = () => {
|
|
||||||
this._removeBeforeUnload();
|
|
||||||
};
|
|
||||||
|
|
||||||
_beforeUnload = (event) => {
|
|
||||||
const { clearConfirmTransaction, cancel } = this.props;
|
|
||||||
const { metricsEvent } = this.context;
|
|
||||||
metricsEvent({
|
|
||||||
eventOpts: {
|
|
||||||
category: 'Transactions',
|
|
||||||
action: 'Sign Request',
|
|
||||||
name: 'Cancel Sig Request Via Notification Close',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
clearConfirmTransaction();
|
|
||||||
cancel(event);
|
|
||||||
};
|
|
||||||
|
|
||||||
_removeBeforeUnload = () => {
|
|
||||||
if (getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION) {
|
|
||||||
window.removeEventListener('beforeunload', this._beforeUnload);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
renderHeader = () => {
|
renderHeader = () => {
|
||||||
return (
|
return (
|
||||||
<div className="request-signature__header">
|
<div className="request-signature__header">
|
||||||
@ -108,12 +75,12 @@ export default class SignatureRequestOriginal extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderBalance = () => {
|
renderBalance = () => {
|
||||||
const { conversionRate } = this.props;
|
const { conversionRate, nativeCurrency } = this.props;
|
||||||
const {
|
const {
|
||||||
fromAccount: { balance },
|
fromAccount: { balance },
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const balanceInEther = conversionUtil(balance, {
|
const balanceInBaseAsset = conversionUtil(balance, {
|
||||||
fromNumericBase: 'hex',
|
fromNumericBase: 'hex',
|
||||||
toNumericBase: 'dec',
|
toNumericBase: 'dec',
|
||||||
fromDenomination: 'WEI',
|
fromDenomination: 'WEI',
|
||||||
@ -127,7 +94,7 @@ export default class SignatureRequestOriginal extends Component {
|
|||||||
{`${this.context.t('balance')}:`}
|
{`${this.context.t('balance')}:`}
|
||||||
</div>
|
</div>
|
||||||
<div className="request-signature__balance-value">
|
<div className="request-signature__balance-value">
|
||||||
{`${balanceInEther} ETH`}
|
{`${balanceInBaseAsset} ${nativeCurrency}`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -300,7 +267,6 @@ export default class SignatureRequestOriginal extends Component {
|
|||||||
large
|
large
|
||||||
className="request-signature__footer__cancel-button"
|
className="request-signature__footer__cancel-button"
|
||||||
onClick={async (event) => {
|
onClick={async (event) => {
|
||||||
this._removeBeforeUnload();
|
|
||||||
await cancel(event);
|
await cancel(event);
|
||||||
metricsEvent({
|
metricsEvent({
|
||||||
eventOpts: {
|
eventOpts: {
|
||||||
@ -325,7 +291,6 @@ export default class SignatureRequestOriginal extends Component {
|
|||||||
className="request-signature__footer__sign-button"
|
className="request-signature__footer__sign-button"
|
||||||
disabled={hardwareWalletRequiresConnection}
|
disabled={hardwareWalletRequiresConnection}
|
||||||
onClick={async (event) => {
|
onClick={async (event) => {
|
||||||
this._removeBeforeUnload();
|
|
||||||
await sign(event);
|
await sign(event);
|
||||||
metricsEvent({
|
metricsEvent({
|
||||||
eventOpts: {
|
eventOpts: {
|
||||||
|
@ -13,7 +13,10 @@ import {
|
|||||||
import { getAccountByAddress } from '../../../helpers/utils/util';
|
import { getAccountByAddress } from '../../../helpers/utils/util';
|
||||||
import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck';
|
import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck';
|
||||||
import { getMostRecentOverviewPage } from '../../../ducks/history/history';
|
import { getMostRecentOverviewPage } from '../../../ducks/history/history';
|
||||||
import { isAddressLedger } from '../../../ducks/metamask/metamask';
|
import {
|
||||||
|
isAddressLedger,
|
||||||
|
getNativeCurrency,
|
||||||
|
} from '../../../ducks/metamask/metamask';
|
||||||
import SignatureRequestOriginal from './signature-request-original.component';
|
import SignatureRequestOriginal from './signature-request-original.component';
|
||||||
|
|
||||||
function mapStateToProps(state, ownProps) {
|
function mapStateToProps(state, ownProps) {
|
||||||
@ -34,6 +37,7 @@ function mapStateToProps(state, ownProps) {
|
|||||||
mostRecentOverviewPage: getMostRecentOverviewPage(state),
|
mostRecentOverviewPage: getMostRecentOverviewPage(state),
|
||||||
hardwareWalletRequiresConnection,
|
hardwareWalletRequiresConnection,
|
||||||
isLedgerWallet,
|
isLedgerWallet,
|
||||||
|
nativeCurrency: getNativeCurrency(state),
|
||||||
// not passed to component
|
// not passed to component
|
||||||
allAccounts: accountsWithSendEtherInfoSelector(state),
|
allAccounts: accountsWithSendEtherInfoSelector(state),
|
||||||
domainMetadata: getDomainMetadata(state),
|
domainMetadata: getDomainMetadata(state),
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
|
|
||||||
import Identicon from '../../ui/identicon';
|
import Identicon from '../../ui/identicon';
|
||||||
import LedgerInstructionField from '../ledger-instruction-field';
|
import LedgerInstructionField from '../ledger-instruction-field';
|
||||||
import Header from './signature-request-header';
|
import Header from './signature-request-header';
|
||||||
import Footer from './signature-request-footer';
|
import Footer from './signature-request-footer';
|
||||||
import Message from './signature-request-message';
|
import Message from './signature-request-message';
|
||||||
import { ENVIRONMENT_TYPE_NOTIFICATION } from './signature-request.constants';
|
|
||||||
|
|
||||||
export default class SignatureRequest extends PureComponent {
|
export default class SignatureRequest extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -17,7 +15,6 @@ export default class SignatureRequest extends PureComponent {
|
|||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
isLedgerWallet: PropTypes.bool,
|
isLedgerWallet: PropTypes.bool,
|
||||||
clearConfirmTransaction: PropTypes.func.isRequired,
|
|
||||||
cancel: PropTypes.func.isRequired,
|
cancel: PropTypes.func.isRequired,
|
||||||
sign: PropTypes.func.isRequired,
|
sign: PropTypes.func.isRequired,
|
||||||
hardwareWalletRequiresConnection: PropTypes.func.isRequired,
|
hardwareWalletRequiresConnection: PropTypes.func.isRequired,
|
||||||
@ -28,33 +25,6 @@ export default class SignatureRequest extends PureComponent {
|
|||||||
metricsEvent: PropTypes.func,
|
metricsEvent: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
if (getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION) {
|
|
||||||
window.addEventListener('beforeunload', this._beforeUnload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_beforeUnload = (event) => {
|
|
||||||
const {
|
|
||||||
clearConfirmTransaction,
|
|
||||||
cancel,
|
|
||||||
txData: { type },
|
|
||||||
} = this.props;
|
|
||||||
const { metricsEvent } = this.context;
|
|
||||||
metricsEvent({
|
|
||||||
eventOpts: {
|
|
||||||
category: 'Transactions',
|
|
||||||
action: 'Sign Request',
|
|
||||||
name: 'Cancel Sig Request Via Notification Close',
|
|
||||||
},
|
|
||||||
customVariables: {
|
|
||||||
type,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
clearConfirmTransaction();
|
|
||||||
cancel(event);
|
|
||||||
};
|
|
||||||
|
|
||||||
formatWallet(wallet) {
|
formatWallet(wallet) {
|
||||||
return `${wallet.slice(0, 8)}...${wallet.slice(
|
return `${wallet.slice(0, 8)}...${wallet.slice(
|
||||||
wallet.length - 8,
|
wallet.length - 8,
|
||||||
@ -79,7 +49,6 @@ export default class SignatureRequest extends PureComponent {
|
|||||||
const { metricsEvent } = this.context;
|
const { metricsEvent } = this.context;
|
||||||
|
|
||||||
const onSign = (event) => {
|
const onSign = (event) => {
|
||||||
window.removeEventListener('beforeunload', this._beforeUnload);
|
|
||||||
sign(event);
|
sign(event);
|
||||||
metricsEvent({
|
metricsEvent({
|
||||||
eventOpts: {
|
eventOpts: {
|
||||||
@ -95,7 +64,6 @@ export default class SignatureRequest extends PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onCancel = (event) => {
|
const onCancel = (event) => {
|
||||||
window.removeEventListener('beforeunload', this._beforeUnload);
|
|
||||||
cancel(event);
|
cancel(event);
|
||||||
metricsEvent({
|
metricsEvent({
|
||||||
eventOpts: {
|
eventOpts: {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck';
|
|
||||||
import {
|
import {
|
||||||
accountsWithSendEtherInfoSelector,
|
accountsWithSendEtherInfoSelector,
|
||||||
doesAddressRequireLedgerHidConnection,
|
doesAddressRequireLedgerHidConnection,
|
||||||
@ -28,12 +27,6 @@ function mapStateToProps(state, ownProps) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
|
||||||
return {
|
|
||||||
clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeProps(stateProps, dispatchProps, ownProps) {
|
function mergeProps(stateProps, dispatchProps, ownProps) {
|
||||||
const {
|
const {
|
||||||
allAccounts,
|
allAccounts,
|
||||||
@ -83,8 +76,4 @@ function mergeProps(stateProps, dispatchProps, ownProps) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(mapStateToProps, null, mergeProps)(SignatureRequest);
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps,
|
|
||||||
mergeProps,
|
|
||||||
)(SignatureRequest);
|
|
||||||
|
@ -15,4 +15,55 @@
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-edit-V2 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 20px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
@include H7;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
color: $primary-1;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
padding-inline-end: 0;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: $primary-1;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-tooltip {
|
||||||
|
align-self: center;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tooltip {
|
||||||
|
p {
|
||||||
|
color: $Grey-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
b {
|
||||||
|
color: $neutral-black;
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 60%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,73 @@
|
|||||||
import React, { useContext } from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { I18nContext } from '../../../contexts/i18n';
|
import { useGasFeeContext } from '../../../contexts/gasFee';
|
||||||
|
import InfoTooltip from '../../ui/info-tooltip/info-tooltip';
|
||||||
|
import Typography from '../../ui/typography/typography';
|
||||||
|
|
||||||
import TransactionDetailItem from '../transaction-detail-item/transaction-detail-item.component';
|
import TransactionDetailItem from '../transaction-detail-item/transaction-detail-item.component';
|
||||||
|
import { COLORS } from '../../../helpers/constants/design-system';
|
||||||
|
import { PRIORITY_LEVEL_ICON_MAP } from '../../../helpers/constants/gas';
|
||||||
|
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||||
|
|
||||||
export default function TransactionDetail({ rows = [], onEdit }) {
|
export default function TransactionDetail({ rows = [], onEdit }) {
|
||||||
const t = useContext(I18nContext);
|
// eslint-disable-next-line prefer-destructuring
|
||||||
|
const EIP_1559_V2 = process.env.EIP_1559_V2;
|
||||||
|
|
||||||
|
const t = useI18nContext();
|
||||||
|
const {
|
||||||
|
gasLimit,
|
||||||
|
estimateUsed,
|
||||||
|
maxFeePerGas,
|
||||||
|
maxPriorityFeePerGas,
|
||||||
|
transaction,
|
||||||
|
} = useGasFeeContext();
|
||||||
|
|
||||||
|
if (EIP_1559_V2 && estimateUsed) {
|
||||||
|
return (
|
||||||
|
<div className="transaction-detail">
|
||||||
|
<div className="transaction-detail-edit-V2">
|
||||||
|
<button onClick={onEdit}>
|
||||||
|
<span className="transaction-detail-edit-V2__icon">
|
||||||
|
{`${PRIORITY_LEVEL_ICON_MAP[estimateUsed]} `}
|
||||||
|
</span>
|
||||||
|
<span className="transaction-detail-edit-V2__label">
|
||||||
|
{t(estimateUsed)}
|
||||||
|
</span>
|
||||||
|
<i className="fas fa-chevron-right asset-list-item__chevron-right" />
|
||||||
|
</button>
|
||||||
|
{estimateUsed === 'custom' && onEdit && (
|
||||||
|
<button onClick={onEdit}>{t('edit')}</button>
|
||||||
|
)}
|
||||||
|
{estimateUsed === 'dappSuggested' && (
|
||||||
|
<InfoTooltip
|
||||||
|
contentText={
|
||||||
|
<div className="transaction-detail-edit-V2__tooltip">
|
||||||
|
<Typography fontSize="12px" color={COLORS.GREY}>
|
||||||
|
{t('dappSuggestedTooltip', [transaction.origin])}
|
||||||
|
</Typography>
|
||||||
|
<Typography fontSize="12px">
|
||||||
|
<b>{t('maxBaseFee')}</b>
|
||||||
|
{maxFeePerGas}
|
||||||
|
</Typography>
|
||||||
|
<Typography fontSize="12px">
|
||||||
|
<b>{t('maxPriorityFee')}</b>
|
||||||
|
{maxPriorityFeePerGas}
|
||||||
|
</Typography>
|
||||||
|
<Typography fontSize="12px">
|
||||||
|
<b>{t('gasLimit')}</b>
|
||||||
|
{gasLimit}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
position="top"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="transaction-detail-rows">{rows}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="transaction-detail">
|
<div className="transaction-detail">
|
||||||
|
@ -0,0 +1,94 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { screen } from '@testing-library/react';
|
||||||
|
|
||||||
|
import { ETH } from '../../../helpers/constants/common';
|
||||||
|
import { GasFeeContextProvider } from '../../../contexts/gasFee';
|
||||||
|
import { renderWithProvider } from '../../../../test/jest';
|
||||||
|
import configureStore from '../../../store/store';
|
||||||
|
|
||||||
|
import TransactionDetail from './transaction-detail.component';
|
||||||
|
|
||||||
|
jest.mock('../../../store/actions', () => ({
|
||||||
|
disconnectGasFeeEstimatePoller: jest.fn(),
|
||||||
|
getGasFeeEstimatesAndStartPolling: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(() => Promise.resolve()),
|
||||||
|
addPollingTokenToAppState: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const render = (props) => {
|
||||||
|
const store = configureStore({
|
||||||
|
metamask: {
|
||||||
|
nativeCurrency: ETH,
|
||||||
|
preferences: {
|
||||||
|
useNativeCurrencyAsPrimaryCurrency: true,
|
||||||
|
},
|
||||||
|
provider: {},
|
||||||
|
cachedBalances: {},
|
||||||
|
accounts: {
|
||||||
|
'0xAddress': {
|
||||||
|
address: '0xAddress',
|
||||||
|
balance: '0x176e5b6f173ebe66',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selectedAddress: '0xAddress',
|
||||||
|
featureFlags: { advancedInlineGas: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return renderWithProvider(
|
||||||
|
<GasFeeContextProvider {...props}>
|
||||||
|
<TransactionDetail
|
||||||
|
onEdit={() => {
|
||||||
|
console.log('on edit');
|
||||||
|
}}
|
||||||
|
rows={[]}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</GasFeeContextProvider>,
|
||||||
|
store,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('TransactionDetail', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env.EIP_1559_V2 = true;
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
process.env.EIP_1559_V2 = false;
|
||||||
|
});
|
||||||
|
it('should render edit link with text low if low gas estimates are selected', () => {
|
||||||
|
render({ transaction: { userFeeLevel: 'low' } });
|
||||||
|
expect(screen.queryByText('🐢')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Low')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('should render edit link with text markey if medium gas estimates are selected', () => {
|
||||||
|
render({ transaction: { userFeeLevel: 'medium' } });
|
||||||
|
expect(screen.queryByText('🦊')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Market')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('should render edit link with text agressive if high gas estimates are selected', () => {
|
||||||
|
render({ transaction: { userFeeLevel: 'high' } });
|
||||||
|
expect(screen.queryByText('🦍')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Aggressive')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('should render edit link with text Site suggested if site suggested estimated are used', () => {
|
||||||
|
render({
|
||||||
|
transaction: {
|
||||||
|
dappSuggestedGasFees: { maxFeePerGas: 1, maxPriorityFeePerGas: 1 },
|
||||||
|
txParams: { maxFeePerGas: 1, maxPriorityFeePerGas: 1 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(screen.queryByText('🌐')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Site suggested')).toBeInTheDocument();
|
||||||
|
expect(document.getElementsByClassName('info-tooltip')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
it('should render edit link with text advance if custom gas estimates are used', () => {
|
||||||
|
render({
|
||||||
|
defaultEstimateToUse: 'custom',
|
||||||
|
});
|
||||||
|
expect(screen.queryByText('⚙')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Advanced')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Edit')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -26,6 +26,7 @@ export default function ActionableMessage({
|
|||||||
type = 'default',
|
type = 'default',
|
||||||
useIcon = false,
|
useIcon = false,
|
||||||
iconFillColor = '',
|
iconFillColor = '',
|
||||||
|
roundedButtons,
|
||||||
}) {
|
}) {
|
||||||
const actionableMessageClassName = classnames(
|
const actionableMessageClassName = classnames(
|
||||||
'actionable-message',
|
'actionable-message',
|
||||||
@ -35,6 +36,9 @@ export default function ActionableMessage({
|
|||||||
{ 'actionable-message--with-icon': useIcon },
|
{ 'actionable-message--with-icon': useIcon },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onlyOneAction =
|
||||||
|
(primaryAction && !secondaryAction) || (secondaryAction && !primaryAction);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={actionableMessageClassName}>
|
<div className={actionableMessageClassName}>
|
||||||
{useIcon ? <InfoTooltipIcon fillColor={iconFillColor} /> : null}
|
{useIcon ? <InfoTooltipIcon fillColor={iconFillColor} /> : null}
|
||||||
@ -47,12 +51,19 @@ export default function ActionableMessage({
|
|||||||
)}
|
)}
|
||||||
<div className="actionable-message__message">{message}</div>
|
<div className="actionable-message__message">{message}</div>
|
||||||
{(primaryAction || secondaryAction) && (
|
{(primaryAction || secondaryAction) && (
|
||||||
<div className="actionable-message__actions">
|
<div
|
||||||
|
className={classnames('actionable-message__actions', {
|
||||||
|
'actionable-message__actions--single': onlyOneAction,
|
||||||
|
})}
|
||||||
|
>
|
||||||
{primaryAction && (
|
{primaryAction && (
|
||||||
<button
|
<button
|
||||||
className={classnames(
|
className={classnames(
|
||||||
'actionable-message__action',
|
'actionable-message__action',
|
||||||
'actionable-message__action--primary',
|
'actionable-message__action--primary',
|
||||||
|
{
|
||||||
|
'actionable-message__action--rounded': roundedButtons,
|
||||||
|
},
|
||||||
)}
|
)}
|
||||||
onClick={primaryAction.onClick}
|
onClick={primaryAction.onClick}
|
||||||
>
|
>
|
||||||
@ -64,6 +75,9 @@ export default function ActionableMessage({
|
|||||||
className={classnames(
|
className={classnames(
|
||||||
'actionable-message__action',
|
'actionable-message__action',
|
||||||
'actionable-message__action--secondary',
|
'actionable-message__action--secondary',
|
||||||
|
{
|
||||||
|
'actionable-message__action--rounded': roundedButtons,
|
||||||
|
},
|
||||||
)}
|
)}
|
||||||
onClick={secondaryAction.onClick}
|
onClick={secondaryAction.onClick}
|
||||||
>
|
>
|
||||||
@ -92,4 +106,5 @@ ActionableMessage.propTypes = {
|
|||||||
infoTooltipText: PropTypes.string,
|
infoTooltipText: PropTypes.string,
|
||||||
useIcon: PropTypes.bool,
|
useIcon: PropTypes.bool,
|
||||||
iconFillColor: PropTypes.string,
|
iconFillColor: PropTypes.string,
|
||||||
|
roundedButtons: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
@ -38,14 +38,22 @@
|
|||||||
&__actions {
|
&__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
justify-content: space-evenly;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
color: $Blue-600;
|
color: $Blue-600;
|
||||||
|
|
||||||
|
&--single {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__action {
|
&__action {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
|
&--rounded {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__info-tooltip-wrapper {
|
&__info-tooltip-wrapper {
|
||||||
|
42
ui/components/ui/card/README.mdx
Normal file
42
ui/components/ui/card/README.mdx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { Story, Canvas, ArgsTable } from '@storybook/addon-docs';
|
||||||
|
|
||||||
|
import Card from '.';
|
||||||
|
|
||||||
|
# Card
|
||||||
|
|
||||||
|
Cards are used to group related content or actions together.
|
||||||
|
|
||||||
|
<Canvas>
|
||||||
|
<Story id="ui-components-ui-card-card-stories-js--default-story" />
|
||||||
|
</Canvas>
|
||||||
|
|
||||||
|
## Component API
|
||||||
|
|
||||||
|
The `Card` component extends the `Box` component. See the `Box` component for an extended list of props.
|
||||||
|
|
||||||
|
<ArgsTable of={Card} />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
The following describes the props and example usage for this component.
|
||||||
|
|
||||||
|
### Padding, Border and Background Color
|
||||||
|
|
||||||
|
The Card component has a set of default props that should meet most card use cases. There is a strong recommendation to not overwrite these to ensure our cards stay consistent across the app.
|
||||||
|
|
||||||
|
That being said all props can be overwritten if necessary.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { COLORS } from '../../../helpers/constants/design-system';
|
||||||
|
|
||||||
|
// To remove the border
|
||||||
|
<Card border={false} />
|
||||||
|
// All border related props of the Box component will work
|
||||||
|
|
||||||
|
// To remove or change padding
|
||||||
|
<Card padding={0} />
|
||||||
|
// All padding related props of the Box component will work
|
||||||
|
|
||||||
|
// To change the background color
|
||||||
|
<Card backgroundColor={COLORS.UI4} />
|
||||||
|
```
|
@ -1,23 +0,0 @@
|
|||||||
import React, { PureComponent } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
|
|
||||||
export default class Card extends PureComponent {
|
|
||||||
static propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
overrideClassName: PropTypes.bool,
|
|
||||||
title: PropTypes.string,
|
|
||||||
children: PropTypes.node,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { className, overrideClassName, title } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classnames({ card: !overrideClassName }, className)}>
|
|
||||||
<div className="card__title">{title}</div>
|
|
||||||
{this.props.children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
import Card from './card.component';
|
|
||||||
|
|
||||||
describe('Card Component', () => {
|
|
||||||
it('should render a card with a title and child element', () => {
|
|
||||||
const wrapper = shallow(
|
|
||||||
<Card title="Test" className="card-test-class">
|
|
||||||
<div className="child-test-class">Child</div>
|
|
||||||
</Card>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(wrapper.hasClass('card-test-class')).toStrictEqual(true);
|
|
||||||
const title = wrapper.find('.card__title');
|
|
||||||
expect(title).toHaveLength(1);
|
|
||||||
expect(title.text()).toStrictEqual('Test');
|
|
||||||
const child = wrapper.find('.child-test-class');
|
|
||||||
expect(child).toHaveLength(1);
|
|
||||||
expect(child.text()).toStrictEqual('Child');
|
|
||||||
});
|
|
||||||
});
|
|
60
ui/components/ui/card/card.js
Normal file
60
ui/components/ui/card/card.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import Box from '../box';
|
||||||
|
import {
|
||||||
|
BORDER_STYLE,
|
||||||
|
COLORS,
|
||||||
|
SIZES,
|
||||||
|
} from '../../../helpers/constants/design-system';
|
||||||
|
|
||||||
|
const Card = ({
|
||||||
|
border = true,
|
||||||
|
padding = 4,
|
||||||
|
backgroundColor = COLORS.WHITE,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const defaultBorderProps = {
|
||||||
|
borderColor: border && COLORS.UI2,
|
||||||
|
borderRadius: border && SIZES.MD,
|
||||||
|
borderStyle: border && BORDER_STYLE.SOLID,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
{...{
|
||||||
|
padding,
|
||||||
|
backgroundColor,
|
||||||
|
...defaultBorderProps,
|
||||||
|
...props,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Card.propTypes = {
|
||||||
|
/**
|
||||||
|
* Whether the Card has a border or not.
|
||||||
|
* Defaults to true
|
||||||
|
*/
|
||||||
|
border: PropTypes.bool,
|
||||||
|
/**
|
||||||
|
* Padding of the Card component accepts number or an array of 2 numbers.
|
||||||
|
* Defaults to 4 (16px)
|
||||||
|
*/
|
||||||
|
padding: Box.propTypes.padding,
|
||||||
|
/**
|
||||||
|
* The background color of the card
|
||||||
|
* Defaults to COLORS.WHITE
|
||||||
|
*/
|
||||||
|
backgroundColor: Box.propTypes.backgroundColor,
|
||||||
|
/**
|
||||||
|
* The Card component accepts all Box component props
|
||||||
|
*/
|
||||||
|
...Box.propTypes,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Card;
|
169
ui/components/ui/card/card.stories.js
Normal file
169
ui/components/ui/card/card.stories.js
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
ALIGN_ITEMS,
|
||||||
|
BLOCK_SIZES,
|
||||||
|
BORDER_STYLE,
|
||||||
|
COLORS,
|
||||||
|
DISPLAY,
|
||||||
|
JUSTIFY_CONTENT,
|
||||||
|
TEXT_ALIGN,
|
||||||
|
} from '../../../helpers/constants/design-system';
|
||||||
|
|
||||||
|
import README from './README.mdx';
|
||||||
|
import Card from '.';
|
||||||
|
|
||||||
|
const sizeOptions = [undefined, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'UI/Card',
|
||||||
|
id: __filename,
|
||||||
|
component: Card,
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
page: README,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
children: { control: 'text' },
|
||||||
|
border: {
|
||||||
|
control: 'boolean',
|
||||||
|
},
|
||||||
|
borderStyle: {
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
},
|
||||||
|
options: Object.values(BORDER_STYLE),
|
||||||
|
},
|
||||||
|
borderWidth: {
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
},
|
||||||
|
options: [...sizeOptions],
|
||||||
|
},
|
||||||
|
borderColor: {
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
},
|
||||||
|
options: Object.values(COLORS),
|
||||||
|
},
|
||||||
|
backgroundColor: {
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
},
|
||||||
|
options: Object.values(COLORS),
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
},
|
||||||
|
options: Object.values(BLOCK_SIZES),
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
},
|
||||||
|
options: Object.values(BLOCK_SIZES),
|
||||||
|
},
|
||||||
|
textAlign: {
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
},
|
||||||
|
options: Object.values(TEXT_ALIGN),
|
||||||
|
},
|
||||||
|
margin: {
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
},
|
||||||
|
options: [...sizeOptions],
|
||||||
|
},
|
||||||
|
marginTop: {
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
},
|
||||||
|
options: [...sizeOptions],
|
||||||
|
},
|
||||||
|
marginRight: {
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
},
|
||||||
|
options: [...sizeOptions],
|
||||||
|
},
|
||||||
|
marginBottom: {
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
},
|
||||||
|
options: [...sizeOptions],
|
||||||
|
},
|
||||||
|
marginLeft: {
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
},
|
||||||
|
options: [...sizeOptions],
|
||||||
|
},
|
||||||
|
padding: {
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
},
|
||||||
|
options: [...sizeOptions],
|
||||||
|
},
|
||||||
|
paddingTop: {
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
},
|
||||||
|
options: [...sizeOptions],
|
||||||
|
},
|
||||||
|
paddingRight: {
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
},
|
||||||
|
options: [...sizeOptions],
|
||||||
|
},
|
||||||
|
paddingBottom: {
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
},
|
||||||
|
options: [...sizeOptions],
|
||||||
|
},
|
||||||
|
paddingLeft: {
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
},
|
||||||
|
options: [...sizeOptions],
|
||||||
|
},
|
||||||
|
display: {
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
},
|
||||||
|
options: Object.values(DISPLAY),
|
||||||
|
},
|
||||||
|
justifyContent: {
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
},
|
||||||
|
options: Object.values(JUSTIFY_CONTENT),
|
||||||
|
},
|
||||||
|
alignItems: {
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
},
|
||||||
|
options: Object.values(ALIGN_ITEMS),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
children: 'Card children',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DefaultStory = (args) => <Card {...args}>{args.children}</Card>;
|
||||||
|
|
||||||
|
DefaultStory.storyName = 'Default';
|
||||||
|
|
||||||
|
DefaultStory.args = {
|
||||||
|
padding: 4,
|
||||||
|
border: true,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: COLORS.UI2,
|
||||||
|
borderStyle: BORDER_STYLE.SOLID,
|
||||||
|
backgroundColor: COLORS.WHITE,
|
||||||
|
display: DISPLAY.BLOCK,
|
||||||
|
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user