mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 18:00:18 +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
|
||||
shellcheck:
|
||||
docker:
|
||||
- image: koalaman/shellcheck-alpine@sha256:35882cba254810c7de458528011e935ba2c4f3ebcb224275dfa7ebfa930ef294
|
||||
- image: koalaman/shellcheck-alpine@sha256:dfaf08fab58c158549d3be64fb101c626abc5f16f341b569092577ae207db199
|
||||
|
||||
workflows:
|
||||
test_and_release:
|
||||
@ -25,7 +25,9 @@ workflows:
|
||||
only:
|
||||
- /^Version-v(\d+)[.](\d+)[.](\d+)/
|
||||
- prep-deps
|
||||
- test-deps-audit
|
||||
- test-deps-audit:
|
||||
requires:
|
||||
- prep-deps
|
||||
- test-deps-depcheck:
|
||||
requires:
|
||||
- prep-deps
|
||||
|
@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
set -u
|
||||
set -x
|
||||
set -o pipefail
|
||||
|
||||
# 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/**
|
||||
lavamoat/*/policy.json
|
||||
lavamoat/**/policy.json
|
||||
dist/**
|
||||
builds/**
|
||||
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`
|
||||
* 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.
|
||||
* The LavaMoat auto-generated policy in `lavamoat/node/policy.json`
|
||||
* Run `yarn lavamoat:auto` to re-generate this 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.
|
||||
* 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.
|
||||
* There are two sets of LavaMoat policy files:
|
||||
* 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
|
||||
|
||||
|
@ -43,9 +43,15 @@
|
||||
"activityLog": {
|
||||
"message": "activity log"
|
||||
},
|
||||
"add": {
|
||||
"message": "Add"
|
||||
},
|
||||
"addANetwork": {
|
||||
"message": "Add a network"
|
||||
},
|
||||
"addANickname": {
|
||||
"message": "Add a nickname"
|
||||
},
|
||||
"addAcquiredTokens": {
|
||||
"message": "Add the tokens you've acquired using MetaMask"
|
||||
},
|
||||
@ -82,6 +88,9 @@
|
||||
"addFriendsAndAddresses": {
|
||||
"message": "Add friends and addresses you trust"
|
||||
},
|
||||
"addMemo": {
|
||||
"message": "Add memo"
|
||||
},
|
||||
"addNFT": {
|
||||
"message": "Add NFT"
|
||||
},
|
||||
@ -100,6 +109,9 @@
|
||||
"addToken": {
|
||||
"message": "Add Token"
|
||||
},
|
||||
"address": {
|
||||
"message": "Address"
|
||||
},
|
||||
"addressBookIcon": {
|
||||
"message": "Address book icon"
|
||||
},
|
||||
@ -167,6 +179,14 @@
|
||||
"message": "MetaMask",
|
||||
"description": "The name of the application"
|
||||
},
|
||||
"appNameBeta": {
|
||||
"message": "MetaMask Beta",
|
||||
"description": "The name of the application (Beta)"
|
||||
},
|
||||
"appNameFlask": {
|
||||
"message": "MetaMask Flask",
|
||||
"description": "The name of the application (Flask)"
|
||||
},
|
||||
"approvalAndAggregatorTxFeeCost": {
|
||||
"message": "Approval and aggregator network fee"
|
||||
},
|
||||
@ -549,6 +569,9 @@
|
||||
"currentlyUnavailable": {
|
||||
"message": "Unavailable on this network"
|
||||
},
|
||||
"custom": {
|
||||
"message": "Advanced"
|
||||
},
|
||||
"customGas": {
|
||||
"message": "Customize Gas"
|
||||
},
|
||||
@ -561,6 +584,16 @@
|
||||
"customToken": {
|
||||
"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": {
|
||||
"message": "Data"
|
||||
},
|
||||
@ -665,6 +698,9 @@
|
||||
"edit": {
|
||||
"message": "Edit"
|
||||
},
|
||||
"editAddressNickname": {
|
||||
"message": "Edit address nickname"
|
||||
},
|
||||
"editContact": {
|
||||
"message": "Edit Contact"
|
||||
},
|
||||
@ -686,6 +722,9 @@
|
||||
"editGasEducationModalTitle": {
|
||||
"message": "How to choose?"
|
||||
},
|
||||
"editGasFeeModalTitle": {
|
||||
"message": "Edit gas fee"
|
||||
},
|
||||
"editGasHigh": {
|
||||
"message": "High"
|
||||
},
|
||||
@ -891,6 +930,9 @@
|
||||
"etherscanView": {
|
||||
"message": "View account on Etherscan"
|
||||
},
|
||||
"etherscanViewOn": {
|
||||
"message": "View on Etherscan"
|
||||
},
|
||||
"expandView": {
|
||||
"message": "Expand view"
|
||||
},
|
||||
@ -984,6 +1026,9 @@
|
||||
"message": "Gas limit must be at least $1",
|
||||
"description": "$1 is the custom gas limit, in decimal."
|
||||
},
|
||||
"gasOption": {
|
||||
"message": "Gas option"
|
||||
},
|
||||
"gasPrice": {
|
||||
"message": "Gas Price (GWEI)"
|
||||
},
|
||||
@ -1002,10 +1047,18 @@
|
||||
"gasPriceInfoTooltipContent": {
|
||||
"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": {
|
||||
"message": "$1 minutes",
|
||||
"description": "$1 represents a number of minutes"
|
||||
},
|
||||
"gasTimingMinutesShort": {
|
||||
"message": "$1 min",
|
||||
"description": "$1 represents a number of minutes"
|
||||
},
|
||||
"gasTimingNegative": {
|
||||
"message": "Maybe in $1",
|
||||
"description": "$1 represents an amount of time"
|
||||
@ -1018,6 +1071,10 @@
|
||||
"message": "$1 seconds",
|
||||
"description": "$1 represents a number of seconds"
|
||||
},
|
||||
"gasTimingSecondsShort": {
|
||||
"message": "$1 sec",
|
||||
"description": "$1 represents a number of seconds"
|
||||
},
|
||||
"gasTimingVeryPositive": {
|
||||
"message": "Very likely in < $1",
|
||||
"description": "$1 represents an amount of time"
|
||||
@ -1100,9 +1157,15 @@
|
||||
"hideZeroBalanceTokens": {
|
||||
"message": "Hide Tokens Without Balance"
|
||||
},
|
||||
"high": {
|
||||
"message": "Aggressive"
|
||||
},
|
||||
"history": {
|
||||
"message": "History"
|
||||
},
|
||||
"id": {
|
||||
"message": "ID"
|
||||
},
|
||||
"import": {
|
||||
"message": "Import",
|
||||
"description": "Button to import an account from a selected file"
|
||||
@ -1117,7 +1180,7 @@
|
||||
"message": "import using Secret Recovery Phrase"
|
||||
},
|
||||
"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": {
|
||||
"message": "Import a wallet with Secret Recovery Phrase"
|
||||
@ -1336,6 +1399,12 @@
|
||||
"lockTimeTooGreat": {
|
||||
"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": {
|
||||
"message": "Ethereum Mainnet"
|
||||
},
|
||||
@ -1349,12 +1418,18 @@
|
||||
"max": {
|
||||
"message": "Max"
|
||||
},
|
||||
"maxBaseFee": {
|
||||
"message": "Max base fee"
|
||||
},
|
||||
"maxFee": {
|
||||
"message": "Max fee"
|
||||
},
|
||||
"maxPriorityFee": {
|
||||
"message": "Max priority fee"
|
||||
},
|
||||
"medium": {
|
||||
"message": "Market"
|
||||
},
|
||||
"memo": {
|
||||
"message": "memo"
|
||||
},
|
||||
@ -1538,6 +1613,12 @@
|
||||
"newContract": {
|
||||
"message": "New Contract"
|
||||
},
|
||||
"newNFTsDetected": {
|
||||
"message": "New NFTs detected"
|
||||
},
|
||||
"newNFTsDetectedInfo": {
|
||||
"message": "One or more new NFTs were detected in your wallet."
|
||||
},
|
||||
"newNetworkAdded": {
|
||||
"message": "“$1” was successfully added!"
|
||||
},
|
||||
@ -1560,9 +1641,15 @@
|
||||
"message": "Nonce is higher than suggested nonce of $1",
|
||||
"description": "The next nonce according to MetaMask's internal logic"
|
||||
},
|
||||
"nftTokenIdPlaceholder": {
|
||||
"message": "Enter the collectible ID"
|
||||
},
|
||||
"nfts": {
|
||||
"message": "NFTs"
|
||||
},
|
||||
"nickname": {
|
||||
"message": "Nickname"
|
||||
},
|
||||
"noAccountsFound": {
|
||||
"message": "No accounts found for the given search query"
|
||||
},
|
||||
@ -2129,6 +2216,9 @@
|
||||
"selectHdPath": {
|
||||
"message": "Select HD Path"
|
||||
},
|
||||
"selectNFTPrivacyPreference": {
|
||||
"message": "Select NFT privacy preference"
|
||||
},
|
||||
"selectPathHelp": {
|
||||
"message": "If you don't see the accounts you expect, try switching the HD path."
|
||||
},
|
||||
@ -2236,6 +2326,9 @@
|
||||
"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": {
|
||||
"message": "Skip"
|
||||
},
|
||||
@ -2742,6 +2835,9 @@
|
||||
"thisWillCreate": {
|
||||
"message": "This will create a new wallet and Secret Recovery Phrase"
|
||||
},
|
||||
"time": {
|
||||
"message": "Time"
|
||||
},
|
||||
"tips": {
|
||||
"message": "Tips"
|
||||
},
|
||||
@ -2896,6 +2992,9 @@
|
||||
"tryAgain": {
|
||||
"message": "Try again"
|
||||
},
|
||||
"tryAnywayOption": {
|
||||
"message": "I will try anyway"
|
||||
},
|
||||
"turnOnTokenDetection": {
|
||||
"message": "Turn on enhanced token detection"
|
||||
},
|
||||
|
@ -503,7 +503,7 @@
|
||||
"message": "编辑权限"
|
||||
},
|
||||
"encryptionPublicKeyNotice": {
|
||||
"message": "$1 希望得到您的加密公钥。同意后该网站将可以想您发送加密信息。",
|
||||
"message": "$1 希望得到您的加密公钥。同意后该网站将可以向您发送加密信息。",
|
||||
"description": "$1 is the web3 site name"
|
||||
},
|
||||
"encryptionPublicKeyRequest": {
|
||||
|
@ -21,6 +21,6 @@
|
||||
"128": "images/icon-128.png",
|
||||
"512": "images/icon-512.png"
|
||||
},
|
||||
"name": "__MSG_appName__ Beta",
|
||||
"short_name": "__MSG_appName__ Beta"
|
||||
"name": "__MSG_appNameBeta__",
|
||||
"short_name": "__MSG_appNameBeta__"
|
||||
}
|
||||
|
@ -21,6 +21,6 @@
|
||||
"128": "images/icon-128.png",
|
||||
"512": "images/icon-512.png"
|
||||
},
|
||||
"name": "__MSG_appName__ Flask",
|
||||
"short_name": "__MSG_appName__ Flask"
|
||||
"name": "__MSG_appNameFlask__",
|
||||
"short_name": "__MSG_appNameFlask__"
|
||||
}
|
||||
|
@ -17,13 +17,19 @@ import {
|
||||
ENVIRONMENT_TYPE_FULLSCREEN,
|
||||
} from '../../shared/constants/app';
|
||||
import { SECOND } from '../../shared/constants/time';
|
||||
import {
|
||||
REJECT_NOTFICIATION_CLOSE,
|
||||
REJECT_NOTFICIATION_CLOSE_SIG,
|
||||
} from '../../shared/constants/metametrics';
|
||||
import migrations from './migrations';
|
||||
import Migrator from './lib/migrator';
|
||||
import ExtensionPlatform from './platforms/extension';
|
||||
import LocalStore from './lib/local-store';
|
||||
import ReadOnlyNetworkStore from './lib/network-store';
|
||||
import createStreamSink from './lib/createStreamSink';
|
||||
import NotificationManager from './lib/notification-manager';
|
||||
import NotificationManager, {
|
||||
NOTIFICATION_MANAGER_EVENTS,
|
||||
} from './lib/notification-manager';
|
||||
import MetamaskController, {
|
||||
METAMASK_CONTROLLER_EVENTS,
|
||||
} from './metamask-controller';
|
||||
@ -475,6 +481,69 @@ function setupController(initState, initLangCode) {
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,13 @@
|
||||
import { createScaffoldMiddleware, mergeMiddleware } from 'json-rpc-engine';
|
||||
import createBlockRefMiddleware from 'eth-json-rpc-middleware/block-ref';
|
||||
import createRetryOnEmptyMiddleware from 'eth-json-rpc-middleware/retryOnEmpty';
|
||||
import createBlockCacheMiddleware from 'eth-json-rpc-middleware/block-cache';
|
||||
import createInflightCacheMiddleware from 'eth-json-rpc-middleware/inflight-cache';
|
||||
import createBlockTrackerInspectorMiddleware from 'eth-json-rpc-middleware/block-tracker-inspector';
|
||||
import providerFromMiddleware from 'eth-json-rpc-middleware/providerFromMiddleware';
|
||||
import {
|
||||
createBlockRefMiddleware,
|
||||
createRetryOnEmptyMiddleware,
|
||||
createBlockCacheMiddleware,
|
||||
createInflightCacheMiddleware,
|
||||
createBlockTrackerInspectorMiddleware,
|
||||
providerFromMiddleware,
|
||||
} from 'eth-json-rpc-middleware';
|
||||
|
||||
import createInfuraMiddleware from 'eth-json-rpc-infura';
|
||||
import { PollingBlockTracker } from 'eth-block-tracker';
|
||||
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { createAsyncMiddleware, mergeMiddleware } from 'json-rpc-engine';
|
||||
import createFetchMiddleware from 'eth-json-rpc-middleware/fetch';
|
||||
import createBlockRefRewriteMiddleware from 'eth-json-rpc-middleware/block-ref-rewrite';
|
||||
import createBlockCacheMiddleware from 'eth-json-rpc-middleware/block-cache';
|
||||
import createInflightMiddleware from 'eth-json-rpc-middleware/inflight-cache';
|
||||
import createBlockTrackerInspectorMiddleware from 'eth-json-rpc-middleware/block-tracker-inspector';
|
||||
import providerFromMiddleware from 'eth-json-rpc-middleware/providerFromMiddleware';
|
||||
import {
|
||||
createFetchMiddleware,
|
||||
createBlockRefRewriteMiddleware,
|
||||
createBlockCacheMiddleware,
|
||||
createInflightCacheMiddleware,
|
||||
createBlockTrackerInspectorMiddleware,
|
||||
providerFromMiddleware,
|
||||
} from 'eth-json-rpc-middleware';
|
||||
import { PollingBlockTracker } from 'eth-block-tracker';
|
||||
import { SECOND } from '../../../../shared/constants/time';
|
||||
|
||||
@ -27,7 +29,7 @@ export default function createJsonRpcClient({ rpcUrl, chainId }) {
|
||||
createChainIdMiddleware(chainId),
|
||||
createBlockRefRewriteMiddleware({ blockTracker }),
|
||||
createBlockCacheMiddleware({ blockTracker }),
|
||||
createInflightMiddleware(),
|
||||
createInflightCacheMiddleware(),
|
||||
createBlockTrackerInspectorMiddleware({ blockTracker }),
|
||||
fetchMiddleware,
|
||||
]);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { createScaffoldMiddleware, mergeMiddleware } from 'json-rpc-engine';
|
||||
import createWalletSubprovider from 'eth-json-rpc-middleware/wallet';
|
||||
import { createWalletMiddleware } from 'eth-json-rpc-middleware';
|
||||
import {
|
||||
createPendingNonceMiddleware,
|
||||
createPendingTxMiddleware,
|
||||
@ -21,11 +21,10 @@ export default function createMetamaskMiddleware({
|
||||
}) {
|
||||
const metamaskMiddleware = mergeMiddleware([
|
||||
createScaffoldMiddleware({
|
||||
// staticSubprovider
|
||||
eth_syncing: false,
|
||||
web3_clientVersion: `MetaMask/v${version}`,
|
||||
}),
|
||||
createWalletSubprovider({
|
||||
createWalletMiddleware({
|
||||
getAccounts,
|
||||
processTransaction,
|
||||
processEthSignMessage,
|
||||
|
@ -2,7 +2,7 @@ import { strict as assert } from 'assert';
|
||||
import EventEmitter from 'events';
|
||||
import { ComposedStore, ObservableStore } from '@metamask/obs-store';
|
||||
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 {
|
||||
createSwappableProxy,
|
||||
@ -430,7 +430,7 @@ export default class NetworkController extends EventEmitter {
|
||||
}
|
||||
|
||||
_setProviderAndBlockTracker({ provider, blockTracker }) {
|
||||
// update or intialize proxies
|
||||
// update or initialize proxies
|
||||
if (this._providerProxy) {
|
||||
this._providerProxy.setTarget(provider);
|
||||
} else {
|
||||
|
@ -37,6 +37,7 @@ export default class PreferencesController {
|
||||
// set to true means the dynamic list from the API is being used
|
||||
// set to false will be using the static list from contract-metadata
|
||||
useTokenDetection: false,
|
||||
advancedGasFee: null,
|
||||
|
||||
// WARNING: Do not use feature flags for security-sensitive things.
|
||||
// Feature flag toggling is available in the global namespace
|
||||
@ -129,6 +130,16 @@ export default class PreferencesController {
|
||||
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
|
||||
*
|
||||
|
@ -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';
|
||||
import fetchWithCache from '../../../ui/helpers/utils/fetch-with-cache';
|
||||
import { MINUTE, SECOND } from '../../../shared/constants/time';
|
||||
import { isEqualCaseInsensitive } from '../../../ui/helpers/utils/util';
|
||||
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
|
||||
@ -91,7 +92,7 @@ export default class SwapsController {
|
||||
networkController,
|
||||
provider,
|
||||
getProviderConfig,
|
||||
tokenRatesStore,
|
||||
getTokenRatesState,
|
||||
fetchTradesInfo = defaultFetchTradesInfo,
|
||||
getCurrentChainId,
|
||||
getEIP1559GasFeeEstimates,
|
||||
@ -105,7 +106,7 @@ export default class SwapsController {
|
||||
this._getEIP1559GasFeeEstimates = getEIP1559GasFeeEstimates;
|
||||
|
||||
this.getBufferedGasLimit = getBufferedGasLimit;
|
||||
this.tokenRatesStore = tokenRatesStore;
|
||||
this.getTokenRatesState = getTokenRatesState;
|
||||
|
||||
this.pollCount = 0;
|
||||
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.
|
||||
// _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.
|
||||
approvalRequired =
|
||||
allowance.eq(0) &&
|
||||
@ -610,7 +611,9 @@ export default class SwapsController {
|
||||
}
|
||||
|
||||
async _findTopQuoteAndCalculateSavings(quotes = {}) {
|
||||
const tokenConversionRates = this.tokenRatesStore.contractExchangeRates;
|
||||
const {
|
||||
contractExchangeRates: tokenConversionRates,
|
||||
} = this.getTokenRatesState();
|
||||
const {
|
||||
swapsState: { customGasPrice, customMaxPriorityFeePerGas },
|
||||
} = this.store.getState();
|
||||
@ -734,7 +737,12 @@ export default class SwapsController {
|
||||
decimalAdjustedDestinationAmount,
|
||||
);
|
||||
|
||||
const tokenConversionRate = tokenConversionRates[destinationToken];
|
||||
const tokenConversionRate =
|
||||
tokenConversionRates[
|
||||
Object.keys(tokenConversionRates).find((tokenAddress) =>
|
||||
isEqualCaseInsensitive(tokenAddress, destinationToken),
|
||||
)
|
||||
];
|
||||
const conversionRateForSorting = tokenConversionRate || 1;
|
||||
|
||||
const ethValueOfTokens = decimalAdjustedDestinationAmount.times(
|
||||
@ -777,7 +785,17 @@ export default class SwapsController {
|
||||
isSwapsDefaultTokenAddress(
|
||||
newQuotes[topAggId].destinationToken,
|
||||
chainId,
|
||||
) || Boolean(tokenConversionRates[newQuotes[topAggId]?.destinationToken]);
|
||||
) ||
|
||||
Boolean(
|
||||
tokenConversionRates[
|
||||
Object.keys(tokenConversionRates).find((tokenAddress) =>
|
||||
isEqualCaseInsensitive(
|
||||
tokenAddress,
|
||||
newQuotes[topAggId]?.destinationToken,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
let savings = null;
|
||||
|
||||
|
@ -82,12 +82,12 @@ const MOCK_FETCH_METADATA = {
|
||||
chainId: MAINNET_CHAIN_ID,
|
||||
};
|
||||
|
||||
const MOCK_TOKEN_RATES_STORE = {
|
||||
const MOCK_TOKEN_RATES_STORE = () => ({
|
||||
contractExchangeRates: {
|
||||
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': 2,
|
||||
'0x1111111111111111111111111111111111111111': 0.1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const MOCK_GET_PROVIDER_CONFIG = () => ({ type: 'FAKE_NETWORK' });
|
||||
|
||||
@ -161,7 +161,7 @@ describe('SwapsController', function () {
|
||||
networkController: getMockNetworkController(),
|
||||
provider,
|
||||
getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
|
||||
tokenRatesStore: MOCK_TOKEN_RATES_STORE,
|
||||
getTokenRatesState: MOCK_TOKEN_RATES_STORE,
|
||||
fetchTradesInfo: fetchTradesInfoStub,
|
||||
getCurrentChainId: getCurrentChainIdStub,
|
||||
getEIP1559GasFeeEstimates: getEIP1559GasFeeEstimatesStub,
|
||||
@ -211,7 +211,7 @@ describe('SwapsController', function () {
|
||||
networkController,
|
||||
provider,
|
||||
getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
|
||||
tokenRatesStore: MOCK_TOKEN_RATES_STORE,
|
||||
getTokenRatesState: MOCK_TOKEN_RATES_STORE,
|
||||
fetchTradesInfo: fetchTradesInfoStub,
|
||||
getCurrentChainId: getCurrentChainIdStub,
|
||||
});
|
||||
@ -235,7 +235,7 @@ describe('SwapsController', function () {
|
||||
networkController,
|
||||
provider,
|
||||
getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
|
||||
tokenRatesStore: MOCK_TOKEN_RATES_STORE,
|
||||
getTokenRatesState: MOCK_TOKEN_RATES_STORE,
|
||||
fetchTradesInfo: fetchTradesInfoStub,
|
||||
getCurrentChainId: getCurrentChainIdStub,
|
||||
});
|
||||
@ -259,7 +259,7 @@ describe('SwapsController', function () {
|
||||
networkController,
|
||||
provider,
|
||||
getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
|
||||
tokenRatesStore: MOCK_TOKEN_RATES_STORE,
|
||||
getTokenRatesState: MOCK_TOKEN_RATES_STORE,
|
||||
fetchTradesInfo: fetchTradesInfoStub,
|
||||
getCurrentChainId: getCurrentChainIdStub,
|
||||
});
|
||||
@ -816,9 +816,10 @@ describe('SwapsController', function () {
|
||||
.stub(swapsController, '_getERC20Allowance')
|
||||
.resolves(ethers.BigNumber.from(1));
|
||||
|
||||
swapsController.tokenRatesStore = {
|
||||
swapsController.getTokenRatesState = () => ({
|
||||
contractExchangeRates: {},
|
||||
};
|
||||
});
|
||||
|
||||
const [newQuotes, topAggId] = await swapsController.fetchAndSetQuotes(
|
||||
MOCK_FETCH_PARAMS,
|
||||
MOCK_FETCH_METADATA,
|
||||
|
@ -8,7 +8,7 @@ const Box = process.env.IN_TEST
|
||||
|
||||
import log from 'loglevel';
|
||||
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 migrations from '../migrations';
|
||||
import createOriginMiddleware from '../lib/createOriginMiddleware';
|
||||
|
@ -26,6 +26,7 @@ import {
|
||||
TRANSACTION_TYPES,
|
||||
TRANSACTION_ENVELOPE_TYPES,
|
||||
} from '../../../../shared/constants/transaction';
|
||||
import { TRANSACTION_ENVELOPE_TYPE_NAMES } from '../../../../ui/helpers/constants/transactions';
|
||||
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
|
||||
import {
|
||||
GAS_LIMITS,
|
||||
@ -1447,8 +1448,8 @@ export default class TransactionController extends EventEmitter {
|
||||
sensitiveProperties: {
|
||||
status,
|
||||
transaction_envelope_type: isEIP1559Transaction(txMeta)
|
||||
? 'fee-market'
|
||||
: 'legacy',
|
||||
? TRANSACTION_ENVELOPE_TYPE_NAMES.FEE_MARKET
|
||||
: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
|
||||
first_seen: time,
|
||||
gas_limit: gasLimit,
|
||||
...gasParamsInGwei,
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
GAS_ESTIMATE_TYPES,
|
||||
GAS_RECOMMENDATIONS,
|
||||
} from '../../../../shared/constants/gas';
|
||||
import { TRANSACTION_ENVELOPE_TYPE_NAMES } from '../../../../ui/helpers/constants/transactions';
|
||||
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
|
||||
import TransactionController, { TRANSACTION_EVENTS } from '.';
|
||||
|
||||
@ -774,7 +775,7 @@ describe('Transaction Controller', function () {
|
||||
nonce: '0x4b',
|
||||
},
|
||||
type: TRANSACTION_TYPES.SIMPLE_SEND,
|
||||
transaction_envelope_type: 'legacy',
|
||||
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
|
||||
origin: 'metamask',
|
||||
chainId: currentChainId,
|
||||
time: 1624408066355,
|
||||
@ -1578,7 +1579,7 @@ describe('Transaction Controller', function () {
|
||||
gas_price: '2',
|
||||
gas_limit: '0x7b0d',
|
||||
first_seen: 1624408066355,
|
||||
transaction_envelope_type: 'legacy',
|
||||
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
|
||||
status: 'unapproved',
|
||||
},
|
||||
};
|
||||
@ -1625,7 +1626,7 @@ describe('Transaction Controller', function () {
|
||||
gas_price: '2',
|
||||
gas_limit: '0x7b0d',
|
||||
first_seen: 1624408066355,
|
||||
transaction_envelope_type: 'legacy',
|
||||
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
|
||||
status: 'unapproved',
|
||||
},
|
||||
};
|
||||
@ -1674,7 +1675,7 @@ describe('Transaction Controller', function () {
|
||||
gas_price: '2',
|
||||
gas_limit: '0x7b0d',
|
||||
first_seen: 1624408066355,
|
||||
transaction_envelope_type: 'legacy',
|
||||
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
|
||||
status: 'unapproved',
|
||||
},
|
||||
};
|
||||
@ -1731,7 +1732,7 @@ describe('Transaction Controller', function () {
|
||||
max_priority_fee_per_gas: '2',
|
||||
gas_limit: '0x7b0d',
|
||||
first_seen: 1624408066355,
|
||||
transaction_envelope_type: 'fee-market',
|
||||
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.FEE_MARKET,
|
||||
status: 'unapproved',
|
||||
estimate_suggested: GAS_RECOMMENDATIONS.MEDIUM,
|
||||
estimate_used: GAS_RECOMMENDATIONS.HIGH,
|
||||
|
@ -1,12 +1,13 @@
|
||||
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 {
|
||||
GOERLI_CHAIN_ID,
|
||||
KOVAN_CHAIN_ID,
|
||||
MAINNET_CHAIN_ID,
|
||||
RINKEBY_CHAIN_ID,
|
||||
ROPSTEN_CHAIN_ID,
|
||||
MAINNET_NETWORK_ID,
|
||||
} from '../../../shared/constants/network';
|
||||
import { SECOND } from '../../../shared/constants/time';
|
||||
import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout';
|
||||
@ -20,7 +21,7 @@ const fetchWithTimeout = getFetchWithTimeout(SECOND * 30);
|
||||
* @returns String
|
||||
*/
|
||||
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`;
|
||||
try {
|
||||
const response = await fetchWithTimeout(fiatOnRampUrlApi, {
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
ROPSTEN_CHAIN_ID,
|
||||
} from '../../../shared/constants/network';
|
||||
import { TRANSAK_API_KEY } from '../constants/on-ramp';
|
||||
import { SWAPS_API_V2_BASE_URL } from '../../../shared/constants/swaps';
|
||||
import getBuyEthUrl from './buy-eth-url';
|
||||
|
||||
const WYRE_ACCOUNT_ID = 'AC-7AG3W4XH4N2';
|
||||
@ -28,8 +29,10 @@ const KOVAN = {
|
||||
|
||||
describe('buy-eth-url', function () {
|
||||
it('returns Wyre url with an ETH address for Ethereum mainnet', async function () {
|
||||
nock('https://api.metaswap.codefi.network')
|
||||
.get(`/fiatOnRampUrl?serviceName=wyre&destinationAddress=${ETH_ADDRESS}`)
|
||||
nock(SWAPS_API_V2_BASE_URL)
|
||||
.get(
|
||||
`/networks/1/fiatOnRampUrl?serviceName=wyre&destinationAddress=${ETH_ADDRESS}`,
|
||||
)
|
||||
.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`,
|
||||
});
|
||||
|
@ -38,13 +38,14 @@ export default class DecryptMessageManager extends EventEmitter {
|
||||
* @property {Array} messages Holds all messages that have been created by this DecryptMessageManager
|
||||
*
|
||||
*/
|
||||
constructor() {
|
||||
constructor(opts) {
|
||||
super();
|
||||
this.memStore = new ObservableStore({
|
||||
unapprovedDecryptMsgs: {},
|
||||
unapprovedDecryptMsgCount: 0,
|
||||
});
|
||||
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.
|
||||
*
|
||||
*/
|
||||
rejectMsg(msgId) {
|
||||
rejectMsg(msgId, reason = undefined) {
|
||||
if (reason) {
|
||||
this.metricsEvent({
|
||||
event: reason,
|
||||
category: 'Messages',
|
||||
properties: {
|
||||
action: 'Decrypt Message Request',
|
||||
},
|
||||
});
|
||||
}
|
||||
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
|
||||
*
|
||||
*/
|
||||
constructor() {
|
||||
constructor(opts) {
|
||||
super();
|
||||
this.memStore = new ObservableStore({
|
||||
unapprovedEncryptionPublicKeyMsgs: {},
|
||||
unapprovedEncryptionPublicKeyMsgCount: 0,
|
||||
});
|
||||
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.
|
||||
*
|
||||
*/
|
||||
rejectMsg(msgId) {
|
||||
rejectMsg(msgId, reason = undefined) {
|
||||
if (reason) {
|
||||
this.metricsEvent({
|
||||
event: reason,
|
||||
category: 'Messages',
|
||||
properties: {
|
||||
action: 'Encryption public key Request',
|
||||
},
|
||||
});
|
||||
}
|
||||
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
|
||||
*
|
||||
*/
|
||||
constructor() {
|
||||
constructor({ metricsEvent }) {
|
||||
super();
|
||||
this.memStore = new ObservableStore({
|
||||
unapprovedMsgs: {},
|
||||
unapprovedMsgCount: 0,
|
||||
});
|
||||
this.messages = [];
|
||||
this.metricsEvent = metricsEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -78,9 +79,9 @@ export default class MessageManager extends EventEmitter {
|
||||
* @returns {promise} after signature has been
|
||||
*
|
||||
*/
|
||||
addUnapprovedMessageAsync(msgParams, req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const msgId = this.addUnapprovedMessage(msgParams, req);
|
||||
async addUnapprovedMessageAsync(msgParams, req) {
|
||||
const msgId = this.addUnapprovedMessage(msgParams, req);
|
||||
return await new Promise((resolve, reject) => {
|
||||
// await finished
|
||||
this.once(`${msgId}:finished`, (data) => {
|
||||
switch (data.status) {
|
||||
@ -92,6 +93,10 @@ export default class MessageManager extends EventEmitter {
|
||||
'MetaMask Message Signature: User denied message signature.',
|
||||
),
|
||||
);
|
||||
case 'errored':
|
||||
return reject(
|
||||
new Error(`MetaMask Message Signature: ${data.error}`),
|
||||
);
|
||||
default:
|
||||
return reject(
|
||||
new Error(
|
||||
@ -217,10 +222,34 @@ export default class MessageManager extends EventEmitter {
|
||||
* @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');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@ -292,7 +321,7 @@ export default class MessageManager extends EventEmitter {
|
||||
* @returns {string} A hex string conversion of the buffer data
|
||||
*
|
||||
*/
|
||||
function normalizeMsgData(data) {
|
||||
export function normalizeMsgData(data) {
|
||||
if (data.slice(0, 2) === '0x') {
|
||||
// data is already hex
|
||||
return data;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import sinon from 'sinon';
|
||||
import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction';
|
||||
import MessageManager from './message-manager';
|
||||
|
||||
@ -6,7 +7,9 @@ describe('Message Manager', function () {
|
||||
let messageManager;
|
||||
|
||||
beforeEach(function () {
|
||||
messageManager = new MessageManager();
|
||||
messageManager = new MessageManager({
|
||||
metricsEvent: sinon.fake(),
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getMsgList', function () {
|
||||
|
@ -1,9 +1,14 @@
|
||||
import EventEmitter from 'safe-event-emitter';
|
||||
import ExtensionPlatform from '../platforms/extension';
|
||||
|
||||
const NOTIFICATION_HEIGHT = 620;
|
||||
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.
|
||||
*
|
||||
@ -12,7 +17,9 @@ export default class NotificationManager {
|
||||
*/
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
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
|
||||
* 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
|
||||
*
|
||||
*/
|
||||
constructor() {
|
||||
constructor({ metricsEvent }) {
|
||||
super();
|
||||
this.memStore = new ObservableStore({
|
||||
unapprovedPersonalMsgs: {},
|
||||
unapprovedPersonalMsgCount: 0,
|
||||
});
|
||||
this.messages = [];
|
||||
this.metricsEvent = metricsEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -106,6 +107,9 @@ export default class PersonalMessageManager extends EventEmitter {
|
||||
),
|
||||
);
|
||||
return;
|
||||
case 'errored':
|
||||
reject(new Error(`MetaMask Message Signature: ${data.error}`));
|
||||
return;
|
||||
default:
|
||||
reject(
|
||||
new Error(
|
||||
@ -238,10 +242,34 @@ export default class PersonalMessageManager extends EventEmitter {
|
||||
* @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');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import sinon from 'sinon';
|
||||
import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction';
|
||||
import PersonalMessageManager from './personal-message-manager';
|
||||
|
||||
@ -6,7 +7,7 @@ describe('Personal Message Manager', function () {
|
||||
let messageManager;
|
||||
|
||||
beforeEach(function () {
|
||||
messageManager = new PersonalMessageManager();
|
||||
messageManager = new PersonalMessageManager({ metricsEvent: sinon.fake() });
|
||||
});
|
||||
|
||||
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';
|
||||
|
||||
const handlerMap = handlers.reduce((map, handler) => {
|
||||
for (const methodName of handler.methodNames) {
|
||||
map.set(methodName, handler.implementation);
|
||||
map.set(methodName, handler);
|
||||
}
|
||||
return 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.
|
||||
*
|
||||
* @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}
|
||||
*/
|
||||
export default function createMethodMiddleware(opts) {
|
||||
return function methodMiddleware(req, res, next, end) {
|
||||
if (handlerMap.has(req.method)) {
|
||||
return handlerMap.get(req.method)(req, res, next, end, opts);
|
||||
// Reject unsupported methods.
|
||||
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();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = {
|
||||
methodNames: [MESSAGE_TYPE.ADD_ETHEREUM_CHAIN],
|
||||
implementation: addEthereumChainHandler,
|
||||
hookNames: {
|
||||
addCustomRpc: true,
|
||||
getCurrentChainId: true,
|
||||
findCustomRpcBy: true,
|
||||
updateRpcTarget: true,
|
||||
requestUserApproval: true,
|
||||
sendMetrics: true,
|
||||
},
|
||||
};
|
||||
export default addEthereumChain;
|
||||
|
||||
|
@ -9,6 +9,9 @@ import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
|
||||
const getProviderState = {
|
||||
methodNames: [MESSAGE_TYPE.GET_PROVIDER_STATE],
|
||||
implementation: getProviderStateHandler,
|
||||
hookNames: {
|
||||
getProviderState: true,
|
||||
},
|
||||
};
|
||||
export default getProviderState;
|
||||
|
||||
|
@ -10,6 +10,11 @@ import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
|
||||
const logWeb3ShimUsage = {
|
||||
methodNames: [MESSAGE_TYPE.LOG_WEB3_SHIM_USAGE],
|
||||
implementation: logWeb3ShimUsageHandler,
|
||||
hookNames: {
|
||||
sendMetrics: true,
|
||||
getWeb3ShimUsageState: true,
|
||||
setWeb3ShimUsageRecorded: true,
|
||||
},
|
||||
};
|
||||
export default logWeb3ShimUsage;
|
||||
|
||||
|
@ -15,6 +15,13 @@ import {
|
||||
const switchEthereumChain = {
|
||||
methodNames: [MESSAGE_TYPE.SWITCH_ETHEREUM_CHAIN],
|
||||
implementation: switchEthereumChainHandler,
|
||||
hookNames: {
|
||||
getCurrentChainId: true,
|
||||
findCustomRpcBy: true,
|
||||
setProviderType: true,
|
||||
updateRpcTarget: true,
|
||||
requestUserApproval: true,
|
||||
},
|
||||
};
|
||||
export default switchEthereumChain;
|
||||
|
||||
|
@ -3,6 +3,9 @@ import { MESSAGE_TYPE } from '../../../../../shared/constants/app';
|
||||
const watchAsset = {
|
||||
methodNames: [MESSAGE_TYPE.WATCH_ASSET, MESSAGE_TYPE.WATCH_ASSET_LEGACY],
|
||||
implementation: watchAssetHandler,
|
||||
hookNames: {
|
||||
handleWatchAssetRequest: true,
|
||||
},
|
||||
};
|
||||
export default watchAsset;
|
||||
|
||||
|
@ -32,7 +32,7 @@ export default class TypedMessageManager extends EventEmitter {
|
||||
/**
|
||||
* Controller in charge of managing - storing, adding, removing, updating - TypedMessage.
|
||||
*/
|
||||
constructor({ getCurrentChainId }) {
|
||||
constructor({ getCurrentChainId, metricEvents }) {
|
||||
super();
|
||||
this._getCurrentChainId = getCurrentChainId;
|
||||
this.memStore = new ObservableStore({
|
||||
@ -40,6 +40,7 @@ export default class TypedMessageManager extends EventEmitter {
|
||||
unapprovedTypedMessagesCount: 0,
|
||||
});
|
||||
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.
|
||||
*
|
||||
*/
|
||||
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');
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@ describe('Typed Message Manager', function () {
|
||||
beforeEach(async function () {
|
||||
typedMessageManager = new TypedMessageManager({
|
||||
getCurrentChainId: sinon.fake.returns('0x1'),
|
||||
metricsEvent: sinon.fake(),
|
||||
});
|
||||
|
||||
msgParamsV1 = {
|
||||
|
@ -7,7 +7,7 @@ import { debounce } from 'lodash';
|
||||
import createEngineStream from 'json-rpc-middleware-stream/engineStream';
|
||||
import createFilterMiddleware from 'eth-json-rpc-filters';
|
||||
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 { Mutex } from 'await-semaphore';
|
||||
import { stripHexPrefix } from 'ethereumjs-util';
|
||||
@ -17,6 +17,7 @@ import LedgerBridgeKeyring from '@metamask/eth-ledger-bridge-keyring';
|
||||
import LatticeKeyring from 'eth-lattice-keyring';
|
||||
import EthQuery from 'eth-query';
|
||||
import nanoid from 'nanoid';
|
||||
import { ethErrors } from 'eth-rpc-errors';
|
||||
import { captureException } from '@sentry/browser';
|
||||
import {
|
||||
AddressBookController,
|
||||
@ -29,6 +30,9 @@ import {
|
||||
TokenListController,
|
||||
TokensController,
|
||||
TokenRatesController,
|
||||
CollectiblesController,
|
||||
AssetsContractController,
|
||||
CollectibleDetectionController,
|
||||
} from '@metamask/controllers';
|
||||
import { TRANSACTION_STATUSES } from '../../shared/constants/transaction';
|
||||
import {
|
||||
@ -61,7 +65,7 @@ import AlertController from './controllers/alert';
|
||||
import OnboardingController from './controllers/onboarding';
|
||||
import ThreeBoxController from './controllers/threebox';
|
||||
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 EncryptionPublicKeyManager from './lib/encryption-public-key-manager';
|
||||
import PersonalMessageManager from './lib/personal-message-manager';
|
||||
@ -175,6 +179,57 @@ export default class MetamaskController extends EventEmitter {
|
||||
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({
|
||||
segment,
|
||||
preferencesStore: this.preferencesController.store,
|
||||
@ -526,14 +581,33 @@ export default class MetamaskController extends EventEmitter {
|
||||
}
|
||||
});
|
||||
this.networkController.lookupNetwork();
|
||||
this.messageManager = new MessageManager();
|
||||
this.personalMessageManager = new PersonalMessageManager();
|
||||
this.decryptMessageManager = new DecryptMessageManager();
|
||||
this.encryptionPublicKeyManager = new EncryptionPublicKeyManager();
|
||||
this.messageManager = new MessageManager({
|
||||
metricsEvent: this.metaMetricsController.trackEvent.bind(
|
||||
this.metaMetricsController,
|
||||
),
|
||||
});
|
||||
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({
|
||||
getCurrentChainId: this.networkController.getCurrentChainId.bind(
|
||||
this.networkController,
|
||||
),
|
||||
metricsEvent: this.metaMetricsController.trackEvent.bind(
|
||||
this.metaMetricsController,
|
||||
),
|
||||
});
|
||||
|
||||
this.swapsController = new SwapsController({
|
||||
@ -545,7 +619,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
getProviderConfig: this.networkController.getProviderConfig.bind(
|
||||
this.networkController,
|
||||
),
|
||||
tokenRatesStore: this.tokenRatesController.state,
|
||||
getTokenRatesState: () => this.tokenRatesController.state,
|
||||
getCurrentChainId: this.networkController.getCurrentChainId.bind(
|
||||
this.networkController,
|
||||
),
|
||||
@ -592,6 +666,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
GasFeeController: this.gasFeeController,
|
||||
TokenListController: this.tokenListController,
|
||||
TokensController: this.tokensController,
|
||||
CollectiblesController: this.collectiblesController,
|
||||
});
|
||||
|
||||
this.memStore = new ComposableObservableStore({
|
||||
@ -626,6 +701,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
GasFeeController: this.gasFeeController,
|
||||
TokenListController: this.tokenListController,
|
||||
TokensController: this.tokensController,
|
||||
CollectiblesController: this.collectiblesController,
|
||||
},
|
||||
controllerMessenger: this.controllerMessenger,
|
||||
});
|
||||
@ -807,6 +883,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
threeBoxController,
|
||||
txController,
|
||||
tokensController,
|
||||
collectiblesController,
|
||||
} = this;
|
||||
|
||||
return {
|
||||
@ -924,6 +1001,26 @@ export default class MetamaskController extends EventEmitter {
|
||||
this.preferencesController.setDismissSeedBackUpReminder,
|
||||
this.preferencesController,
|
||||
),
|
||||
setAdvancedGasFee: nodeify(
|
||||
preferencesController.setAdvancedGasFee,
|
||||
preferencesController,
|
||||
),
|
||||
|
||||
// CollectiblesController
|
||||
addCollectible: nodeify(
|
||||
collectiblesController.addCollectible,
|
||||
collectiblesController,
|
||||
),
|
||||
|
||||
removeAndIgnoreCollectible: nodeify(
|
||||
collectiblesController.removeAndIgnoreCollectible,
|
||||
collectiblesController,
|
||||
),
|
||||
|
||||
removeCollectible: nodeify(
|
||||
collectiblesController.removeCollectible,
|
||||
collectiblesController,
|
||||
),
|
||||
|
||||
// AddressController
|
||||
setAddressBook: nodeify(
|
||||
@ -985,9 +1082,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
),
|
||||
createCancelTransaction: nodeify(this.createCancelTransaction, this),
|
||||
createSpeedUpTransaction: nodeify(this.createSpeedUpTransaction, this),
|
||||
isNonceTaken: nodeify(txController.isNonceTaken, txController),
|
||||
estimateGas: nodeify(this.estimateGas, this),
|
||||
getPendingNonce: nodeify(this.getPendingNonce, this),
|
||||
getNextNonce: nodeify(this.getNextNonce, this),
|
||||
addUnapprovedTransaction: nodeify(
|
||||
txController.addUnapprovedTransaction,
|
||||
@ -1071,13 +1166,6 @@ export default class MetamaskController extends EventEmitter {
|
||||
permissionsController.approvePermissionsRequest,
|
||||
permissionsController,
|
||||
),
|
||||
clearPermissions: permissionsController.clearPermissions.bind(
|
||||
permissionsController,
|
||||
),
|
||||
getApprovedAccounts: nodeify(
|
||||
permissionsController.getAccounts,
|
||||
permissionsController,
|
||||
),
|
||||
rejectPermissionsRequest: nodeify(
|
||||
permissionsController.rejectPermissionsRequest,
|
||||
permissionsController,
|
||||
@ -1232,6 +1320,14 @@ export default class MetamaskController extends EventEmitter {
|
||||
this.detectTokensController.detectNewTokens,
|
||||
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 {Function} cb - The callback function called with the signature.
|
||||
*/
|
||||
newUnsignedMessage(msgParams, req) {
|
||||
const promise = this.messageManager.addUnapprovedMessageAsync(
|
||||
msgParams,
|
||||
req,
|
||||
);
|
||||
this.sendUpdate();
|
||||
this.opts.showUserConfirmation();
|
||||
return promise;
|
||||
async newUnsignedMessage(msgParams, req) {
|
||||
const data = normalizeMsgData(msgParams.data);
|
||||
let promise;
|
||||
// 64 hex + "0x" at the beginning
|
||||
// This is needed because Ethereum's EcSign works only on 32 byte numbers
|
||||
// For 67 length see: https://github.com/MetaMask/metamask-extension/pull/12679/files#r749479607
|
||||
if (data.length === 66 || data.length === 67) {
|
||||
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.
|
||||
* @returns {Promise<Object>} Full state update.
|
||||
*/
|
||||
signMessage(msgParams) {
|
||||
async signMessage(msgParams) {
|
||||
log.info('MetaMaskController - signMessage');
|
||||
const msgId = msgParams.metamaskId;
|
||||
|
||||
// sets the status op the message to 'approved'
|
||||
// and removes the metamaskId for signing
|
||||
return this.messageManager
|
||||
.approveMessage(msgParams)
|
||||
.then((cleanMsgParams) => {
|
||||
// signs the message
|
||||
return this.keyringController.signMessage(cleanMsgParams);
|
||||
})
|
||||
.then((rawSig) => {
|
||||
// tells the listener that the message has been signed
|
||||
// and can be returned to the dapp
|
||||
this.messageManager.setMsgStatusSigned(msgId, rawSig);
|
||||
return this.getState();
|
||||
});
|
||||
try {
|
||||
// sets the status op the message to 'approved'
|
||||
// and removes the metamaskId for signing
|
||||
const cleanMsgParams = await this.messageManager.approveMessage(
|
||||
msgParams,
|
||||
);
|
||||
const rawSig = await this.keyringController.signMessage(cleanMsgParams);
|
||||
this.messageManager.setMsgStatusSigned(msgId, rawSig);
|
||||
return this.getState();
|
||||
} catch (error) {
|
||||
log.info('MetaMaskController - eth_sign failed', error);
|
||||
this.messageManager.errorMessage(msgId, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1909,23 +2012,27 @@ export default class MetamaskController extends EventEmitter {
|
||||
* @param {Object} msgParams - The params of the message to sign & return to the Dapp.
|
||||
* @returns {Promise<Object>} A full state update.
|
||||
*/
|
||||
signPersonalMessage(msgParams) {
|
||||
async signPersonalMessage(msgParams) {
|
||||
log.info('MetaMaskController - signPersonalMessage');
|
||||
const msgId = msgParams.metamaskId;
|
||||
// sets the status op the message to 'approved'
|
||||
// and removes the metamaskId for signing
|
||||
return this.personalMessageManager
|
||||
.approveMessage(msgParams)
|
||||
.then((cleanMsgParams) => {
|
||||
// signs the message
|
||||
return this.keyringController.signPersonalMessage(cleanMsgParams);
|
||||
})
|
||||
.then((rawSig) => {
|
||||
// tells the listener that the message has been signed
|
||||
// and can be returned to the dapp
|
||||
this.personalMessageManager.setMsgStatusSigned(msgId, rawSig);
|
||||
return this.getState();
|
||||
});
|
||||
try {
|
||||
const cleanMsgParams = await this.personalMessageManager.approveMessage(
|
||||
msgParams,
|
||||
);
|
||||
const rawSig = await this.keyringController.signPersonalMessage(
|
||||
cleanMsgParams,
|
||||
);
|
||||
// tells the listener that the message has been signed
|
||||
// and can be returned to the dapp
|
||||
this.personalMessageManager.setMsgStatusSigned(msgId, rawSig);
|
||||
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;
|
||||
|
||||
const address = '0xc42edfcc21ed14dda456aa0756c153f7985d8813';
|
||||
const data = '0x43727970746f6b697474696573';
|
||||
const data =
|
||||
'0x0000000000000000000000000000000000000043727970746f6b697474696573';
|
||||
|
||||
beforeEach(async function () {
|
||||
sandbox.stub(metamaskController, 'getBalance');
|
||||
@ -885,6 +886,19 @@ describe('MetaMaskController', function () {
|
||||
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 () {
|
||||
try {
|
||||
await metamaskController.signMessage(messages[0].msgParams);
|
||||
|
@ -162,6 +162,10 @@ export default class ExtensionPlatform {
|
||||
}
|
||||
}
|
||||
|
||||
addOnRemovedListener(listener) {
|
||||
extension.windows.onRemoved.addListener(listener);
|
||||
}
|
||||
|
||||
getAllWindows() {
|
||||
return new Promise((resolve, reject) => {
|
||||
extension.windows.getAll((windows) => {
|
||||
|
@ -358,10 +358,14 @@ function createFactoredBuild({
|
||||
// lavamoat will add lavapack but it will be removed by bify-module-groups
|
||||
// we will re-add it later by installing a lavapack runtime
|
||||
const lavamoatOpts = {
|
||||
policy: path.resolve(__dirname, '../../lavamoat/browserify/policy.json'),
|
||||
policy: path.resolve(
|
||||
__dirname,
|
||||
`../../lavamoat/browserify/${buildType}/policy.json`,
|
||||
),
|
||||
policyName: buildType,
|
||||
policyOverride: path.resolve(
|
||||
__dirname,
|
||||
'../../lavamoat/browserify/policy-override.json',
|
||||
`../../lavamoat/browserify/${buildType}/policy-override.json`,
|
||||
),
|
||||
writeAutoPolicy: process.env.WRITE_AUTO_POLICY,
|
||||
};
|
||||
@ -456,7 +460,7 @@ function createFactoredBuild({
|
||||
groupSet,
|
||||
commonSet,
|
||||
browserPlatforms,
|
||||
useLavamoat: false,
|
||||
useLavamoat: true,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
@ -41,11 +41,16 @@ class RemoveFencedCodeTransform extends Transform {
|
||||
// stream, immediately before the "end" event is emitted.
|
||||
// It applies the transform to the concatenated file contents.
|
||||
_flush(end) {
|
||||
const [fileContent, didModify] = removeFencedCode(
|
||||
this.filePath,
|
||||
this.buildType,
|
||||
Buffer.concat(this._fileBuffers).toString('utf8'),
|
||||
);
|
||||
let fileContent, didModify;
|
||||
try {
|
||||
[fileContent, didModify] = removeFencedCode(
|
||||
this.filePath,
|
||||
this.buildType,
|
||||
Buffer.concat(this._fileBuffers).toString('utf8'),
|
||||
);
|
||||
} catch (error) {
|
||||
return end(error);
|
||||
}
|
||||
|
||||
const pushAndEnd = () => {
|
||||
this.push(fileContent);
|
||||
@ -53,12 +58,11 @@ class RemoveFencedCodeTransform extends Transform {
|
||||
};
|
||||
|
||||
if (this.shouldLintTransformedFiles && didModify) {
|
||||
lintTransformedFile(fileContent, this.filePath)
|
||||
return lintTransformedFile(fileContent, this.filePath)
|
||||
.then(pushAndEnd)
|
||||
.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 () => {
|
||||
lintTransformedFileMock.mockImplementationOnce(() =>
|
||||
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
|
||||
const messageExceptions = ['appName', 'appDescription'];
|
||||
const messageExceptions = [
|
||||
'appName',
|
||||
'appNameBeta',
|
||||
'appNameFlask',
|
||||
'appDescription',
|
||||
];
|
||||
|
||||
const englishMessages = Object.keys(englishLocale);
|
||||
const unusedMessages = englishMessages.filter(
|
||||
|
@ -325,6 +325,7 @@
|
||||
"@ethersproject/bignumber": true,
|
||||
"@ethersproject/bytes": true,
|
||||
"@ethersproject/keccak256": true,
|
||||
"@ethersproject/logger": true,
|
||||
"@ethersproject/sha2": true,
|
||||
"@ethersproject/strings": true
|
||||
}
|
||||
@ -525,6 +526,7 @@
|
||||
"ethjs-util": true,
|
||||
"events": true,
|
||||
"human-standard-collectible-abi": true,
|
||||
"human-standard-multi-collectible-abi": true,
|
||||
"human-standard-token-abi": true,
|
||||
"immer": true,
|
||||
"isomorphic-fetch": true,
|
||||
@ -1555,11 +1557,15 @@
|
||||
},
|
||||
"eth-json-rpc-middleware": {
|
||||
"globals": {
|
||||
"URL": true,
|
||||
"btoa": true,
|
||||
"console.error": true,
|
||||
"fetch": true,
|
||||
"setTimeout": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/safe-event-emitter": true,
|
||||
"browser-resolve": true,
|
||||
"btoa": true,
|
||||
"clone": 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:lavamoat": "yarn build dev",
|
||||
"dist": "yarn build prod",
|
||||
"build": "lavamoat development/build/index.js",
|
||||
"build": "yarn lavamoat:build",
|
||||
"build:dev": "node development/build/index.js",
|
||||
"start:test": "yarn build testDev",
|
||||
"benchmark:chrome": "SELENIUM_BROWSER=chrome node test/e2e/benchmark.js",
|
||||
@ -41,8 +41,9 @@
|
||||
"test:coverage:path": "nyc --check-coverage yarn test:unit:path",
|
||||
"ganache:start": "./development/run-ganache.sh",
|
||||
"sentry:publish": "node ./development/sentry-publish.js",
|
||||
"lint": "prettier --check '**/*.json' && eslint . --ext js,snap --cache && yarn lint:styles",
|
||||
"lint:fix": "prettier --write '**/*.json' && eslint . --ext js --cache --fix",
|
||||
"lint:prettier": "prettier '**/*.json'",
|
||||
"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: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",
|
||||
@ -63,9 +64,10 @@
|
||||
"storybook:deploy": "storybook-to-ghpages --existing-output-dir storybook-build --remote storybook --branch master",
|
||||
"update-changelog": "auto-changelog update",
|
||||
"generate:migration": "./development/generate-migration.sh",
|
||||
"lavamoat:build:auto": "lavamoat ./development/build/index.js --writeAutoPolicy",
|
||||
"lavamoat:debug:build": "lavamoat ./development/build/index.js --writeAutoPolicyDebug",
|
||||
"lavamoat:background:auto": "WRITE_AUTO_POLICY=1 yarn build prod",
|
||||
"lavamoat:build": "lavamoat development/build/index.js --policy lavamoat/build-system/policy.json --policyOverride lavamoat/build-system/policy-override.json",
|
||||
"lavamoat:build:auto": "yarn lavamoat:build --writeAutoPolicy",
|
||||
"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"
|
||||
},
|
||||
"resolutions": {
|
||||
@ -91,7 +93,8 @@
|
||||
"netmask": "^2.0.1",
|
||||
"pubnub/superagent-proxy": "^3.0.0",
|
||||
"pull-ws": "^3.3.2",
|
||||
"ws": "^7.4.6"
|
||||
"ws": "^7.4.6",
|
||||
"json-schema": "^0.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"3box": "^1.10.2",
|
||||
@ -105,7 +108,7 @@
|
||||
"@fortawesome/fontawesome-free": "^5.13.0",
|
||||
"@material-ui/core": "^4.11.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-token-tracker": "^3.0.1",
|
||||
"@metamask/etherscan-link": "^2.1.0",
|
||||
@ -135,7 +138,7 @@
|
||||
"eth-ens-namehash": "^2.0.8",
|
||||
"eth-json-rpc-filters": "^4.2.1",
|
||||
"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-lattice-keyring": "^0.4.0",
|
||||
"eth-method-registry": "^2.0.0",
|
||||
@ -221,7 +224,7 @@
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@babel/register": "^7.5.5",
|
||||
"@lavamoat/allow-scripts": "^1.0.6",
|
||||
"@lavamoat/lavapack": "^2.0.3",
|
||||
"@lavamoat/lavapack": "^2.0.4",
|
||||
"@metamask/auto-changelog": "^2.1.0",
|
||||
"@metamask/eslint-config": "^6.0.0",
|
||||
"@metamask/eslint-config-jest": "^6.0.0",
|
||||
|
@ -30,6 +30,17 @@ export const GAS_RECOMMENDATIONS = {
|
||||
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
|
||||
*/
|
||||
|
@ -140,3 +140,7 @@ export const METAMETRICS_BACKGROUND_PAGE_OBJECT = {
|
||||
* @property {() => void} identify - Identify an anonymous user. We do not
|
||||
* 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_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": ""
|
||||
},
|
||||
"useTokenDetection": true,
|
||||
"advancedGasFee": {
|
||||
"maxBaseFee": "1.5",
|
||||
"priorityFee": "2"
|
||||
},
|
||||
"tokenList": {
|
||||
"0x2260fac5e5542a773aa44fbcfedf7c193bc2c599": {
|
||||
"address": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599",
|
||||
|
@ -27,38 +27,72 @@ describe('Metamask Import UI', function () {
|
||||
async ({ driver }) => {
|
||||
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',
|
||||
});
|
||||
if (process.env.ONBOARDING_V2 === '1') {
|
||||
// welcome
|
||||
await driver.clickElement('[data-testid="onboarding-import-wallet"]');
|
||||
|
||||
// clicks the "Import Wallet" option
|
||||
await driver.clickElement({ text: 'Import wallet', tag: 'button' });
|
||||
// metrics
|
||||
await driver.clickElement('[data-testid="metametrics-no-thanks"]');
|
||||
|
||||
// clicks the "No thanks" option on the metametrics opt-in screen
|
||||
await driver.clickElement('.btn-secondary');
|
||||
// import with recovery phrase
|
||||
await driver.fill('[data-testid="import-srp-text"]', testSeedPhrase);
|
||||
await driver.clickElement('[data-testid="import-srp-confirm"]');
|
||||
|
||||
// Import Secret Recovery Phrase
|
||||
await driver.fill(
|
||||
'input[placeholder="Paste Secret Recovery Phrase from clipboard"]',
|
||||
testSeedPhrase,
|
||||
);
|
||||
// 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-import"]');
|
||||
|
||||
await driver.fill('#password', 'correct horse battery staple');
|
||||
await driver.fill('#confirm-password', 'correct horse battery staple');
|
||||
// complete
|
||||
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
|
||||
await driver.findElement({ text: 'Congratulations', tag: 'div' });
|
||||
await driver.clickElement({
|
||||
text: enLocaleMessages.endOfFlowMessage10.message,
|
||||
tag: 'button',
|
||||
});
|
||||
// clicks the "No thanks" option on the metametrics opt-in screen
|
||||
await driver.clickElement('.btn-secondary');
|
||||
|
||||
// Import Secret Recovery Phrase
|
||||
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
|
||||
await driver.clickElement(
|
||||
@ -233,10 +267,15 @@ describe('Metamask Import UI', function () {
|
||||
// should remove the account
|
||||
await driver.clickElement({ text: 'Remove', tag: 'button' });
|
||||
|
||||
const currentActiveAccountName = await driver.findElement(
|
||||
'.selected-account__name',
|
||||
// Wait until selected account switches away from removed account to first account
|
||||
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.clickElement('.account-menu__icon');
|
||||
|
||||
|
@ -16,50 +16,6 @@ describe('Metamask Responsive UI', function () {
|
||||
async ({ driver }) => {
|
||||
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) {
|
||||
await driver.clickElement(
|
||||
`[data-testid="seed-phrase-sorted"] [data-testid="draggable-seed-${word}"]`,
|
||||
@ -67,26 +23,126 @@ describe('Metamask Responsive UI', function () {
|
||||
await driver.delay(tinyDelayMs);
|
||||
}
|
||||
|
||||
// can retype the Secret Recovery Phrase
|
||||
const words = seedPhrase.split(' ');
|
||||
for (const word of words) {
|
||||
await clickWordAndWait(word);
|
||||
if (process.env.ONBOARDING_V2 === '1') {
|
||||
// welcome
|
||||
await driver.clickElement('[data-testid="onboarding-create-wallet"]');
|
||||
|
||||
// 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
|
||||
await driver.findElement({ text: 'Congratulations', tag: 'div' });
|
||||
await driver.clickElement({
|
||||
text: enLocaleMessages.endOfFlowMessage10.message,
|
||||
tag: 'button',
|
||||
});
|
||||
|
||||
// Show account information
|
||||
// balance renders
|
||||
await driver.waitForSelector({
|
||||
css: '[data-testid="eth-overview__primary-currency"]',
|
||||
text: '0 ETH',
|
||||
});
|
||||
// assert balance
|
||||
const balance = await driver.findElement(
|
||||
'[data-testid="wallet-balance"]',
|
||||
);
|
||||
assert.ok(/^0\sETH$/u.test(await balance.getText()));
|
||||
},
|
||||
);
|
||||
});
|
||||
|
@ -1,17 +1,19 @@
|
||||
const { strict: assert } = require('assert');
|
||||
const { errorCodes } = require('eth-rpc-errors');
|
||||
const { withFixtures } = require('../helpers');
|
||||
|
||||
describe('MetaMask', function () {
|
||||
const ganacheOptions = {
|
||||
accounts: [
|
||||
{
|
||||
secretKey:
|
||||
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC',
|
||||
balance: 25000000000000000000,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it('provider should inform dapp when switching networks', async function () {
|
||||
const ganacheOptions = {
|
||||
accounts: [
|
||||
{
|
||||
secretKey:
|
||||
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC',
|
||||
balance: 25000000000000000000,
|
||||
},
|
||||
],
|
||||
};
|
||||
await withFixtures(
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* For Selenium WebDriver API documentation, see:
|
||||
* https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html
|
||||
*/
|
||||
class Driver {
|
||||
/**
|
||||
* @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) {
|
||||
return this.driver.executeScript(script, args);
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { JsonRpcEngine } from 'json-rpc-engine';
|
||||
import scaffoldMiddleware from 'eth-json-rpc-middleware/scaffold';
|
||||
import providerAsMiddleware from 'eth-json-rpc-middleware/providerAsMiddleware';
|
||||
import { JsonRpcEngine, createScaffoldMiddleware } from 'json-rpc-engine';
|
||||
import { providerAsMiddleware } from 'eth-json-rpc-middleware';
|
||||
import GanacheCore from 'ganache-core';
|
||||
|
||||
export function getTestSeed() {
|
||||
@ -45,7 +44,7 @@ export function providerFromEngine(engine) {
|
||||
export function createTestProviderTools(opts = {}) {
|
||||
const engine = createEngineForTestData();
|
||||
// handle provided hooks
|
||||
engine.push(scaffoldMiddleware(opts.scaffold || {}));
|
||||
engine.push(createScaffoldMiddleware(opts.scaffold || {}));
|
||||
// handle block tracker methods
|
||||
engine.push(
|
||||
providerAsMiddleware(
|
||||
|
@ -13,6 +13,8 @@
|
||||
@import 'connected-status-indicator/index';
|
||||
@import 'edit-gas-display/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-price-button-group/index';
|
||||
@import 'gas-customization/index';
|
||||
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import Box from '../../ui/box';
|
||||
import Button from '../../ui/button';
|
||||
import Typography from '../../ui/typography/typography';
|
||||
import NewCollectiblesNotice from '../new-collectibles-notice';
|
||||
import {
|
||||
COLORS,
|
||||
TYPOGRAPHY,
|
||||
@ -15,6 +16,7 @@ import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||
|
||||
export default function CollectiblesList({ onAddNFT }) {
|
||||
const collectibles = [];
|
||||
const newNFTsDetected = true;
|
||||
const t = useI18nContext();
|
||||
|
||||
return (
|
||||
@ -22,7 +24,8 @@ export default function CollectiblesList({ onAddNFT }) {
|
||||
{collectibles.length > 0 ? (
|
||||
<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}>
|
||||
<img src="./images/no-nfts.svg" />
|
||||
</Box>
|
||||
|
@ -10,6 +10,14 @@ import ConfirmPageContainer, {
|
||||
ConfirmPageContainerNavigation,
|
||||
} from '.';
|
||||
|
||||
jest.mock('../../../store/actions', () => ({
|
||||
disconnectGasFeeEstimatePoller: jest.fn(),
|
||||
getGasFeeEstimatesAndStartPolling: jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve()),
|
||||
addPollingTokenToAppState: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Confirm Page Container Container Test', () => {
|
||||
let wrapper;
|
||||
|
||||
@ -31,6 +39,8 @@ describe('Confirm Page Container Container Test', () => {
|
||||
selectedAddress: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5',
|
||||
addressBook: [],
|
||||
chainId: 'test',
|
||||
identities: [],
|
||||
featureFlags: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { Tabs, Tab } from '../../../ui/tabs';
|
||||
import ErrorMessage from '../../../ui/error-message';
|
||||
import ActionableMessage from '../../../ui/actionable-message/actionable-message';
|
||||
import { PageContainerFooter } from '../../../ui/page-container';
|
||||
import { ConfirmPageContainerSummary, ConfirmPageContainerWarning } from '.';
|
||||
|
||||
@ -17,6 +18,7 @@ export default class ConfirmPageContainerContent extends Component {
|
||||
detailsComponent: PropTypes.node,
|
||||
errorKey: PropTypes.string,
|
||||
errorMessage: PropTypes.string,
|
||||
hasSimulationError: PropTypes.bool,
|
||||
hideSubtitle: PropTypes.bool,
|
||||
identiconAddress: PropTypes.string,
|
||||
nonce: PropTypes.string,
|
||||
@ -31,8 +33,10 @@ export default class ConfirmPageContainerContent extends Component {
|
||||
onCancel: PropTypes.func,
|
||||
cancelText: PropTypes.string,
|
||||
onSubmit: PropTypes.func,
|
||||
onConfirmAnyways: PropTypes.func,
|
||||
submitText: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
hideConfirmAnyways: PropTypes.bool,
|
||||
unapprovedTxCount: PropTypes.number,
|
||||
rejectNText: PropTypes.string,
|
||||
hideTitle: PropTypes.boolean,
|
||||
@ -71,6 +75,7 @@ export default class ConfirmPageContainerContent extends Component {
|
||||
action,
|
||||
errorKey,
|
||||
errorMessage,
|
||||
hasSimulationError,
|
||||
title,
|
||||
titleComponent,
|
||||
subtitleComponent,
|
||||
@ -91,14 +96,32 @@ export default class ConfirmPageContainerContent extends Component {
|
||||
origin,
|
||||
ethGasPriceWarning,
|
||||
hideTitle,
|
||||
onConfirmAnyways,
|
||||
hideConfirmAnyways,
|
||||
} = this.props;
|
||||
|
||||
const primaryAction = hideConfirmAnyways
|
||||
? null
|
||||
: {
|
||||
label: this.context.t('tryAnywayOption'),
|
||||
onClick: onConfirmAnyways,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="confirm-page-container-content">
|
||||
{warning ? <ConfirmPageContainerWarning warning={warning} /> : null}
|
||||
{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
|
||||
className={classnames({
|
||||
'confirm-page-container-summary--border':
|
||||
@ -115,7 +138,7 @@ export default class ConfirmPageContainerContent extends Component {
|
||||
hideTitle={hideTitle}
|
||||
/>
|
||||
{this.renderContent()}
|
||||
{(errorKey || errorMessage) && (
|
||||
{(errorKey || errorMessage) && !hasSimulationError && (
|
||||
<div className="confirm-page-container-content__error-container">
|
||||
<ErrorMessage errorMessage={errorMessage} errorKey={errorKey} />
|
||||
</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 EditGasPopover from '../edit-gas-popover';
|
||||
import { EDIT_GAS_MODES } from '../../../../shared/constants/gas';
|
||||
import { GasFeeContextProvider } from '../../../contexts/gasFee';
|
||||
import ErrorMessage from '../../ui/error-message';
|
||||
import { TRANSACTION_TYPES } from '../../../../shared/constants/transaction';
|
||||
import Dialog from '../../ui/dialog';
|
||||
import EditGasFeePopover from '../edit-gas-fee-popover/edit-gas-fee-popover';
|
||||
import {
|
||||
ConfirmPageContainerHeader,
|
||||
ConfirmPageContainerContent,
|
||||
ConfirmPageContainerNavigation,
|
||||
} from '.';
|
||||
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
const EIP_1559_V2 = process.env.EIP_1559_V2;
|
||||
|
||||
export default class ConfirmPageContainer extends Component {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
@ -135,102 +140,107 @@ export default class ConfirmPageContainer extends Component {
|
||||
currentTransaction.txParams?.value === '0x0';
|
||||
|
||||
return (
|
||||
<div className="page-container">
|
||||
<ConfirmPageContainerNavigation
|
||||
totalTx={totalTx}
|
||||
positionOfCurrentTx={positionOfCurrentTx}
|
||||
nextTxId={nextTxId}
|
||||
prevTxId={prevTxId}
|
||||
showNavigation={showNavigation}
|
||||
onNextTx={(txId) => onNextTx(txId)}
|
||||
firstTx={firstTx}
|
||||
lastTx={lastTx}
|
||||
ofText={ofText}
|
||||
requestsWaitingText={requestsWaitingText}
|
||||
/>
|
||||
<ConfirmPageContainerHeader
|
||||
showEdit={showEdit}
|
||||
onEdit={() => onEdit()}
|
||||
showAccountInHeader={showAccountInHeader}
|
||||
accountAddress={fromAddress}
|
||||
>
|
||||
{hideSenderToRecipient ? null : (
|
||||
<SenderToRecipient
|
||||
senderName={fromName}
|
||||
senderAddress={fromAddress}
|
||||
recipientName={toName}
|
||||
recipientAddress={toAddress}
|
||||
recipientEns={toEns}
|
||||
recipientNickname={toNickname}
|
||||
<GasFeeContextProvider transaction={currentTransaction}>
|
||||
<div className="page-container">
|
||||
<ConfirmPageContainerNavigation
|
||||
totalTx={totalTx}
|
||||
positionOfCurrentTx={positionOfCurrentTx}
|
||||
nextTxId={nextTxId}
|
||||
prevTxId={prevTxId}
|
||||
showNavigation={showNavigation}
|
||||
onNextTx={(txId) => onNextTx(txId)}
|
||||
firstTx={firstTx}
|
||||
lastTx={lastTx}
|
||||
ofText={ofText}
|
||||
requestsWaitingText={requestsWaitingText}
|
||||
/>
|
||||
<ConfirmPageContainerHeader
|
||||
showEdit={showEdit}
|
||||
onEdit={() => onEdit()}
|
||||
showAccountInHeader={showAccountInHeader}
|
||||
accountAddress={fromAddress}
|
||||
>
|
||||
{hideSenderToRecipient ? null : (
|
||||
<SenderToRecipient
|
||||
senderName={fromName}
|
||||
senderAddress={fromAddress}
|
||||
recipientName={toName}
|
||||
recipientAddress={toAddress}
|
||||
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>
|
||||
<div>
|
||||
{showAddToAddressDialog && (
|
||||
<Dialog
|
||||
type="message"
|
||||
className="send__dialog"
|
||||
onClick={() => showAddToAddressBookModal()}
|
||||
{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}
|
||||
>
|
||||
{this.context.t('newAccountDetectedDialogMessage')}
|
||||
</Dialog>
|
||||
{unapprovedTxCount > 1 && (
|
||||
<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>
|
||||
{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}
|
||||
/>
|
||||
)}
|
||||
{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>
|
||||
</GasFeeContextProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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 { 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
|
||||
const SECOND_CUTOFF = 90;
|
||||
@ -49,6 +50,7 @@ export default function GasTiming({
|
||||
|
||||
const [customEstimatedTime, setCustomEstimatedTime] = useState(null);
|
||||
const t = useContext(I18nContext);
|
||||
const { estimateUsed } = useGasFeeContext();
|
||||
|
||||
// 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
|
||||
@ -94,12 +96,17 @@ export default function GasTiming({
|
||||
previousIsUnknownLow,
|
||||
]);
|
||||
|
||||
const unknownProcessingTimeText = (
|
||||
<>
|
||||
{t('editGasTooLow')}{' '}
|
||||
<InfoTooltip position="top" contentText={t('editGasTooLowTooltip')} />
|
||||
</>
|
||||
);
|
||||
let unknownProcessingTimeText;
|
||||
if (EIP_1559_V2) {
|
||||
unknownProcessingTimeText = t('editGasTooLow');
|
||||
} else {
|
||||
unknownProcessingTimeText = (
|
||||
<>
|
||||
{t('editGasTooLow')}{' '}
|
||||
<InfoTooltip position="top" contentText={t('editGasTooLowTooltip')} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
gasWarnings?.maxPriorityFee === GAS_FORM_ERRORS.MAX_PRIORITY_FEE_TOO_LOW ||
|
||||
@ -148,8 +155,9 @@ export default function GasTiming({
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
attitude = 'negative';
|
||||
|
||||
if (!EIP_1559_V2 || estimateUsed === 'low') {
|
||||
attitude = 'negative';
|
||||
}
|
||||
// If the user has chosen a value less than our low estimate,
|
||||
// calculate a potential wait time
|
||||
if (isUnknownLow) {
|
||||
@ -191,7 +199,8 @@ export default function GasTiming({
|
||||
<Typography
|
||||
variant={TYPOGRAPHY.H7}
|
||||
className={classNames('gas-timing', {
|
||||
[`gas-timing--${attitude}`]: attitude,
|
||||
[`gas-timing--${attitude}`]: attitude && !EIP_1559_V2,
|
||||
[`gas-timing--${attitude}-V2`]: attitude && EIP_1559_V2,
|
||||
})}
|
||||
>
|
||||
{text}
|
||||
|
@ -14,6 +14,11 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&--negative-V2 {
|
||||
color: $secondary-1;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.info-tooltip {
|
||||
display: inline-block;
|
||||
margin-inline-start: 4px;
|
||||
|
@ -84,9 +84,7 @@ export default class AccountDetailsModal extends Component {
|
||||
? this.context.t('blockExplorerView', [
|
||||
getURLHostName(rpcPrefs.blockExplorerUrl),
|
||||
])
|
||||
: this.context.t('viewOnEtherscan', [
|
||||
this.context.t('blockExplorerAccountAction'),
|
||||
])}
|
||||
: this.context.t('etherscanViewOn')}
|
||||
</Button>
|
||||
|
||||
{exportPrivateKeyFeatureEnabled ? (
|
||||
|
@ -8,13 +8,13 @@
|
||||
& &__button {
|
||||
margin-top: 17px;
|
||||
padding: 10px 22px;
|
||||
width: 286px;
|
||||
width: 284px;
|
||||
}
|
||||
|
||||
&__divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
margin: 19px 0 8px 0;
|
||||
margin: 16px 0 8px 0;
|
||||
background-color: $alto;
|
||||
}
|
||||
|
||||
|
@ -64,7 +64,7 @@ const accountModalStyle = {
|
||||
margin: '0 auto',
|
||||
},
|
||||
laptopModalStyle: {
|
||||
width: '360px',
|
||||
width: '335px',
|
||||
// top: 'calc(33% + 45px)',
|
||||
boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px',
|
||||
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 LedgerInstructionField from '../ledger-instruction-field';
|
||||
|
||||
import {
|
||||
ENVIRONMENT_TYPE_NOTIFICATION,
|
||||
MESSAGE_TYPE,
|
||||
} from '../../../../shared/constants/app';
|
||||
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
|
||||
import { MESSAGE_TYPE } from '../../../../shared/constants/app';
|
||||
import Identicon from '../../ui/identicon';
|
||||
import AccountListItem from '../account-list-item';
|
||||
import { conversionUtil } from '../../../../shared/modules/conversion.utils';
|
||||
@ -39,42 +35,13 @@ export default class SignatureRequestOriginal extends Component {
|
||||
domainMetadata: PropTypes.object,
|
||||
hardwareWalletRequiresConnection: PropTypes.bool,
|
||||
isLedgerWallet: PropTypes.bool,
|
||||
nativeCurrency: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
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 = () => {
|
||||
return (
|
||||
<div className="request-signature__header">
|
||||
@ -108,12 +75,12 @@ export default class SignatureRequestOriginal extends Component {
|
||||
};
|
||||
|
||||
renderBalance = () => {
|
||||
const { conversionRate } = this.props;
|
||||
const { conversionRate, nativeCurrency } = this.props;
|
||||
const {
|
||||
fromAccount: { balance },
|
||||
} = this.state;
|
||||
|
||||
const balanceInEther = conversionUtil(balance, {
|
||||
const balanceInBaseAsset = conversionUtil(balance, {
|
||||
fromNumericBase: 'hex',
|
||||
toNumericBase: 'dec',
|
||||
fromDenomination: 'WEI',
|
||||
@ -127,7 +94,7 @@ export default class SignatureRequestOriginal extends Component {
|
||||
{`${this.context.t('balance')}:`}
|
||||
</div>
|
||||
<div className="request-signature__balance-value">
|
||||
{`${balanceInEther} ETH`}
|
||||
{`${balanceInBaseAsset} ${nativeCurrency}`}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -300,7 +267,6 @@ export default class SignatureRequestOriginal extends Component {
|
||||
large
|
||||
className="request-signature__footer__cancel-button"
|
||||
onClick={async (event) => {
|
||||
this._removeBeforeUnload();
|
||||
await cancel(event);
|
||||
metricsEvent({
|
||||
eventOpts: {
|
||||
@ -325,7 +291,6 @@ export default class SignatureRequestOriginal extends Component {
|
||||
className="request-signature__footer__sign-button"
|
||||
disabled={hardwareWalletRequiresConnection}
|
||||
onClick={async (event) => {
|
||||
this._removeBeforeUnload();
|
||||
await sign(event);
|
||||
metricsEvent({
|
||||
eventOpts: {
|
||||
|
@ -13,7 +13,10 @@ import {
|
||||
import { getAccountByAddress } from '../../../helpers/utils/util';
|
||||
import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck';
|
||||
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';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
@ -34,6 +37,7 @@ function mapStateToProps(state, ownProps) {
|
||||
mostRecentOverviewPage: getMostRecentOverviewPage(state),
|
||||
hardwareWalletRequiresConnection,
|
||||
isLedgerWallet,
|
||||
nativeCurrency: getNativeCurrency(state),
|
||||
// not passed to component
|
||||
allAccounts: accountsWithSendEtherInfoSelector(state),
|
||||
domainMetadata: getDomainMetadata(state),
|
||||
|
@ -1,12 +1,10 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
|
||||
import Identicon from '../../ui/identicon';
|
||||
import LedgerInstructionField from '../ledger-instruction-field';
|
||||
import Header from './signature-request-header';
|
||||
import Footer from './signature-request-footer';
|
||||
import Message from './signature-request-message';
|
||||
import { ENVIRONMENT_TYPE_NOTIFICATION } from './signature-request.constants';
|
||||
|
||||
export default class SignatureRequest extends PureComponent {
|
||||
static propTypes = {
|
||||
@ -17,7 +15,6 @@ export default class SignatureRequest extends PureComponent {
|
||||
name: PropTypes.string,
|
||||
}).isRequired,
|
||||
isLedgerWallet: PropTypes.bool,
|
||||
clearConfirmTransaction: PropTypes.func.isRequired,
|
||||
cancel: PropTypes.func.isRequired,
|
||||
sign: PropTypes.func.isRequired,
|
||||
hardwareWalletRequiresConnection: PropTypes.func.isRequired,
|
||||
@ -28,33 +25,6 @@ export default class SignatureRequest extends PureComponent {
|
||||
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) {
|
||||
return `${wallet.slice(0, 8)}...${wallet.slice(
|
||||
wallet.length - 8,
|
||||
@ -79,7 +49,6 @@ export default class SignatureRequest extends PureComponent {
|
||||
const { metricsEvent } = this.context;
|
||||
|
||||
const onSign = (event) => {
|
||||
window.removeEventListener('beforeunload', this._beforeUnload);
|
||||
sign(event);
|
||||
metricsEvent({
|
||||
eventOpts: {
|
||||
@ -95,7 +64,6 @@ export default class SignatureRequest extends PureComponent {
|
||||
};
|
||||
|
||||
const onCancel = (event) => {
|
||||
window.removeEventListener('beforeunload', this._beforeUnload);
|
||||
cancel(event);
|
||||
metricsEvent({
|
||||
eventOpts: {
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck';
|
||||
import {
|
||||
accountsWithSendEtherInfoSelector,
|
||||
doesAddressRequireLedgerHidConnection,
|
||||
@ -28,12 +27,6 @@ function mapStateToProps(state, ownProps) {
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
|
||||
};
|
||||
}
|
||||
|
||||
function mergeProps(stateProps, dispatchProps, ownProps) {
|
||||
const {
|
||||
allAccounts,
|
||||
@ -83,8 +76,4 @@ function mergeProps(stateProps, dispatchProps, ownProps) {
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
mergeProps,
|
||||
)(SignatureRequest);
|
||||
export default connect(mapStateToProps, null, mergeProps)(SignatureRequest);
|
||||
|
@ -15,4 +15,55 @@
|
||||
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 { 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 { 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 }) {
|
||||
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 (
|
||||
<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',
|
||||
useIcon = false,
|
||||
iconFillColor = '',
|
||||
roundedButtons,
|
||||
}) {
|
||||
const actionableMessageClassName = classnames(
|
||||
'actionable-message',
|
||||
@ -35,6 +36,9 @@ export default function ActionableMessage({
|
||||
{ 'actionable-message--with-icon': useIcon },
|
||||
);
|
||||
|
||||
const onlyOneAction =
|
||||
(primaryAction && !secondaryAction) || (secondaryAction && !primaryAction);
|
||||
|
||||
return (
|
||||
<div className={actionableMessageClassName}>
|
||||
{useIcon ? <InfoTooltipIcon fillColor={iconFillColor} /> : null}
|
||||
@ -47,12 +51,19 @@ export default function ActionableMessage({
|
||||
)}
|
||||
<div className="actionable-message__message">{message}</div>
|
||||
{(primaryAction || secondaryAction) && (
|
||||
<div className="actionable-message__actions">
|
||||
<div
|
||||
className={classnames('actionable-message__actions', {
|
||||
'actionable-message__actions--single': onlyOneAction,
|
||||
})}
|
||||
>
|
||||
{primaryAction && (
|
||||
<button
|
||||
className={classnames(
|
||||
'actionable-message__action',
|
||||
'actionable-message__action--primary',
|
||||
{
|
||||
'actionable-message__action--rounded': roundedButtons,
|
||||
},
|
||||
)}
|
||||
onClick={primaryAction.onClick}
|
||||
>
|
||||
@ -64,6 +75,9 @@ export default function ActionableMessage({
|
||||
className={classnames(
|
||||
'actionable-message__action',
|
||||
'actionable-message__action--secondary',
|
||||
{
|
||||
'actionable-message__action--rounded': roundedButtons,
|
||||
},
|
||||
)}
|
||||
onClick={secondaryAction.onClick}
|
||||
>
|
||||
@ -92,4 +106,5 @@ ActionableMessage.propTypes = {
|
||||
infoTooltipText: PropTypes.string,
|
||||
useIcon: PropTypes.bool,
|
||||
iconFillColor: PropTypes.string,
|
||||
roundedButtons: PropTypes.bool,
|
||||
};
|
||||
|
@ -38,14 +38,22 @@
|
||||
&__actions {
|
||||
display: flex;
|
||||
width: 80%;
|
||||
justify-content: space-evenly;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
color: $Blue-600;
|
||||
|
||||
&--single {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__action {
|
||||
font-weight: bold;
|
||||
|
||||
&--rounded {
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&__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