mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Merge remote-tracking branch 'origin/develop' into master-sync
This commit is contained in:
commit
5a8ca9d16e
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,6 +43,9 @@
|
||||
"activityLog": {
|
||||
"message": "activity log"
|
||||
},
|
||||
"add": {
|
||||
"message": "Add"
|
||||
},
|
||||
"addANetwork": {
|
||||
"message": "Add a network"
|
||||
},
|
||||
@ -100,6 +103,9 @@
|
||||
"addToken": {
|
||||
"message": "Add Token"
|
||||
},
|
||||
"address": {
|
||||
"message": "Address"
|
||||
},
|
||||
"addressBookIcon": {
|
||||
"message": "Address book icon"
|
||||
},
|
||||
@ -167,6 +173,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 +563,9 @@
|
||||
"currentlyUnavailable": {
|
||||
"message": "Unavailable on this network"
|
||||
},
|
||||
"custom": {
|
||||
"message": "Advanced"
|
||||
},
|
||||
"customGas": {
|
||||
"message": "Customize Gas"
|
||||
},
|
||||
@ -561,6 +578,13 @@
|
||||
"customToken": {
|
||||
"message": "Custom Token"
|
||||
},
|
||||
"dappSuggested": {
|
||||
"message": "Site suggested"
|
||||
},
|
||||
"dappSuggestedTooltip": {
|
||||
"message": "$1 has recommended this price.",
|
||||
"description": "$1 represents the Dapp's origin"
|
||||
},
|
||||
"data": {
|
||||
"message": "Data"
|
||||
},
|
||||
@ -891,6 +915,9 @@
|
||||
"etherscanView": {
|
||||
"message": "View account on Etherscan"
|
||||
},
|
||||
"etherscanViewOn": {
|
||||
"message": "View on Etherscan"
|
||||
},
|
||||
"expandView": {
|
||||
"message": "Expand view"
|
||||
},
|
||||
@ -1002,6 +1029,9 @@
|
||||
"gasPriceInfoTooltipContent": {
|
||||
"message": "Gas price specifies the amount of Ether you are willing to pay for each unit of gas."
|
||||
},
|
||||
"gasPriceLabel": {
|
||||
"message": "Gas price"
|
||||
},
|
||||
"gasTimingMinutes": {
|
||||
"message": "$1 minutes",
|
||||
"description": "$1 represents a number of minutes"
|
||||
@ -1100,9 +1130,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 +1153,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 +1372,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 +1391,18 @@
|
||||
"max": {
|
||||
"message": "Max"
|
||||
},
|
||||
"maxBaseFee": {
|
||||
"message": "Max base fee"
|
||||
},
|
||||
"maxFee": {
|
||||
"message": "Max fee"
|
||||
},
|
||||
"maxPriorityFee": {
|
||||
"message": "Max priority fee"
|
||||
},
|
||||
"medium": {
|
||||
"message": "Market"
|
||||
},
|
||||
"memo": {
|
||||
"message": "memo"
|
||||
},
|
||||
@ -1560,6 +1608,9 @@
|
||||
"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"
|
||||
},
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,14 @@ import {
|
||||
METAMETRICS_BACKGROUND_PAGE_OBJECT,
|
||||
} from '../../../shared/constants/metametrics';
|
||||
|
||||
const defaultCaptureException = (err) => {
|
||||
// throw error on clean stack so its captured by platform integrations (eg sentry)
|
||||
// but does not interupt the call stack
|
||||
setTimeout(() => {
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {import('../../../shared/constants/metametrics').MetaMetricsContext} MetaMetricsContext
|
||||
* @typedef {import('../../../shared/constants/metametrics').MetaMetricsEventPayload} MetaMetricsEventPayload
|
||||
@ -51,7 +59,9 @@ export default class MetaMetricsController {
|
||||
version,
|
||||
environment,
|
||||
initState,
|
||||
captureException = defaultCaptureException,
|
||||
}) {
|
||||
this._captureException = captureException;
|
||||
const prefState = preferencesStore.getState();
|
||||
this.chainId = getCurrentChainId();
|
||||
this.network = getNetworkIdentifier();
|
||||
@ -258,32 +268,52 @@ export default class MetaMetricsController {
|
||||
* view
|
||||
*/
|
||||
trackPage({ name, params, environmentType, page, referrer }, options) {
|
||||
if (this.state.participateInMetaMetrics === false) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (this.state.participateInMetaMetrics === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.participateInMetaMetrics === null && !options?.isOptInPath) {
|
||||
return;
|
||||
if (
|
||||
this.state.participateInMetaMetrics === null &&
|
||||
!options?.isOptInPath
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const { metaMetricsId } = this.state;
|
||||
const idTrait = metaMetricsId ? 'userId' : 'anonymousId';
|
||||
const idValue = metaMetricsId ?? METAMETRICS_ANONYMOUS_ID;
|
||||
this.segment.page({
|
||||
[idTrait]: idValue,
|
||||
name,
|
||||
properties: {
|
||||
params,
|
||||
locale: this.locale,
|
||||
network: this.network,
|
||||
chain_id: this.chainId,
|
||||
environment_type: environmentType,
|
||||
},
|
||||
context: this._buildContext(referrer, page),
|
||||
});
|
||||
} catch (err) {
|
||||
this._captureException(err);
|
||||
}
|
||||
const { metaMetricsId } = this.state;
|
||||
const idTrait = metaMetricsId ? 'userId' : 'anonymousId';
|
||||
const idValue = metaMetricsId ?? METAMETRICS_ANONYMOUS_ID;
|
||||
this.segment.page({
|
||||
[idTrait]: idValue,
|
||||
name,
|
||||
properties: {
|
||||
params,
|
||||
locale: this.locale,
|
||||
network: this.network,
|
||||
chain_id: this.chainId,
|
||||
environment_type: environmentType,
|
||||
},
|
||||
context: this._buildContext(referrer, page),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* track a metametrics event, performing necessary payload manipulation and
|
||||
* submits a metametrics event, not waiting for it to complete or allowing its error to bubble up
|
||||
* @param {MetaMetricsEventPayload} payload - details of the event
|
||||
* @param {MetaMetricsEventOptions} [options] - options for handling/routing the event
|
||||
*/
|
||||
trackEvent(payload, options) {
|
||||
// validation is not caught and handled
|
||||
this.validatePayload(payload);
|
||||
this.submitEvent(payload, options).catch((err) =>
|
||||
this._captureException(err),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* submits (or queues for submission) a metametrics event, performing necessary payload manipulation and
|
||||
* routing the event to the appropriate segment source. Will split events
|
||||
* with sensitiveProperties into two events, tracking the sensitiveProperties
|
||||
* with the anonymousId only.
|
||||
@ -291,21 +321,8 @@ export default class MetaMetricsController {
|
||||
* @param {MetaMetricsEventOptions} [options] - options for handling/routing the event
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async trackEvent(payload, options) {
|
||||
// event and category are required fields for all payloads
|
||||
if (!payload.event || !payload.category) {
|
||||
throw new Error(
|
||||
`Must specify event and category. Event was: ${
|
||||
payload.event
|
||||
}. Category was: ${payload.category}. Payload keys were: ${Object.keys(
|
||||
payload,
|
||||
)}. ${
|
||||
typeof payload.properties === 'object'
|
||||
? `Payload property keys were: ${Object.keys(payload.properties)}`
|
||||
: ''
|
||||
}`,
|
||||
);
|
||||
}
|
||||
async submitEvent(payload, options) {
|
||||
this.validatePayload(payload);
|
||||
|
||||
if (!this.state.participateInMetaMetrics && !options?.isOptIn) {
|
||||
return;
|
||||
@ -345,4 +362,25 @@ export default class MetaMetricsController {
|
||||
|
||||
await Promise.all(events);
|
||||
}
|
||||
|
||||
/**
|
||||
* validates a metametrics event
|
||||
* @param {MetaMetricsEventPayload} payload - details of the event
|
||||
*/
|
||||
validatePayload(payload) {
|
||||
// event and category are required fields for all payloads
|
||||
if (!payload.event || !payload.category) {
|
||||
throw new Error(
|
||||
`Must specify event and category. Event was: ${
|
||||
payload.event
|
||||
}. Category was: ${payload.category}. Payload keys were: ${Object.keys(
|
||||
payload,
|
||||
)}. ${
|
||||
typeof payload.properties === 'object'
|
||||
? `Payload property keys were: ${Object.keys(payload.properties)}`
|
||||
: ''
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -196,14 +196,14 @@ describe('MetaMetricsController', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackEvent', function () {
|
||||
describe('submitEvent', function () {
|
||||
it('should not track an event if user is not participating in metametrics', function () {
|
||||
const mock = sinon.mock(segment);
|
||||
const metaMetricsController = getMetaMetricsController({
|
||||
participateInMetaMetrics: false,
|
||||
});
|
||||
mock.expects('track').never();
|
||||
metaMetricsController.trackEvent({
|
||||
metaMetricsController.submitEvent({
|
||||
event: 'Fake Event',
|
||||
category: 'Unit Test',
|
||||
properties: {
|
||||
@ -230,7 +230,7 @@ describe('MetaMetricsController', function () {
|
||||
...DEFAULT_EVENT_PROPERTIES,
|
||||
},
|
||||
});
|
||||
metaMetricsController.trackEvent(
|
||||
metaMetricsController.submitEvent(
|
||||
{
|
||||
event: 'Fake Event',
|
||||
category: 'Unit Test',
|
||||
@ -260,7 +260,7 @@ describe('MetaMetricsController', function () {
|
||||
...DEFAULT_EVENT_PROPERTIES,
|
||||
},
|
||||
});
|
||||
metaMetricsController.trackEvent(
|
||||
metaMetricsController.submitEvent(
|
||||
{
|
||||
event: 'Fake Event',
|
||||
category: 'Unit Test',
|
||||
@ -289,7 +289,7 @@ describe('MetaMetricsController', function () {
|
||||
...DEFAULT_EVENT_PROPERTIES,
|
||||
},
|
||||
});
|
||||
metaMetricsController.trackEvent(
|
||||
metaMetricsController.submitEvent(
|
||||
{
|
||||
event: 'Fake Event',
|
||||
category: 'Unit Test',
|
||||
@ -317,7 +317,7 @@ describe('MetaMetricsController', function () {
|
||||
...DEFAULT_EVENT_PROPERTIES,
|
||||
},
|
||||
});
|
||||
metaMetricsController.trackEvent({
|
||||
metaMetricsController.submitEvent({
|
||||
event: 'Fake Event',
|
||||
category: 'Unit Test',
|
||||
properties: {
|
||||
@ -331,7 +331,7 @@ describe('MetaMetricsController', function () {
|
||||
const metaMetricsController = getMetaMetricsController();
|
||||
const flushStub = sinon.stub(segment, 'flush');
|
||||
const flushCalled = waitUntilCalled(flushStub, segment);
|
||||
metaMetricsController.trackEvent(
|
||||
metaMetricsController.submitEvent(
|
||||
{
|
||||
event: 'Fake Event',
|
||||
category: 'Unit Test',
|
||||
@ -344,13 +344,13 @@ describe('MetaMetricsController', function () {
|
||||
it('should throw if event or category not provided', function () {
|
||||
const metaMetricsController = getMetaMetricsController();
|
||||
assert.rejects(
|
||||
() => metaMetricsController.trackEvent({ event: 'test' }),
|
||||
() => metaMetricsController.submitEvent({ event: 'test' }),
|
||||
/Must specify event and category\./u,
|
||||
'must specify category',
|
||||
);
|
||||
|
||||
assert.rejects(
|
||||
() => metaMetricsController.trackEvent({ category: 'test' }),
|
||||
() => metaMetricsController.submitEvent({ category: 'test' }),
|
||||
/Must specify event and category\./u,
|
||||
'must specify event',
|
||||
);
|
||||
@ -360,7 +360,7 @@ describe('MetaMetricsController', function () {
|
||||
const metaMetricsController = getMetaMetricsController();
|
||||
assert.rejects(
|
||||
() =>
|
||||
metaMetricsController.trackEvent(
|
||||
metaMetricsController.submitEvent(
|
||||
{
|
||||
event: 'Fake Event',
|
||||
category: 'Unit Test',
|
||||
@ -375,7 +375,7 @@ describe('MetaMetricsController', function () {
|
||||
it('should track sensitiveProperties in a separate, anonymous event', function () {
|
||||
const metaMetricsController = getMetaMetricsController();
|
||||
const spy = sinon.spy(segment, 'track');
|
||||
metaMetricsController.trackEvent({
|
||||
metaMetricsController.submitEvent({
|
||||
event: 'Fake Event',
|
||||
category: 'Unit Test',
|
||||
sensitiveProperties: { foo: 'bar' },
|
||||
|
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -280,7 +280,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) &&
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -217,7 +218,18 @@ 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');
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -238,7 +239,18 @@ 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');
|
||||
}
|
||||
|
||||
|
@ -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 { captureException } from '@sentry/browser';
|
||||
import {
|
||||
AddressBookController,
|
||||
ApprovalController,
|
||||
@ -190,6 +191,7 @@ export default class MetamaskController extends EventEmitter {
|
||||
version: this.platform.getVersion(),
|
||||
environment: process.env.METAMASK_ENVIRONMENT,
|
||||
initState: initState.MetaMetricsController,
|
||||
captureException,
|
||||
});
|
||||
|
||||
const gasFeeMessenger = this.controllerMessenger.getRestricted({
|
||||
@ -524,14 +526,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({
|
||||
@ -922,6 +943,10 @@ export default class MetamaskController extends EventEmitter {
|
||||
this.preferencesController.setDismissSeedBackUpReminder,
|
||||
this.preferencesController,
|
||||
),
|
||||
setAdvancedGasFee: nodeify(
|
||||
preferencesController.setAdvancedGasFee,
|
||||
preferencesController,
|
||||
),
|
||||
|
||||
// AddressController
|
||||
setAddressBook: nodeify(
|
||||
|
@ -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;
|
||||
}
|
||||
|
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
|
||||
}
|
||||
@ -1555,11 +1556,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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
4772
lavamoat/browserify/flask/policy.json
Normal file
4772
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
4772
lavamoat/browserify/main/policy.json
Normal file
4772
lavamoat/browserify/main/policy.json
Normal file
File diff suppressed because it is too large
Load Diff
18
package.json
18
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": {
|
||||
@ -135,10 +137,10 @@
|
||||
"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",
|
||||
"eth-lattice-keyring": "^0.4.0",
|
||||
"eth-query": "^2.1.2",
|
||||
"eth-rpc-errors": "^4.0.2",
|
||||
"eth-sig-util": "^3.0.0",
|
||||
|
@ -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(
|
||||
|
@ -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: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -4,6 +4,7 @@ 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';
|
||||
@ -135,102 +136,104 @@ 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 && (
|
||||
<EditGasPopover
|
||||
mode={EDIT_GAS_MODES.MODIFY_IN_PLACE}
|
||||
onClose={handleCloseEditGas}
|
||||
transaction={currentTransaction}
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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 { estimateToUse } = 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 || estimateToUse === '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',
|
||||
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,11 +2,94 @@ import React, { useContext } 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';
|
||||
|
||||
const GasLevelIconMap = {
|
||||
low: '🐢',
|
||||
medium: '🦊',
|
||||
high: '🦍',
|
||||
dappSuggested: '🌐',
|
||||
custom: '⚙',
|
||||
};
|
||||
|
||||
export default function TransactionDetail({ rows = [], onEdit }) {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
const EIP_1559_V2 = process.env.EIP_1559_V2;
|
||||
|
||||
const t = useContext(I18nContext);
|
||||
const {
|
||||
estimateToUse,
|
||||
gasLimit,
|
||||
gasPrice,
|
||||
isUsingDappSuggestedGasFees,
|
||||
maxFeePerGas,
|
||||
maxPriorityFeePerGas,
|
||||
transaction,
|
||||
supportsEIP1559,
|
||||
} = useGasFeeContext();
|
||||
const estimateUsed = isUsingDappSuggestedGasFees
|
||||
? 'dappSuggested'
|
||||
: estimateToUse;
|
||||
|
||||
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">
|
||||
{`${GasLevelIconMap[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>
|
||||
{supportsEIP1559 ? (
|
||||
<>
|
||||
<Typography fontSize="12px">
|
||||
<b>{t('maxBaseFee')}</b>
|
||||
{maxFeePerGas}
|
||||
</Typography>
|
||||
<Typography fontSize="12px">
|
||||
<b>{t('maxPriorityFee')}</b>
|
||||
{maxPriorityFeePerGas}
|
||||
</Typography>
|
||||
</>
|
||||
) : (
|
||||
<Typography fontSize="12px">
|
||||
<b>{t('gasPriceLabel')}</b>
|
||||
{gasPrice}
|
||||
</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();
|
||||
});
|
||||
});
|
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,
|
||||
};
|
11
ui/components/ui/card/card.test.js
Normal file
11
ui/components/ui/card/card.test.js
Normal file
@ -0,0 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import Card from '.';
|
||||
|
||||
describe('Card', () => {
|
||||
it('should render the Card without crashing', () => {
|
||||
const { getByText } = render(<Card>Card content</Card>);
|
||||
|
||||
expect(getByText('Card content')).toBeDefined();
|
||||
});
|
||||
});
|
@ -1 +1 @@
|
||||
export { default } from './card.component';
|
||||
export { default } from './card';
|
||||
|
@ -1,11 +0,0 @@
|
||||
.card {
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
||||
padding: 8px;
|
||||
|
||||
&__title {
|
||||
border-bottom: 1px solid #d8d8d8;
|
||||
padding-bottom: 4px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
15
ui/components/ui/chip/README.mdx
Normal file
15
ui/components/ui/chip/README.mdx
Normal file
@ -0,0 +1,15 @@
|
||||
import { Story, Canvas, ArgsTable } from '@storybook/addon-docs';
|
||||
|
||||
import Chip from '.';
|
||||
|
||||
# Chip
|
||||
|
||||
Chips are compact elements that represent an input, status, or action.
|
||||
|
||||
<Canvas>
|
||||
<Story id="ui-components-ui-chip-chip-stories-js--default-story" />
|
||||
</Canvas>
|
||||
|
||||
## Component API
|
||||
|
||||
<ArgsTable of={Chip} />
|
@ -5,6 +5,7 @@ import { COLORS } from '../../../helpers/constants/design-system';
|
||||
import Chip from '.';
|
||||
|
||||
export function ChipWithInput({
|
||||
dataTestId,
|
||||
className,
|
||||
borderColor = COLORS.UI1,
|
||||
inputValue,
|
||||
@ -17,6 +18,7 @@ export function ChipWithInput({
|
||||
>
|
||||
{setInputValue && (
|
||||
<input
|
||||
data-testid={dataTestId}
|
||||
type="text"
|
||||
className="chip__input"
|
||||
onChange={(e) => {
|
||||
@ -30,6 +32,7 @@ export function ChipWithInput({
|
||||
}
|
||||
|
||||
ChipWithInput.propTypes = {
|
||||
dataTestId: PropTypes.string,
|
||||
borderColor: PropTypes.oneOf(Object.values(COLORS)),
|
||||
className: PropTypes.string,
|
||||
inputValue: PropTypes.string,
|
||||
|
@ -6,9 +6,11 @@ import Typography from '../typography';
|
||||
import { COLORS, TYPOGRAPHY } from '../../../helpers/constants/design-system';
|
||||
|
||||
export default function Chip({
|
||||
dataTestId,
|
||||
className,
|
||||
children,
|
||||
borderColor = COLORS.UI1,
|
||||
backgroundColor,
|
||||
label,
|
||||
labelProps = {},
|
||||
leftIcon,
|
||||
@ -25,12 +27,14 @@ export default function Chip({
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid={dataTestId}
|
||||
onClick={onClick}
|
||||
onKeyPress={onKeyPress}
|
||||
className={classnames(className, 'chip', {
|
||||
'chip--with-left-icon': Boolean(leftIcon),
|
||||
'chip--with-right-icon': Boolean(rightIcon),
|
||||
[`chip--${borderColor}`]: true,
|
||||
[`chip--border-color-${borderColor}`]: true,
|
||||
[`chip--background-color-${backgroundColor}`]: true,
|
||||
})}
|
||||
role={isInteractive ? 'button' : undefined}
|
||||
tabIndex={isInteractive ? 0 : undefined}
|
||||
@ -53,16 +57,46 @@ export default function Chip({
|
||||
}
|
||||
|
||||
Chip.propTypes = {
|
||||
/**
|
||||
* Data test id used for testing of the Chip component
|
||||
*/
|
||||
dataTestId: PropTypes.string,
|
||||
/**
|
||||
* The border color of the Chip
|
||||
*/
|
||||
borderColor: PropTypes.oneOf(Object.values(COLORS)),
|
||||
/**
|
||||
* The background color of the Chip component
|
||||
*/
|
||||
backgroundColor: PropTypes.oneOf(Object.values(COLORS)),
|
||||
/**
|
||||
* The label of the Chip component has a default typography variant of h6 and is a span html element
|
||||
*/
|
||||
label: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
/**
|
||||
* The label props of the component. Most Typography props can be used
|
||||
*/
|
||||
labelProps: PropTypes.shape({
|
||||
...omit(Typography.propTypes, ['children', 'className']),
|
||||
}),
|
||||
/**
|
||||
* Children will replace the label of the Chip component.
|
||||
*/
|
||||
children: PropTypes.node,
|
||||
/**
|
||||
* An icon component that can be passed to appear on the left of the label
|
||||
*/
|
||||
leftIcon: PropTypes.node,
|
||||
/**
|
||||
* An icon component that can be passed to appear on the right of the label
|
||||
*/
|
||||
rightIcon: PropTypes.node,
|
||||
/**
|
||||
* The className of the Chip
|
||||
*/
|
||||
className: PropTypes.string,
|
||||
/**
|
||||
* The onClick handler to be passed to the Chip component
|
||||
*/
|
||||
onClick: PropTypes.func,
|
||||
inputValue: PropTypes.string,
|
||||
setInputValue: PropTypes.func,
|
||||
};
|
||||
|
@ -18,9 +18,12 @@
|
||||
}
|
||||
|
||||
@each $variant, $color in design-system.$color-map {
|
||||
&--#{$variant} {
|
||||
&--border-color-#{$variant} {
|
||||
border-color: $color;
|
||||
}
|
||||
&--background-color-#{$variant} {
|
||||
background-color: $color;
|
||||
}
|
||||
}
|
||||
|
||||
&--with-left-icon,
|
||||
|
@ -1,51 +1,111 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { select, text } from '@storybook/addon-knobs';
|
||||
|
||||
import { COLORS, TYPOGRAPHY } from '../../../helpers/constants/design-system';
|
||||
|
||||
import ApproveIcon from '../icon/approve-icon.component';
|
||||
import Identicon from '../identicon/identicon.component';
|
||||
import { ChipWithInput } from './chip-with-input';
|
||||
|
||||
import README from './README.mdx';
|
||||
|
||||
import Chip from '.';
|
||||
|
||||
export default {
|
||||
title: 'Chip',
|
||||
title: 'UI/Chip',
|
||||
id: __filename,
|
||||
component: Chip,
|
||||
parameters: {
|
||||
docs: {
|
||||
page: README,
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
leftIcon: {
|
||||
control: {
|
||||
type: 'select',
|
||||
},
|
||||
options: ['ApproveIcon'],
|
||||
mapping: {
|
||||
ApproveIcon: <ApproveIcon size={24} color="#4cd964" />,
|
||||
},
|
||||
},
|
||||
rightIcon: {
|
||||
control: {
|
||||
type: 'select',
|
||||
},
|
||||
options: ['Identicon'],
|
||||
mapping: {
|
||||
Identicon: (
|
||||
<Identicon
|
||||
address="0x5CfE73b6021E818B776b421B1c4Db2474086a7e1"
|
||||
diameter={25}
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
label: {
|
||||
control: 'text',
|
||||
},
|
||||
labelProps: {
|
||||
color: {
|
||||
control: {
|
||||
type: 'select',
|
||||
},
|
||||
options: Object.values(COLORS),
|
||||
},
|
||||
variant: {
|
||||
color: {
|
||||
control: {
|
||||
type: 'select',
|
||||
},
|
||||
options: Object.values(TYPOGRAPHY),
|
||||
},
|
||||
},
|
||||
},
|
||||
borderColor: {
|
||||
control: {
|
||||
type: 'select',
|
||||
},
|
||||
options: Object.values(COLORS),
|
||||
},
|
||||
backgroundColor: {
|
||||
control: {
|
||||
type: 'select',
|
||||
},
|
||||
options: Object.values(COLORS),
|
||||
},
|
||||
children: {
|
||||
control: 'text',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Plain = ({
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
label = 'Hello',
|
||||
borderColor = COLORS.UI1,
|
||||
fontColor = COLORS.BLACK,
|
||||
}) => (
|
||||
<Chip
|
||||
leftIcon={leftIcon}
|
||||
rightIcon={rightIcon}
|
||||
label={text('label', label)}
|
||||
labelProps={{
|
||||
color: select('color', COLORS, fontColor),
|
||||
variant: select('typography', TYPOGRAPHY, TYPOGRAPHY.H6),
|
||||
}}
|
||||
borderColor={select('borderColor', COLORS, borderColor)}
|
||||
/>
|
||||
);
|
||||
export const DefaultStory = (args) => <Chip {...args} />;
|
||||
|
||||
DefaultStory.storyName = 'Default';
|
||||
|
||||
DefaultStory.args = {
|
||||
label: 'Chip',
|
||||
borderColor: COLORS.UI3,
|
||||
backgroundColor: COLORS.UI1,
|
||||
labelProps: {
|
||||
color: COLORS.BLACK,
|
||||
variant: TYPOGRAPHY.H6,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithLeftIcon = () => (
|
||||
<Plain
|
||||
<Chip
|
||||
label="Done!"
|
||||
borderColor={COLORS.SUCCESS3}
|
||||
fontColor={COLORS.SUCCESS3}
|
||||
leftIcon={<ApproveIcon size={24} color="#4cd964" />}
|
||||
/>
|
||||
);
|
||||
|
||||
export const WithRightIcon = () => (
|
||||
<Plain
|
||||
<Chip
|
||||
label="0x5CfE73b6021E818B776b421B1c4Db2474086a7e1"
|
||||
borderColor={COLORS.UI4}
|
||||
fontColor={COLORS.UI4}
|
||||
rightIcon={
|
||||
<Identicon
|
||||
address="0x5CfE73b6021E818B776b421B1c4Db2474086a7e1"
|
||||
@ -56,10 +116,9 @@ export const WithRightIcon = () => (
|
||||
);
|
||||
|
||||
export const WithBothIcons = () => (
|
||||
<Plain
|
||||
<Chip
|
||||
label="Account 1"
|
||||
borderColor={COLORS.UI4}
|
||||
fontColor={COLORS.UI4}
|
||||
rightIcon={
|
||||
<svg
|
||||
width="10"
|
||||
@ -82,13 +141,17 @@ export const WithBothIcons = () => (
|
||||
}
|
||||
/>
|
||||
);
|
||||
export const WithInput = () => {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
export const WithInput = (args) => {
|
||||
const [inputValue, setInputValue] = useState('Chip with input');
|
||||
return (
|
||||
<ChipWithInput
|
||||
{...args}
|
||||
inputValue={inputValue}
|
||||
setInputValue={setInputValue}
|
||||
borderColor={select('borderColor', COLORS, COLORS.UI3)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
WithInput.args = {
|
||||
borderColor: COLORS.UI3,
|
||||
};
|
||||
|
@ -16,6 +16,7 @@ import NumericInput from '../numeric-input/numeric-input.component';
|
||||
import InfoTooltip from '../info-tooltip/info-tooltip';
|
||||
|
||||
export default function FormField({
|
||||
dataTestId,
|
||||
titleText,
|
||||
titleUnit,
|
||||
tooltipText,
|
||||
@ -94,6 +95,7 @@ export default function FormField({
|
||||
type={password ? 'password' : 'text'}
|
||||
autoFocus={autoFocus}
|
||||
disabled={disabled}
|
||||
data-testid={dataTestId}
|
||||
/>
|
||||
)}
|
||||
{error && (
|
||||
@ -111,6 +113,7 @@ export default function FormField({
|
||||
}
|
||||
|
||||
FormField.propTypes = {
|
||||
dataTestId: PropTypes.string,
|
||||
titleText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
titleUnit: PropTypes.string,
|
||||
tooltipText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
|
@ -25,10 +25,41 @@
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
|
||||
&__address-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
&__tooltip-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
.qr-code__copy-icon__svg {
|
||||
fill: $primary-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__address {
|
||||
@include H7;
|
||||
|
||||
background-color: $ui-1;
|
||||
padding: 12px;
|
||||
background-color: $Grey-000;
|
||||
width: 76%;
|
||||
padding: 8px 12px;
|
||||
word-break: break-all;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__copy-icon {
|
||||
height: 13px;
|
||||
padding: 17px 0;
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
|
||||
&__svg {
|
||||
fill: $ui-5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,11 @@ import React from 'react';
|
||||
import qrCode from 'qrcode-generator';
|
||||
import { connect } from 'react-redux';
|
||||
import { isHexPrefixed } from 'ethereumjs-util';
|
||||
import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
|
||||
import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils';
|
||||
import Tooltip from '../tooltip';
|
||||
import CopyIcon from '../icon/copy-icon.component';
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||
|
||||
export default connect(mapStateToProps)(QrCodeView);
|
||||
|
||||
@ -22,6 +26,8 @@ function QrCodeView(props) {
|
||||
const address = `${
|
||||
isHexPrefixed(data) ? 'ethereum:' : ''
|
||||
}${toChecksumHexAddress(data)}`;
|
||||
const [copied, handleCopy] = useCopyToClipboard();
|
||||
const t = useI18nContext();
|
||||
const qrImage = qrCode(4, 'M');
|
||||
qrImage.addData(address);
|
||||
qrImage.make();
|
||||
@ -50,7 +56,23 @@ function QrCodeView(props) {
|
||||
__html: qrImage.createTableTag(4),
|
||||
}}
|
||||
/>
|
||||
<div className="qr-code__address">{toChecksumHexAddress(data)}</div>
|
||||
<Tooltip
|
||||
wrapperClassName="qr-code__address-container__tooltip-wrapper"
|
||||
position="bottom"
|
||||
title={copied ? t('copiedExclamation') : t('copyToClipboard')}
|
||||
>
|
||||
<div
|
||||
className="qr-code__address-container"
|
||||
onClick={() => {
|
||||
handleCopy(toChecksumHexAddress(data));
|
||||
}}
|
||||
>
|
||||
<div className="qr-code__address">{toChecksumHexAddress(data)}</div>
|
||||
<div className="qr-code__copy-icon">
|
||||
<CopyIcon size={11} className="qr-code__copy-icon__svg" color="" />
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -75,11 +75,12 @@ export default class TokenInput extends PureComponent {
|
||||
return Number(decimalValueString) ? decimalValueString : '';
|
||||
}
|
||||
|
||||
handleChange = (decimalValue) => {
|
||||
handleChange = (decimalValue, applyDecimals = false) => {
|
||||
const { token: { decimals } = {}, onChange } = this.props;
|
||||
|
||||
let newDecimalValue = decimalValue;
|
||||
if (decimals) {
|
||||
|
||||
if (decimals && applyDecimals) {
|
||||
newDecimalValue = parseFloat(decimalValue).toFixed(decimals);
|
||||
}
|
||||
|
||||
@ -94,6 +95,10 @@ export default class TokenInput extends PureComponent {
|
||||
onChange(hexValue);
|
||||
};
|
||||
|
||||
handleBlur = (decimalValue) => {
|
||||
this.handleChange(decimalValue, true);
|
||||
};
|
||||
|
||||
renderConversionComponent() {
|
||||
const {
|
||||
tokenExchangeRates,
|
||||
@ -155,6 +160,7 @@ export default class TokenInput extends PureComponent {
|
||||
{...restProps}
|
||||
suffix={token.symbol}
|
||||
onChange={this.handleChange}
|
||||
onBlur={this.handleBlur}
|
||||
value={decimalValue}
|
||||
>
|
||||
{this.renderConversionComponent()}
|
||||
|
@ -8,7 +8,6 @@
|
||||
@import 'button-group/index';
|
||||
@import 'button/buttons';
|
||||
@import 'callout/callout';
|
||||
@import 'card/index';
|
||||
@import 'check-box/index';
|
||||
@import 'chip/chip';
|
||||
@import 'circle-icon/index';
|
||||
|
@ -17,6 +17,7 @@ export default class UnitInput extends PureComponent {
|
||||
actionComponent: PropTypes.node,
|
||||
error: PropTypes.bool,
|
||||
onChange: PropTypes.func,
|
||||
onBlur: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
suffix: PropTypes.string,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
@ -55,6 +56,8 @@ export default class UnitInput extends PureComponent {
|
||||
if (value === '') {
|
||||
this.setState({ value: '0' });
|
||||
}
|
||||
|
||||
this.props.onBlur && this.props.onBlur(value);
|
||||
};
|
||||
|
||||
handleChange = (event) => {
|
||||
|
37
ui/contexts/gasFee.js
Normal file
37
ui/contexts/gasFee.js
Normal file
@ -0,0 +1,37 @@
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useGasFeeInputs } from '../hooks/gasFeeInput/useGasFeeInputs';
|
||||
|
||||
export const GasFeeContext = createContext({});
|
||||
|
||||
export const GasFeeContextProvider = ({
|
||||
children,
|
||||
defaultEstimateToUse,
|
||||
transaction,
|
||||
minimumGasLimit,
|
||||
editGasMode,
|
||||
}) => {
|
||||
const gasFeeDetails = useGasFeeInputs(
|
||||
defaultEstimateToUse,
|
||||
transaction,
|
||||
minimumGasLimit,
|
||||
editGasMode,
|
||||
);
|
||||
return (
|
||||
<GasFeeContext.Provider value={gasFeeDetails}>
|
||||
{children}
|
||||
</GasFeeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export function useGasFeeContext() {
|
||||
return useContext(GasFeeContext);
|
||||
}
|
||||
|
||||
GasFeeContextProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
defaultEstimateToUse: PropTypes.string,
|
||||
transaction: PropTypes.object.isRequired,
|
||||
minimumGasLimit: PropTypes.string,
|
||||
editGasMode: PropTypes.string,
|
||||
};
|
@ -103,6 +103,7 @@ $ui-1: #f2f3f4;
|
||||
$ui-2: #d6d9dc;
|
||||
$ui-3: #bbc0c5;
|
||||
$ui-4: #6a737d;
|
||||
$ui-5: #c4c4c4;
|
||||
|
||||
$mainnet: #29b6af;
|
||||
$ropsten: #ff4a8d;
|
||||
@ -116,6 +117,7 @@ $color-map: (
|
||||
'ui-2': $ui-2,
|
||||
'ui-3': $ui-3,
|
||||
'ui-4': $ui-4,
|
||||
'ui-5': $ui-5,
|
||||
'white': $ui-white,
|
||||
'black': $ui-black,
|
||||
'grey': $ui-grey,
|
||||
|
@ -38,6 +38,7 @@ const AWAITING_SIGNATURES_ROUTE = '/swaps/awaiting-signatures';
|
||||
const AWAITING_SWAP_ROUTE = '/swaps/awaiting-swap';
|
||||
const SWAPS_ERROR_ROUTE = '/swaps/swaps-error';
|
||||
const SWAPS_MAINTENANCE_ROUTE = '/swaps/maintenance';
|
||||
const ADD_COLLECTIBLE_ROUTE = '/add-collectible';
|
||||
|
||||
const INITIALIZE_ROUTE = '/initialize';
|
||||
const INITIALIZE_WELCOME_ROUTE = '/initialize/welcome';
|
||||
@ -218,6 +219,7 @@ export {
|
||||
AWAITING_SIGNATURES_ROUTE,
|
||||
SWAPS_ERROR_ROUTE,
|
||||
SWAPS_MAINTENANCE_ROUTE,
|
||||
ADD_COLLECTIBLE_ROUTE,
|
||||
ONBOARDING_ROUTE,
|
||||
ONBOARDING_HELP_US_IMPROVE_ROUTE,
|
||||
ONBOARDING_CREATE_PASSWORD_ROUTE,
|
||||
|
@ -19,3 +19,8 @@ export const TOKEN_CATEGORY_HASH = {
|
||||
[TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER]: true,
|
||||
[TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM]: true,
|
||||
};
|
||||
|
||||
export const TRANSACTION_ENVELOPE_TYPE_NAMES = {
|
||||
FEE_MARKET: 'fee-market',
|
||||
LEGACY: 'legacy',
|
||||
};
|
||||
|
@ -130,7 +130,10 @@ const getMaxFeeWarning = (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const getBalanceError = (minimumCostInHexWei, transaction, ethBalance) => {
|
||||
const hasBalanceError = (minimumCostInHexWei, transaction, ethBalance) => {
|
||||
if (minimumCostInHexWei === undefined || ethBalance === undefined) {
|
||||
return false;
|
||||
}
|
||||
const minimumTxCostInHexWei = addHexes(
|
||||
minimumCostInHexWei,
|
||||
transaction?.txParams?.value || '0x0',
|
||||
@ -247,7 +250,7 @@ export function useGasFeeErrors({
|
||||
);
|
||||
|
||||
const { balance: ethBalance } = useSelector(getSelectedAccount);
|
||||
const balanceError = getBalanceError(
|
||||
const balanceError = hasBalanceError(
|
||||
minimumCostInHexWei,
|
||||
transaction,
|
||||
ethBalance,
|
||||
|
@ -1,13 +1,19 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { getAdvancedInlineGasShown } from '../../selectors';
|
||||
import { hexToDecimal } from '../../helpers/utils/conversions.util';
|
||||
import { GAS_FORM_ERRORS } from '../../helpers/constants/gas';
|
||||
import {
|
||||
GAS_RECOMMENDATIONS,
|
||||
CUSTOM_GAS_ESTIMATE,
|
||||
GAS_RECOMMENDATIONS,
|
||||
EDIT_GAS_MODES,
|
||||
} from '../../../shared/constants/gas';
|
||||
import { GAS_FORM_ERRORS } from '../../helpers/constants/gas';
|
||||
import { areDappSuggestedAndTxParamGasFeesTheSame } from '../../helpers/utils/confirm-tx.util';
|
||||
import {
|
||||
checkNetworkAndAccountSupports1559,
|
||||
getAdvancedInlineGasShown,
|
||||
} from '../../selectors';
|
||||
import { hexToDecimal } from '../../helpers/utils/conversions.util';
|
||||
import { isLegacyTransaction } from '../../helpers/utils/transactions.util';
|
||||
import { useGasFeeEstimates } from '../useGasFeeEstimates';
|
||||
|
||||
import { useGasFeeErrors } from './useGasFeeErrors';
|
||||
@ -65,8 +71,12 @@ export function useGasFeeInputs(
|
||||
defaultEstimateToUse = GAS_RECOMMENDATIONS.MEDIUM,
|
||||
transaction,
|
||||
minimumGasLimit = '0x5208',
|
||||
editGasMode,
|
||||
editGasMode = EDIT_GAS_MODES.MODIFY_IN_PLACE,
|
||||
) {
|
||||
const supportsEIP1559 =
|
||||
useSelector(checkNetworkAndAccountSupports1559) &&
|
||||
!isLegacyTransaction(transaction?.txParams);
|
||||
|
||||
// We need the gas estimates from the GasFeeController in the background.
|
||||
// Calling this hooks initiates polling for new gas estimates and returns the
|
||||
// current estimate.
|
||||
@ -90,6 +100,13 @@ export function useGasFeeInputs(
|
||||
return defaultEstimateToUse;
|
||||
});
|
||||
|
||||
const [
|
||||
isUsingDappSuggestedGasFees,
|
||||
setIsUsingDappSuggestedGasFees,
|
||||
] = useState(() =>
|
||||
Boolean(areDappSuggestedAndTxParamGasFeesTheSame(transaction)),
|
||||
);
|
||||
|
||||
const [gasLimit, setGasLimit] = useState(
|
||||
Number(hexToDecimal(transaction?.txParams?.gas ?? '0x0')),
|
||||
);
|
||||
@ -191,6 +208,7 @@ export function useGasFeeInputs(
|
||||
setMaxPriorityFeePerGas(null);
|
||||
setGasPrice(null);
|
||||
setGasPriceHasBeenManuallySet(false);
|
||||
setIsUsingDappSuggestedGasFees(false);
|
||||
},
|
||||
[
|
||||
setInternalEstimateToUse,
|
||||
@ -199,6 +217,7 @@ export function useGasFeeInputs(
|
||||
setMaxPriorityFeePerGas,
|
||||
setGasPrice,
|
||||
setGasPriceHasBeenManuallySet,
|
||||
setIsUsingDappSuggestedGasFees,
|
||||
],
|
||||
);
|
||||
|
||||
@ -226,6 +245,7 @@ export function useGasFeeInputs(
|
||||
]);
|
||||
|
||||
return {
|
||||
transaction,
|
||||
maxFeePerGas,
|
||||
maxFeePerGasFiat,
|
||||
setMaxFeePerGas,
|
||||
@ -243,6 +263,7 @@ export function useGasFeeInputs(
|
||||
estimatedMaximumNative,
|
||||
estimatedMinimumNative,
|
||||
isGasEstimatesLoading,
|
||||
isUsingDappSuggestedGasFees,
|
||||
gasFeeEstimates,
|
||||
gasEstimateType,
|
||||
estimatedGasFeeTimeBounds,
|
||||
@ -254,5 +275,6 @@ export function useGasFeeInputs(
|
||||
gasErrors,
|
||||
gasWarnings,
|
||||
hasGasErrors,
|
||||
supportsEIP1559,
|
||||
};
|
||||
}
|
||||
|
65
ui/pages/add-collectible/add-collectible.component.js
Normal file
65
ui/pages/add-collectible/add-collectible.component.js
Normal file
@ -0,0 +1,65 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useI18nContext } from '../../hooks/useI18nContext';
|
||||
import { DEFAULT_ROUTE } from '../../helpers/constants/routes';
|
||||
|
||||
import Box from '../../components/ui/box';
|
||||
import TextField from '../../components/ui/text-field';
|
||||
import PageContainer from '../../components/ui/page-container';
|
||||
|
||||
export default function AddCollectible() {
|
||||
const t = useI18nContext();
|
||||
const history = useHistory();
|
||||
|
||||
const [address, setAddress] = useState('');
|
||||
const [tokenId, setTokenId] = useState('');
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title={t('addNFT')}
|
||||
onSubmit={() => {
|
||||
console.log(
|
||||
`Adding collectible with ID: ${tokenId} and address ${address}`,
|
||||
);
|
||||
history.push(DEFAULT_ROUTE);
|
||||
}}
|
||||
submitText={t('add')}
|
||||
onCancel={() => {
|
||||
history.push(DEFAULT_ROUTE);
|
||||
}}
|
||||
onClose={() => {
|
||||
history.push(DEFAULT_ROUTE);
|
||||
}}
|
||||
disabled={false}
|
||||
contentComponent={
|
||||
<Box padding={4}>
|
||||
<Box>
|
||||
<TextField
|
||||
id="address"
|
||||
label={t('address')}
|
||||
placeholder="0x..."
|
||||
type="text"
|
||||
value={address}
|
||||
onChange={(e) => setAddress(e.target.value)}
|
||||
fullWidth
|
||||
autoFocus
|
||||
margin="normal"
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<TextField
|
||||
id="token-id"
|
||||
label={t('id')}
|
||||
placeholder={t('nftTokenIdPlaceholder')}
|
||||
type="number"
|
||||
value={tokenId}
|
||||
onChange={(e) => setTokenId(e.target.value)}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
1
ui/pages/add-collectible/index.js
Normal file
1
ui/pages/add-collectible/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './add-collectible.component';
|
@ -9,9 +9,7 @@ import Identicon from '../../components/ui/identicon';
|
||||
import Tooltip from '../../components/ui/tooltip';
|
||||
import Copy from '../../components/ui/icon/copy-icon.component';
|
||||
|
||||
import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../shared/constants/app';
|
||||
import { SECOND } from '../../../shared/constants/time';
|
||||
import { getEnvironmentType } from '../../../app/scripts/lib/util';
|
||||
import { conversionUtil } from '../../../shared/modules/conversion.utils';
|
||||
|
||||
export default class ConfirmDecryptMessage extends Component {
|
||||
@ -44,44 +42,6 @@ export default class ConfirmDecryptMessage extends Component {
|
||||
hasCopied: false,
|
||||
};
|
||||
|
||||
componentDidMount = () => {
|
||||
if (
|
||||
getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION
|
||||
) {
|
||||
window.addEventListener('beforeunload', this._beforeUnload);
|
||||
}
|
||||
};
|
||||
|
||||
componentWillUnmount = () => {
|
||||
this._removeBeforeUnload();
|
||||
};
|
||||
|
||||
_beforeUnload = async (event) => {
|
||||
const {
|
||||
clearConfirmTransaction,
|
||||
cancelDecryptMessage,
|
||||
txData,
|
||||
} = this.props;
|
||||
const { metricsEvent } = this.context;
|
||||
await cancelDecryptMessage(txData, event);
|
||||
metricsEvent({
|
||||
eventOpts: {
|
||||
category: 'Messages',
|
||||
action: 'Decrypt Message Request',
|
||||
name: 'Cancel Via Notification Close',
|
||||
},
|
||||
});
|
||||
clearConfirmTransaction();
|
||||
};
|
||||
|
||||
_removeBeforeUnload = () => {
|
||||
if (
|
||||
getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION
|
||||
) {
|
||||
window.removeEventListener('beforeunload', this._beforeUnload);
|
||||
}
|
||||
};
|
||||
|
||||
copyMessage = () => {
|
||||
copyToClipboard(this.state.rawMessage);
|
||||
this.context.metricsEvent({
|
||||
|
@ -5,8 +5,6 @@ import AccountListItem from '../../components/app/account-list-item';
|
||||
import Button from '../../components/ui/button';
|
||||
import Identicon from '../../components/ui/identicon';
|
||||
|
||||
import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../shared/constants/app';
|
||||
import { getEnvironmentType } from '../../../app/scripts/lib/util';
|
||||
import { conversionUtil } from '../../../shared/modules/conversion.utils';
|
||||
|
||||
export default class ConfirmEncryptionPublicKey extends Component {
|
||||
@ -33,44 +31,6 @@ export default class ConfirmEncryptionPublicKey extends Component {
|
||||
nativeCurrency: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount = () => {
|
||||
if (
|
||||
getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION
|
||||
) {
|
||||
window.addEventListener('beforeunload', this._beforeUnload);
|
||||
}
|
||||
};
|
||||
|
||||
componentWillUnmount = () => {
|
||||
this._removeBeforeUnload();
|
||||
};
|
||||
|
||||
_beforeUnload = async (event) => {
|
||||
const {
|
||||
clearConfirmTransaction,
|
||||
cancelEncryptionPublicKey,
|
||||
txData,
|
||||
} = this.props;
|
||||
const { metricsEvent } = this.context;
|
||||
await cancelEncryptionPublicKey(txData, event);
|
||||
metricsEvent({
|
||||
eventOpts: {
|
||||
category: 'Messages',
|
||||
action: 'Encryption public key Request',
|
||||
name: 'Cancel Via Notification Close',
|
||||
},
|
||||
});
|
||||
clearConfirmTransaction();
|
||||
};
|
||||
|
||||
_removeBeforeUnload = () => {
|
||||
if (
|
||||
getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION
|
||||
) {
|
||||
window.removeEventListener('beforeunload', this._beforeUnload);
|
||||
}
|
||||
};
|
||||
|
||||
renderHeader = () => {
|
||||
return (
|
||||
<div className="request-encryption-public-key__header">
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user