1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Merge pull request #16485 from MetaMask/Version-v10.23.0

Version v10.23.0
This commit is contained in:
Dan J Miller 2022-12-06 17:08:08 -03:30 committed by GitHub
commit 6af3f9a4fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
203 changed files with 7076 additions and 2432 deletions

View File

@ -323,9 +323,25 @@ jobs:
- checkout
- attach_workspace:
at: .
- run:
name: build:dist
command: yarn build --build-type flask dist
- when:
condition:
not:
matches:
pattern: /^master$/
value: << pipeline.git.branch >>
steps:
- run:
name: build:dist
command: yarn build --build-type flask dist
- when:
condition:
matches:
pattern: /^master$/
value: << pipeline.git.branch >>
steps:
- run:
name: build:prod
command: yarn build --build-type flask prod
- run:
name: build:debug
command: find dist/ -type f -exec md5sum {} \; | sort -k 2
@ -532,6 +548,7 @@ jobs:
test-e2e-chrome:
executor: node-browsers
parallelism: 8
steps:
- checkout
- run:
@ -556,9 +573,10 @@ jobs:
- store_artifacts:
path: test-artifacts
destination: test-artifacts
test-e2e-chrome-mv3:
executor: node-browsers
parallelism: 8
steps:
- checkout
- run:
@ -586,6 +604,7 @@ jobs:
test-e2e-firefox-snaps:
executor: node-browsers
parallelism: 2
steps:
- checkout
- run:
@ -613,6 +632,7 @@ jobs:
test-e2e-chrome-snaps:
executor: node-browsers
parallelism: 2
steps:
- checkout
- run:
@ -640,6 +660,7 @@ jobs:
test-e2e-firefox:
executor: node-browsers-medium-plus
parallelism: 8
steps:
- checkout
- run:
@ -808,6 +829,11 @@ jobs:
- store_artifacts:
path: development/ts-migration-dashboard/build
destination: ts-migration-dashboard
- run:
name: Set branch parent commit env var
command: |
echo "export PARENT_COMMIT=$(git rev-parse "$(git rev-list --topo-order --reverse HEAD ^origin/develop | head -1)"^)" >> $BASH_ENV
source $BASH_ENV
- run:
name: build:announce
command: ./development/metamaskbot-build-announce.js

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,6 +6,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [10.23.0]
### Added
- Add Picker Network Component ([#16340](https://github.com/MetaMask/metamask-extension/pull/16340))
- Add Button component, unifying primary, secondary and link buttons([#16305](https://github.com/MetaMask/metamask-extension/pull/16305))
- Add Button Icon component ([#16277](https://github.com/MetaMask/metamask-extension/pull/16277))
- Swaps: enable Swaps functionality on Arbitrum and Optimism networks ([#16396](https://github.com/MetaMask/metamask-extension/pull/16396))
- [FLASK] Add snap cronjobs ([#16239](https://github.com/MetaMask/metamask-extension/pull/16239))
### Changed
- Replace every Address value by the Address component on SignTypedData v4 Signature screen ([#16018](https://github.com/MetaMask/metamask-extension/pull/16018))
- Update Address component on Transaction data screen by displaying Account name, Contact name, or Contract name when corresponds ([#15888](https://github.com/MetaMask/metamask-extension/pull/15888))
- Bump `@metamask/providers` from `10.0.0` to `10.2.0` ([#16361](https://github.com/MetaMask/metamask-extension/pull/16361))
- [FLASK] **BREAKING**: Snaps no longer automatically receive a `Buffer` polyfill ([#16394](https://github.com/MetaMask/metamask-extension/pull/16394))
- To work around this you can either use typed arrays or include a polyfill yourself.
- [FLASK] **BREAKING**: Snap RPC methods now use `@metamask/key-tree@6.0.0` ([#16394](https://github.com/MetaMask/metamask-extension/pull/16394))
- In the new version, all hexadecimal values are prefixed with `0x`
- All fields containing the word `Buffer` has also been renamed to `Bytes`
- Please update your snap to use the latest version
### Fixed
- Fix Settings Search pointing into the incorrect row for Token Detection entry ([#16407](https://github.com/MetaMask/metamask-extension/pull/16407))
- Fix Balance not updating when using a duplicated `chainId` network ([#14245](https://github.com/MetaMask/metamask-extension/pull/14245))
- [FLASK] Fix an issue with updating snaps that require certain permissions ([#16473](https://github.com/MetaMask/metamask-extension/pull/16473))
- [FLASK] Fix some issues with installing snaps that request `eth_accounts` ([#16365](https://github.com/MetaMask/metamask-extension/pull/16365))
- [FLASK] Catch and display errors in snaps insight ([#16416](https://github.com/MetaMask/metamask-extension/pull/16416))
## [10.22.3]
### Added
- [Beta]: Add Beta banner to all screens ([#16307](https://github.com/MetaMask/metamask-extension/pull/16307))
@ -30,6 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Add Aurora network to the Popular Custom Network list ([#16039](https://github.com/MetaMask/metamask-extension/pull/16039))
- Add array of valid sizes for Box `height` and `width` to support responsive layout ([#16111](https://github.com/MetaMask/metamask-extension/pull/16111))
- Add Warning on a Send transaction request when user doesn't have funds ([#16220](https://github.com/MetaMask/metamask-extension/pull/16220))
- [FLASK] Allow snaps insights to show on regular EOA transactions ([#16093](https://github.com/MetaMask/metamask-extension/pull/16093))
### Changed
@ -3309,7 +3336,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Uncategorized
- Added the ability to restore accounts from seed words.
[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.22.3...HEAD
[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.23.0...HEAD
[10.23.0]: https://github.com/MetaMask/metamask-extension/compare/v10.22.3...v10.23.0
[10.22.3]: https://github.com/MetaMask/metamask-extension/compare/v10.22.2...v10.22.3
[10.22.2]: https://github.com/MetaMask/metamask-extension/compare/v10.22.1...v10.22.2
[10.22.1]: https://github.com/MetaMask/metamask-extension/compare/v10.22.0...v10.22.1

View File

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

View File

@ -2153,6 +2153,9 @@
"networkName": {
"message": "Network name"
},
"networkNameArbitrum": {
"message": "Arbitrum"
},
"networkNameAvalanche": {
"message": "Avalanche"
},
@ -2168,6 +2171,9 @@
"networkNameGoerli": {
"message": "Goerli"
},
"networkNameOptimism": {
"message": "Optimism"
},
"networkNamePolygon": {
"message": "Polygon"
},
@ -2664,6 +2670,10 @@
"message": "Connect to the $1 Snap.",
"description": "The description for the `wallet_snap_*` permission. $1 is the name of the Snap."
},
"permission_cronjob": {
"message": "Schedule and execute periodic actions.",
"description": "The description for the `snap_cronjob` permission"
},
"permission_customConfirmation": {
"message": "Display a confirmation in MetaMask.",
"description": "The description for the `snap_confirm` permission"
@ -2947,6 +2957,10 @@
"message": "By revoking permission, the following $1 will no longer be able to access your $2",
"description": "$1 is either key 'account' or 'contract', and $2 is either a string or link of a given token symbol or name"
},
"revokeSpendingCap": {
"message": "Revoke spending cap for your $1",
"description": "$1 is a token symbol"
},
"revokeSpendingCapTooltipText": {
"message": "This contract will be unable to spend any more of your current or future tokens."
},
@ -3135,7 +3149,8 @@
"description": "The token symbol that is being approved"
},
"setSpendingCap": {
"message": "Set a spending cap for your"
"message": "Set a spending cap for your $1",
"description": "$1 is a token symbol"
},
"settings": {
"message": "Settings"
@ -3270,6 +3285,10 @@
"snaps": {
"message": "Snaps"
},
"snapsInsightError": {
"message": "An error occured with $1: $2",
"description": "This is shown when the insight snap throws an error. $1 is the snap name, $2 is the error message."
},
"snapsInsightLoading": {
"message": "Loading transaction insight..."
},
@ -3288,6 +3307,9 @@
"someNetworksMayPoseSecurity": {
"message": "Some networks may pose security and/or privacy risks. Understand the risks before adding & using a network."
},
"somethingIsWrong": {
"message": "Something's gone wrong. Try reloading the page."
},
"somethingWentWrong": {
"message": "Oops! Something went wrong."
},

View File

@ -79,7 +79,7 @@ const localStore = inTest ? new ReadOnlyNetworkStore() : new LocalStore();
let versionedData;
if (inTest || process.env.METAMASK_DEBUG) {
global.metamaskGetState = localStore.get.bind(localStore);
global.stateHooks.metamaskGetState = localStore.get.bind(localStore);
}
const phishingPageUrl = new URL(process.env.PHISHING_WARNING_PAGE_URL);
@ -88,6 +88,9 @@ const ONE_SECOND_IN_MILLISECONDS = 1_000;
// Timeout for initializing phishing warning page.
const PHISHING_WARNING_PAGE_TIMEOUT = ONE_SECOND_IN_MILLISECONDS;
const ACK_KEEP_ALIVE_MESSAGE = 'ACK_KEEP_ALIVE_MESSAGE';
const WORKER_KEEP_ALIVE_MESSAGE = 'WORKER_KEEP_ALIVE_MESSAGE';
/**
* In case of MV3 we attach a "onConnect" event listener as soon as the application is initialised.
* Reason is that in case of MV3 a delay in doing this was resulting in missing first connect event after service worker is re-activated.
@ -439,6 +442,14 @@ function setupController(initState, initLangCode, remoteSourcePort) {
// This ensures that UI is initialised only after background is ready
// It fixes the issue of blank screen coming when extension is loaded, the issue is very frequent in MV3
remotePort.postMessage({ name: 'CONNECTION_READY' });
// If we get a WORKER_KEEP_ALIVE message, we respond with an ACK
remotePort.onMessage.addListener((message) => {
if (message.name === WORKER_KEEP_ALIVE_MESSAGE) {
// To test un-comment this line and wait for 1 minute. An error should be shown on MetaMask UI.
remotePort.postMessage({ name: ACK_KEEP_ALIVE_MESSAGE });
}
});
}
if (processName === ENVIRONMENT_TYPE_POPUP) {
@ -742,7 +753,7 @@ browser.runtime.onInstalled.addListener(({ reason }) => {
});
function setupSentryGetStateGlobal(store) {
global.sentryHooks.getSentryState = function () {
global.stateHooks.getSentryState = function () {
const fullState = store.getState();
const debugState = maskObject({ metamask: fullState }, SENTRY_STATE);
return {

View File

@ -193,11 +193,9 @@ export default class AppStateController extends EventEmitter {
const { timeoutMinutes } = this.store.getState();
if (this.timer) {
if (isManifestV3) {
chrome.alarms.clear(AUTO_LOCK_TIMEOUT_ALARM);
} else {
clearTimeout(this.timer);
}
clearTimeout(this.timer);
} else if (isManifestV3) {
chrome.alarms.clear(AUTO_LOCK_TIMEOUT_ALARM);
}
if (!timeoutMinutes) {
@ -209,16 +207,11 @@ export default class AppStateController extends EventEmitter {
delayInMinutes: timeoutMinutes,
periodInMinutes: timeoutMinutes,
});
chrome.alarms.onAlarm.addListener(() => {
chrome.alarms.getAll((alarms) => {
const hasAlarm = alarms.find(
(alarm) => alarm.name === AUTO_LOCK_TIMEOUT_ALARM,
);
if (hasAlarm) {
this.onInactiveTimeout();
chrome.alarms.clear(AUTO_LOCK_TIMEOUT_ALARM);
}
});
chrome.alarms.onAlarm.addListener((alarmInfo) => {
if (alarmInfo.name === AUTO_LOCK_TIMEOUT_ALARM) {
this.onInactiveTimeout();
chrome.alarms.clear(AUTO_LOCK_TIMEOUT_ALARM);
}
});
} else {
this.timer = setTimeout(

View File

@ -3,12 +3,12 @@ import sinon from 'sinon';
import nock from 'nock';
import { ObservableStore } from '@metamask/obs-store';
import BigNumber from 'bignumber.js';
import { ControllerMessenger } from '@metamask/controllers';
import {
ControllerMessenger,
TokenListController,
TokensController,
AssetsContractController,
} from '@metamask/controllers';
} from '@metamask/assets-controllers';
import { NETWORK_TYPES } from '../../../shared/constants/network';
import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils';
import DetectTokensController from './detect-tokens';

View File

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

View File

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

View File

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

View File

@ -1,9 +1,7 @@
import { strict as assert } from 'assert';
import sinon from 'sinon';
import {
ControllerMessenger,
TokenListController,
} from '@metamask/controllers';
import { ControllerMessenger } from '@metamask/controllers';
import { TokenListController } from '@metamask/assets-controllers';
import { CHAIN_IDS } from '../../../shared/constants/network';
import PreferencesController from './preferences';
import NetworkController from './network';

View File

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

View File

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

View File

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

View File

@ -174,3 +174,19 @@ export {
getChainType,
checkAlarmExists,
};
// Taken from https://stackoverflow.com/a/1349426/3696652
const characters =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
export const generateRandomId = () => {
let result = '';
const charactersLength = characters.length;
for (let i = 0; i < 20; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
};
export const isValidDate = (d) => {
return d instanceof Date && !isNaN(d);
};

View File

@ -27,15 +27,10 @@ import {
AddressBookController,
ApprovalController,
ControllerMessenger,
CurrencyRateController,
PhishingController,
AnnouncementController,
GasFeeController,
TokenListController,
TokensController,
TokenRatesController,
CollectiblesController,
AssetsContractController,
CollectibleDetectionController,
PermissionController,
SubjectMetadataController,
@ -46,9 +41,17 @@ import {
NotificationController,
///: END:ONLY_INCLUDE_IN
} from '@metamask/controllers';
import {
CurrencyRateController,
TokenListController,
TokensController,
TokenRatesController,
AssetsContractController,
} from '@metamask/assets-controllers';
import SmartTransactionsController from '@metamask/smart-transactions-controller';
///: BEGIN:ONLY_INCLUDE_IN(flask)
import {
CronjobController,
SnapController,
IframeExecutionService,
} from '@metamask/snap-controllers';
@ -296,7 +299,8 @@ export default class MetamaskController extends EventEmitter {
onPreferencesStateChange: (listener) =>
this.preferencesController.store.subscribe(listener),
onNetworkStateChange: (cb) =>
this.networkController.store.subscribe((networkState) => {
this.networkController.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => {
const networkState = this.networkController.store.getState();
const modifiedNetworkState = {
...networkState,
provider: {
@ -539,6 +543,9 @@ export default class MetamaskController extends EventEmitter {
getCurrentChainId: this.networkController.getCurrentChainId.bind(
this.networkController,
),
getNetworkIdentifier: this.networkController.getNetworkIdentifier.bind(
this.networkController,
),
});
// start and stop polling for balances based on activeControllerConnections
@ -664,7 +671,7 @@ export default class MetamaskController extends EventEmitter {
///: BEGIN:ONLY_INCLUDE_IN(flask)
this.snapExecutionService = new IframeExecutionService({
iframeUrl: new URL(
'https://metamask.github.io/iframe-execution-environment/0.9.1',
'https://metamask.github.io/iframe-execution-environment/0.10.0',
),
messenger: this.controllerMessenger.getRestricted({
name: 'ExecutionService',
@ -750,6 +757,24 @@ export default class MetamaskController extends EventEmitter {
},
},
});
// --- Snaps Cronjob Controller configuration
const cronjobControllerMessenger = this.controllerMessenger.getRestricted({
name: 'CronjobController',
allowedEvents: [
'SnapController:snapInstalled',
'SnapController:snapUpdated',
'SnapController:snapRemoved',
],
allowedActions: [
`${this.permissionController.name}:getPermissions`,
'SnapController:handleRequest',
'SnapController:getAll',
],
});
this.cronjobController = new CronjobController({
state: initState.CronjobController,
messenger: cronjobControllerMessenger,
});
///: END:ONLY_INCLUDE_IN
this.detectTokensController = new DetectTokensController({
preferences: this.preferencesController,
@ -1041,6 +1066,7 @@ export default class MetamaskController extends EventEmitter {
CollectiblesController: this.collectiblesController,
///: BEGIN:ONLY_INCLUDE_IN(flask)
SnapController: this.snapController,
CronjobController: this.cronjobController,
NotificationController: this.notificationController,
///: END:ONLY_INCLUDE_IN
});
@ -1082,6 +1108,7 @@ export default class MetamaskController extends EventEmitter {
CollectiblesController: this.collectiblesController,
///: BEGIN:ONLY_INCLUDE_IN(flask)
SnapController: this.snapController,
CronjobController: this.cronjobController,
NotificationController: this.notificationController,
///: END:ONLY_INCLUDE_IN
},
@ -1130,10 +1157,6 @@ export default class MetamaskController extends EventEmitter {
return {
...buildSnapEndowmentSpecifications(),
...buildSnapRestrictedMethodSpecifications({
addSnap: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapController:add',
),
clearSnapState: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapController:clearSnapState',
@ -2042,7 +2065,7 @@ export default class MetamaskController extends EventEmitter {
}
}
async addCustomNetwork(customRpc) {
async addCustomNetwork(customRpc, actionId) {
const { chainId, chainName, rpcUrl, ticker, blockExplorerUrl } = customRpc;
await this.preferencesController.addToFrequentRpcList(
@ -2078,6 +2101,7 @@ export default class MetamaskController extends EventEmitter {
sensitiveProperties: {
rpc_url: rpcUrlOrigin,
},
actionId,
});
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -468,6 +468,55 @@
"@babel/runtime": true
}
},
"@metamask/assets-controllers": {
"globals": {
"Headers": true,
"URL": true,
"clearInterval": true,
"clearTimeout": true,
"console.log": true,
"setInterval": true,
"setTimeout": true
},
"packages": {
"@metamask/assets-controllers>@metamask/base-controller": true,
"@metamask/assets-controllers>@metamask/controller-utils": true,
"@metamask/contract-metadata": true,
"@metamask/controllers>@ethersproject/abi": true,
"@metamask/controllers>@ethersproject/contracts": true,
"@metamask/controllers>@ethersproject/providers": true,
"@metamask/controllers>abort-controller": true,
"@metamask/controllers>async-mutex": true,
"@metamask/controllers>multiformats": true,
"@metamask/metamask-eth-abis": true,
"browserify>events": true,
"eth-query": true,
"eth-rpc-errors": true,
"ethereumjs-util": true,
"single-call-balance-checker-abi": true,
"uuid": true
}
},
"@metamask/assets-controllers>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/assets-controllers>@metamask/controller-utils": {
"globals": {
"console.error": true,
"fetch": true,
"setTimeout": true
},
"packages": {
"@metamask/controllers>isomorphic-fetch": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true
}
},
"@metamask/controllers": {
"globals": {
"Headers": true,

View File

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

View File

@ -468,6 +468,55 @@
"@babel/runtime": true
}
},
"@metamask/assets-controllers": {
"globals": {
"Headers": true,
"URL": true,
"clearInterval": true,
"clearTimeout": true,
"console.log": true,
"setInterval": true,
"setTimeout": true
},
"packages": {
"@metamask/assets-controllers>@metamask/base-controller": true,
"@metamask/assets-controllers>@metamask/controller-utils": true,
"@metamask/contract-metadata": true,
"@metamask/controllers>@ethersproject/abi": true,
"@metamask/controllers>@ethersproject/contracts": true,
"@metamask/controllers>@ethersproject/providers": true,
"@metamask/controllers>abort-controller": true,
"@metamask/controllers>async-mutex": true,
"@metamask/controllers>multiformats": true,
"@metamask/metamask-eth-abis": true,
"browserify>events": true,
"eth-query": true,
"eth-rpc-errors": true,
"ethereumjs-util": true,
"single-call-balance-checker-abi": true,
"uuid": true
}
},
"@metamask/assets-controllers>@metamask/base-controller": {
"packages": {
"immer": true
}
},
"@metamask/assets-controllers>@metamask/controller-utils": {
"globals": {
"console.error": true,
"fetch": true,
"setTimeout": true
},
"packages": {
"@metamask/controllers>isomorphic-fetch": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
"ethereumjs-util": true,
"ethjs>ethjs-unit": true
}
},
"@metamask/controllers": {
"globals": {
"Headers": true,

View File

@ -1,6 +1,6 @@
{
"name": "metamask-crx",
"version": "10.22.3",
"version": "10.23.0",
"private": true,
"repository": {
"type": "git",
@ -111,6 +111,7 @@
"@keystonehq/bc-ur-registry-eth": "^0.12.1",
"@keystonehq/metamask-airgapped-keyring": "^0.6.1",
"@material-ui/core": "^4.11.0",
"@metamask/assets-controllers": "^1.0.0",
"@metamask/contract-metadata": "^1.31.0",
"@metamask/controllers": "^32.0.2",
"@metamask/design-tokens": "^1.9.0",
@ -122,13 +123,13 @@
"@metamask/logo": "^3.1.1",
"@metamask/metamask-eth-abis": "^3.0.0",
"@metamask/obs-store": "^5.0.0",
"@metamask/post-message-stream": "^4.0.0",
"@metamask/post-message-stream": "^6.0.0",
"@metamask/providers": "^10.0.0",
"@metamask/rpc-methods": "^0.22.2",
"@metamask/rpc-methods": "^0.23.0",
"@metamask/slip44": "^2.1.0",
"@metamask/smart-transactions-controller": "^3.0.0",
"@metamask/snap-controllers": "^0.22.2",
"@metamask/snap-utils": "^0.22.2",
"@metamask/snap-controllers": "^0.23.0",
"@metamask/snap-utils": "^0.23.0",
"@ngraveio/bc-ur": "^1.1.6",
"@popperjs/core": "^2.4.0",
"@reduxjs/toolkit": "^1.6.2",
@ -187,7 +188,7 @@
"localforage": "^1.9.0",
"lodash": "^4.17.21",
"loglevel": "^1.4.1",
"luxon": "^1.26.0",
"luxon": "^3.1.0",
"nanoid": "^2.1.6",
"nonce-tracker": "^1.0.0",
"obj-multiplex": "^1.0.0",
@ -325,7 +326,7 @@
"fast-glob": "^3.2.2",
"fs-extra": "^8.1.0",
"ganache": "^v7.0.4",
"geckodriver": "^1.21.0",
"geckodriver": "^3.2.0",
"gh-pages": "^3.2.3",
"globby": "^11.0.4",
"gulp": "^4.0.2",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -136,7 +136,7 @@ describe('Test Snap bip-32', function () {
);
assert.equal(
await publicKeyResult.getText(),
'Public key: "043e98d696ae15caef75fa8dd204a7c5c08d1272b2218ba3c20feeb4c691eec366606ece56791c361a2320e7fad8bcbb130f66d51c591fc39767ab2856e93f8dfb"',
'Public key: "0x043e98d696ae15caef75fa8dd204a7c5c08d1272b2218ba3c20feeb4c691eec366606ece56791c361a2320e7fad8bcbb130f66d51c591fc39767ab2856e93f8dfb"',
);
// wait then run compressed public key test
@ -149,7 +149,7 @@ describe('Test Snap bip-32', function () {
);
assert.equal(
await compressedPublicKeyResult.getText(),
'Public key: "033e98d696ae15caef75fa8dd204a7c5c08d1272b2218ba3c20feeb4c691eec366"',
'Public key: "0x033e98d696ae15caef75fa8dd204a7c5c08d1272b2218ba3c20feeb4c691eec366"',
);
},
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -92,7 +92,7 @@ describe('Confirm Page Container Container Test', () => {
expect(senderRecipient).toBeInTheDocument();
});
it('should render recipient as address', () => {
const recipientName = screen.queryByText(shortenAddress(props.toAddress));
const recipientName = screen.queryByText('New contract');
expect(recipientName).toBeInTheDocument();
});
@ -118,7 +118,7 @@ describe('Confirm Page Container Container Test', () => {
describe('Contact/AddressBook name should appear in recipient header', () => {
it('should not show add to address dialog if recipient is in contact list and should display contact name', () => {
const addressBookName = 'test save name';
const addressBookName = 'New contract';
const addressBook = {
'0x5': {

View File

@ -65,6 +65,7 @@ export default class ConfirmPageContainer extends Component {
fromName: PropTypes.string,
toAddress: PropTypes.string,
toName: PropTypes.string,
toMetadataName: PropTypes.string,
toEns: PropTypes.string,
toNickname: PropTypes.string,
// Content
@ -119,6 +120,7 @@ export default class ConfirmPageContainer extends Component {
fromName,
fromAddress,
toName,
toMetadataName,
toEns,
toNickname,
toAddress,
@ -233,6 +235,7 @@ export default class ConfirmPageContainer extends Component {
senderName={fromName}
senderAddress={fromAddress}
recipientName={toName}
recipientMetadataName={toMetadataName}
recipientAddress={toAddress}
recipientEns={toEns}
recipientNickname={toNickname}

View File

@ -5,6 +5,9 @@ import {
getIsBuyableChain,
getNetworkIdentifier,
getSwapsDefaultToken,
getMetadataContractName,
getAccountName,
getMetaMaskIdentities,
} from '../../../selectors';
import ConfirmPageContainer from './confirm-page-container.component';
@ -15,11 +18,15 @@ function mapStateToProps(state, ownProps) {
const networkIdentifier = getNetworkIdentifier(state);
const defaultToken = getSwapsDefaultToken(state);
const accountBalance = defaultToken.string;
const identities = getMetaMaskIdentities(state);
const toName = getAccountName(identities, to);
const toMetadataName = getMetadataContractName(state, to);
return {
isBuyableChain,
contact,
toName: contact?.name || ownProps.toName,
toName,
toMetadataName,
isOwnedAccount: getAccountsWithLabels(state)
.map((accountWithLabel) => accountWithLabel.address)
.includes(to),

View File

@ -15,10 +15,15 @@ import { useI18nContext } from '../../../../hooks/useI18nContext';
import { useTransactionInsightSnap } from '../../../../hooks/flask/useTransactionInsightSnap';
import SnapContentFooter from '../../flask/snap-content-footer/snap-content-footer';
import Box from '../../../ui/box/box';
import ActionableMessage from '../../../ui/actionable-message/actionable-message';
export const SnapInsight = ({ transaction, chainId, selectedSnap }) => {
const t = useI18nContext();
const response = useTransactionInsightSnap({
const {
data: response,
error,
loading,
} = useTransactionInsightSnap({
transaction,
chainId,
snapId: selectedSnap.id,
@ -26,8 +31,8 @@ export const SnapInsight = ({ transaction, chainId, selectedSnap }) => {
const data = response?.insights;
const hasNoData = !data || !Object.keys(data).length;
const hasNoData =
!error && (loading || !data || (data && Object.keys(data).length === 0));
return (
<Box
flexDirection={FLEX_DIRECTION.COLUMN}
@ -39,13 +44,13 @@ export const SnapInsight = ({ transaction, chainId, selectedSnap }) => {
textAlign={hasNoData && TEXT_ALIGN.CENTER}
className="snap-insight"
>
{data ? (
{!loading && !error && (
<Box
height="full"
flexDirection={FLEX_DIRECTION.COLUMN}
className="snap-insight__container"
>
{Object.keys(data).length ? (
{data && Object.keys(data).length > 0 ? (
<>
<Box
flexDirection={FLEX_DIRECTION.COLUMN}
@ -94,7 +99,29 @@ export const SnapInsight = ({ transaction, chainId, selectedSnap }) => {
</Typography>
)}
</Box>
) : (
)}
{!loading && error && (
<Box
paddingTop={0}
paddingRight={6}
paddingBottom={3}
paddingLeft={6}
className="snap-insight__container__error"
>
<ActionableMessage
message={t('snapsInsightError', [
selectedSnap.manifest.proposedName,
error.message,
])}
type="danger"
useIcon
iconFillColor="var(--color-error-default)"
/>
</Box>
)}
{loading && (
<>
<Preloader size={40} />
<Typography

View File

@ -1,4 +1,5 @@
import React, { useState, useContext } from 'react';
import React, { useState, useContext, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { I18nContext } from '../../../contexts/i18n';
import Box from '../../ui/box';
@ -15,6 +16,8 @@ import {
JUSTIFY_CONTENT,
SIZES,
} from '../../../helpers/constants/design-system';
import { getCustomTokenAmount } from '../../../selectors';
import { setCustomTokenAmount } from '../../../ducks/app/app';
import { CustomSpendingCapTooltip } from './custom-spending-cap-tooltip';
export default function CustomSpendingCap({
@ -22,12 +25,17 @@ export default function CustomSpendingCap({
currentTokenBalance,
dappProposedValue,
siteOrigin,
onEdit,
passTheErrorText,
}) {
const t = useContext(I18nContext);
const [value, setValue] = useState('');
const [customSpendingCapText, setCustomSpendingCapText] = useState('');
const dispatch = useDispatch();
const value = useSelector(getCustomTokenAmount);
const [error, setError] = useState('');
const [showUseDefaultButton, setShowUseDefaultButton] = useState(
value !== String(dappProposedValue) && true,
);
const inputLogicEmptyStateText = t('inputLogicEmptyState');
const getInputTextLogic = (inputNumber) => {
@ -57,6 +65,10 @@ export default function CustomSpendingCap({
};
};
const [customSpendingCapText, setCustomSpendingCapText] = useState(
getInputTextLogic(value).description,
);
const handleChange = (valueInput) => {
let spendingCapError = '';
const inputTextLogic = getInputTextLogic(valueInput);
@ -71,9 +83,19 @@ export default function CustomSpendingCap({
setError('');
}
setValue(valueInput);
dispatch(setCustomTokenAmount(String(valueInput)));
};
useEffect(() => {
if (value !== String(dappProposedValue)) {
setShowUseDefaultButton(true);
}
}, [value, dappProposedValue]);
useEffect(() => {
passTheErrorText(error);
}, [error, passTheErrorText]);
const chooseTooltipContentText =
value > currentTokenBalance
? t('warningTooltipText', [
@ -100,7 +122,6 @@ export default function CustomSpendingCap({
onClick={(e) => {
e.preventDefault();
handleChange(currentTokenBalance);
setValue(currentTokenBalance);
}}
>
{t('max')}
@ -131,6 +152,7 @@ export default function CustomSpendingCap({
}
>
<FormField
numeric
dataTestId="custom-spending-cap-input"
autoFocus
wrappingLabelProps={{ as: 'div' }}
@ -151,21 +173,19 @@ export default function CustomSpendingCap({
error={error}
value={value}
titleDetail={
<button
className="custom-spending-cap__input--button"
type="link"
onClick={(e) => {
e.preventDefault();
if (value <= currentTokenBalance || error) {
showUseDefaultButton && (
<button
className="custom-spending-cap__input--button"
type="link"
onClick={(e) => {
e.preventDefault();
setShowUseDefaultButton(false);
handleChange(dappProposedValue);
setValue(dappProposedValue);
} else {
onEdit();
}
}}
>
{value > currentTokenBalance ? t('edit') : t('useDefault')}
</button>
}}
>
{t('useDefault')}
</button>
)
}
titleDetailWrapperProps={{ marginBottom: 2, marginRight: 0 }}
allowDecimals
@ -202,7 +222,7 @@ CustomSpendingCap.propTypes = {
*/
siteOrigin: PropTypes.string,
/**
* onClick handler for the Edit link
* Parent component's callback function passed in order to get the error text
*/
onEdit: PropTypes.func,
passTheErrorText: PropTypes.func,
};

View File

@ -17,8 +17,8 @@ export default {
siteOrigin: {
control: { type: 'text' },
},
onEdit: {
action: 'onEdit',
passTheErrorText: {
action: 'passTheErrorText',
},
},
args: {

View File

@ -21,6 +21,7 @@
position: absolute;
margin-top: 55px;
margin-inline-start: -75px;
z-index: 1;
}
}
@ -28,4 +29,10 @@
color: var(--color-error-default);
padding-inline-end: 60px;
}
input[type='number']::-webkit-inner-spin-button,
input[type='number']:hover::-webkit-inner-spin-button {
-webkit-appearance: none;
display: none;
}
}

View File

@ -14,77 +14,89 @@ export default function UpdateSnapPermissionList({
const t = useI18nContext();
const ApprovedPermissions = () => {
return Object.keys(approvedPermissions).map((approvedPermission) => {
const { label, rightIcon } = getPermissionDescription(
t,
approvedPermission,
);
const { date } = approvedPermissions[approvedPermission];
const formattedDate = formatDate(date, 'yyyy-MM-dd');
return (
<div className="approved-permission" key={approvedPermission}>
<i className="fas fa-check" />
<div className="permission-description">
{label}
<Typography
color={COLORS.TEXT_ALTERNATIVE}
className="permission-description-subtext"
boxProps={{ paddingTop: 1 }}
>
{t('approvedOn', [formattedDate])}
</Typography>
return Object.entries(approvedPermissions).map(
([permissionName, permissionValue]) => {
const { label, rightIcon } = getPermissionDescription(
t,
permissionName,
permissionValue,
);
const { date } = permissionValue;
const formattedDate = formatDate(date, 'yyyy-MM-dd');
return (
<div className="approved-permission" key={permissionName}>
<i className="fas fa-check" />
<div className="permission-description">
{label}
<Typography
color={COLORS.TEXT_ALTERNATIVE}
className="permission-description-subtext"
boxProps={{ paddingTop: 1 }}
>
{t('approvedOn', [formattedDate])}
</Typography>
</div>
{rightIcon && <i className={rightIcon} />}
</div>
{rightIcon && <i className={rightIcon} />}
</div>
);
});
);
},
);
};
const RevokedPermissions = () => {
return Object.keys(revokedPermissions).map((revokedPermission) => {
const { label, rightIcon } = getPermissionDescription(
t,
revokedPermission,
);
return (
<div className="revoked-permission" key={revokedPermission}>
<i className="fas fa-x" />
<div className="permission-description">
{label}
<Typography
color={COLORS.TEXT_ALTERNATIVE}
boxProps={{ paddingTop: 1 }}
className="permission-description-subtext"
>
{t('permissionRevoked')}
</Typography>
return Object.entries(revokedPermissions).map(
([permissionName, permissionValue]) => {
const { label, rightIcon } = getPermissionDescription(
t,
permissionName,
permissionValue,
);
return (
<div className="revoked-permission" key={permissionName}>
<i className="fas fa-x" />
<div className="permission-description">
{label}
<Typography
color={COLORS.TEXT_ALTERNATIVE}
boxProps={{ paddingTop: 1 }}
className="permission-description-subtext"
>
{t('permissionRevoked')}
</Typography>
</div>
{rightIcon && <i className={rightIcon} />}
</div>
{rightIcon && <i className={rightIcon} />}
</div>
);
});
);
},
);
};
const NewPermissions = () => {
return Object.keys(newPermissions).map((newPermission) => {
const { label, rightIcon } = getPermissionDescription(t, newPermission);
return (
<div className="new-permission" key={newPermission}>
<i className="fas fa-arrow-right" />
<div className="permission-description">
{label}
<Typography
color={COLORS.TEXT_ALTERNATIVE}
boxProps={{ paddingTop: 1 }}
className="permission-description-subtext"
>
{t('permissionRequested')}
</Typography>
return Object.entries(newPermissions).map(
([permissionName, permissionValue]) => {
const { label, rightIcon } = getPermissionDescription(
t,
permissionName,
permissionValue,
);
return (
<div className="new-permission" key={permissionName}>
<i className="fas fa-arrow-right" />
<div className="permission-description">
{label}
<Typography
color={COLORS.TEXT_ALTERNATIVE}
boxProps={{ paddingTop: 1 }}
className="permission-description-subtext"
>
{t('permissionRequested')}
</Typography>
</div>
{rightIcon && <i className={rightIcon} />}
</div>
{rightIcon && <i className={rightIcon} />}
</div>
);
});
);
},
);
};
return (

View File

@ -1,6 +1,7 @@
@import 'signature-request-footer/index';
@import 'signature-request-header/index';
@import 'signature-request-message/index';
@import 'signature-request-data/index';
.signature-request {
display: flex;

View File

@ -0,0 +1 @@
export { default } from './signature-request-data';

View File

@ -0,0 +1,26 @@
.signature-request-data {
&__node {
&__value {
white-space: pre-line;
overflow: hidden;
word-wrap: break-word;
&__address {
[dir='rtl'] & {
/*rtl:ignore*/
direction: ltr;
/*rtl:ignore*/
text-align: right;
span {
display: block;
/*rtl:ignore*/
direction: rtl;
}
}
}
}
}
}

View File

@ -0,0 +1,79 @@
import React from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { getMetaMaskIdentities, getAccountName } from '../../../../selectors';
import Address from '../../transaction-decoding/components/decoding/address';
import {
isValidHexAddress,
toChecksumHexAddress,
} from '../../../../../shared/modules/hexstring-utils';
import Box from '../../../ui/box';
import Typography from '../../../ui/typography';
import {
DISPLAY,
COLORS,
FONT_WEIGHT,
TYPOGRAPHY,
} from '../../../../helpers/constants/design-system';
export default function SignatureRequestData({ data }) {
const identities = useSelector(getMetaMaskIdentities);
return (
<Box className="signature-request-data__node">
{Object.entries(data).map(([label, value], i) => (
<Box
className="signature-request-data__node"
key={i}
paddingLeft={2}
display={
typeof value !== 'object' || value === null ? DISPLAY.FLEX : null
}
>
<Typography
as="span"
color={COLORS.TEXT_DEFAULT}
marginLeft={4}
fontWeight={
typeof value === 'object' ? FONT_WEIGHT.BOLD : FONT_WEIGHT.NORMAL
}
>
{label.charAt(0).toUpperCase() + label.slice(1)}:{' '}
</Typography>
{typeof value === 'object' && value !== null ? (
<SignatureRequestData data={value} />
) : (
<Typography
as="span"
color={COLORS.TEXT_DEFAULT}
marginLeft={4}
className="signature-request-data__node__value"
>
{isValidHexAddress(value, {
mixedCaseUseChecksum: true,
}) ? (
<Typography
variant={TYPOGRAPHY.H7}
color={COLORS.INFO_DEFAULT}
className="signature-request-data__node__value__address"
>
<Address
addressOnly
checksummedRecipientAddress={toChecksumHexAddress(value)}
recipientName={getAccountName(identities, value)}
/>
</Typography>
) : (
`${value}`
)}
</Typography>
)}
</Box>
))}
</Box>
);
}
SignatureRequestData.propTypes = {
data: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired,
};

View File

@ -1 +1 @@
export { default } from './signature-request-message.component';
export { default } from './signature-request-message';

View File

@ -1,75 +1,24 @@
.signature-request-message {
flex: 1 60%;
display: flex;
max-height: 231px;
flex-direction: column;
position: relative;
&__title {
@include H6;
font-weight: 500;
color: var(--color-text-alternative);
margin-left: 12px;
}
h2 {
@include H6;
flex: 1 1 0;
text-align: left;
border-bottom: 1px solid var(--color-border-default);
padding: 0.5rem;
margin: 0;
color: var(--color-text-alternative);
}
&--root {
&__root {
flex: 1 100%;
background-color: var(--color-background-alternative);
padding-bottom: 0.5rem;
overflow: auto;
padding-left: 12px;
padding-right: 12px;
@include screen-sm-min {
width: auto;
}
}
&--node,
&--node-leaf {
padding-left: 0.3rem;
&-label {
color: var(--color-text-alternative);
margin-left: 0.5rem;
}
&-value {
color: var(--color-text-default);
margin-left: 0.5rem;
white-space: pre-line;
overflow: hidden;
word-wrap: break-word;
}
}
&--node-leaf {
display: flex;
}
&__scroll-button {
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--color-border-default);
background: var(--color-background-alternative);
color: var(--color-icon-default);
position: absolute;
right: 24px;
right: 28px;
bottom: 12px;
border-radius: 50%;
height: 24px;
width: 24px;
cursor: pointer;
}
}

View File

@ -1,103 +0,0 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { debounce } from 'lodash';
import classnames from 'classnames';
export default class SignatureRequestMessage extends PureComponent {
static propTypes = {
data: PropTypes.object.isRequired,
onMessageScrolled: PropTypes.func,
setMessageRootRef: PropTypes.func,
messageRootRef: PropTypes.object,
messageIsScrollable: PropTypes.bool,
};
static contextTypes = {
t: PropTypes.func,
};
state = {
messageIsScrolled: false,
};
setMessageIsScrolled = () => {
if (!this.props.messageRootRef || this.state.messageIsScrolled) {
return;
}
const { scrollTop, offsetHeight, scrollHeight } = this.props.messageRootRef;
const isAtBottom = Math.round(scrollTop) + offsetHeight >= scrollHeight;
if (isAtBottom) {
this.setState({ messageIsScrolled: true });
this.props.onMessageScrolled();
}
};
onScroll = debounce(this.setMessageIsScrolled, 25);
renderNode(data) {
return (
<div className="signature-request-message--node">
{Object.entries(data).map(([label, value], i) => (
<div
className={classnames('signature-request-message--node', {
'signature-request-message--node-leaf':
typeof value !== 'object' || value === null,
})}
key={i}
>
<span className="signature-request-message--node-label">
{label}:{' '}
</span>
{typeof value === 'object' && value !== null ? (
this.renderNode(value)
) : (
<span className="signature-request-message--node-value">
{`${value}`}
</span>
)}
</div>
))}
</div>
);
}
renderScrollButton() {
return (
<div
onClick={() => {
this.setState({ messageIsScrolled: true });
this.props.onMessageScrolled();
this.props.messageRootRef.scrollTo(
0,
this.props.messageRootRef.scrollHeight,
);
}}
className="signature-request-message__scroll-button"
data-testid="signature-request-scroll-button"
>
<i className="fa fa-arrow-down" title={this.context.t('scrollDown')} />
</div>
);
}
render() {
const { data, messageIsScrollable } = this.props;
return (
<div onScroll={this.onScroll} className="signature-request-message">
{messageIsScrollable ? this.renderScrollButton() : null}
<div className="signature-request-message__title">
{this.context.t('signatureRequest1')}
</div>
<div
className="signature-request-message--root"
ref={this.props.setMessageRootRef}
>
{this.renderNode(data)}
</div>
</div>
);
}
}

View File

@ -0,0 +1,98 @@
import React, { useContext, useState } from 'react';
import PropTypes from 'prop-types';
import { debounce } from 'lodash';
import { I18nContext } from '../../../../contexts/i18n';
import Box from '../../../ui/box';
import Typography from '../../../ui/typography';
import {
DISPLAY,
ALIGN_ITEMS,
JUSTIFY_CONTENT,
COLORS,
FONT_WEIGHT,
FLEX_DIRECTION,
SIZES,
} from '../../../../helpers/constants/design-system';
import SignatureRequestData from '../signature-request-data';
export default function SignatureRequestMessage({
data,
onMessageScrolled,
setMessageRootRef,
messageRootRef,
messageIsScrollable,
}) {
const t = useContext(I18nContext);
const [messageIsScrolled, setMessageIsScrolled] = useState(false);
const setMessageIsScrolledAtBottom = () => {
if (!messageRootRef || messageIsScrolled) {
return;
}
const { scrollTop, offsetHeight, scrollHeight } = messageRootRef;
const isAtBottom = Math.round(scrollTop) + offsetHeight >= scrollHeight;
if (isAtBottom) {
setMessageIsScrolled(true);
onMessageScrolled();
}
};
return (
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
onScroll={debounce(setMessageIsScrolledAtBottom, 25)}
className="signature-request-message"
>
{messageIsScrollable ? (
<Box
display={DISPLAY.FLEX}
alignItems={ALIGN_ITEMS.CENTER}
justifyContent={JUSTIFY_CONTENT.CENTER}
borderColor={COLORS.BORDER_DEFAULT}
backgroundColor={COLORS.BACKGROUND_DEFAULT}
color={COLORS.ICON_DEFAULT}
onClick={() => {
setMessageIsScrolled(true);
onMessageScrolled();
messageRootRef?.scrollTo(0, messageRootRef?.scrollHeight);
}}
className="signature-request-message__scroll-button"
data-testid="signature-request-scroll-button"
>
<i className="fa fa-arrow-down" aria-label={t('scrollDown')} />
</Box>
) : null}
<Box
backgroundColor={COLORS.BACKGROUND_DEFAULT}
paddingBottom={3}
paddingTop={3}
paddingRight={3}
margin={2}
borderRadius={SIZES.XL}
borderColor={COLORS.BORDER_MUTED}
className="signature-request-message__root"
ref={setMessageRootRef}
>
<Typography
fontWeight={FONT_WEIGHT.BOLD}
color={COLORS.TEXT_DEFAULT}
marginLeft={4}
className="signature-request-message__title"
>
{t('signatureRequest1')}
</Typography>
<SignatureRequestData data={data} />
</Box>
</Box>
);
}
SignatureRequestMessage.propTypes = {
data: PropTypes.object.isRequired,
onMessageScrolled: PropTypes.func,
setMessageRootRef: PropTypes.func,
messageRootRef: PropTypes.object,
messageIsScrollable: PropTypes.bool,
};

View File

@ -0,0 +1,58 @@
import React from 'react';
import SignatureRequestMessage from './signature-request-message';
export default {
title: 'Components/App/SignatureRequestMessage',
id: __filename,
component: SignatureRequestMessage,
argTypes: {
data: { control: 'object' },
onMessageScrolled: { action: 'onMessageScrolled' },
setMessageRootRef: { action: 'setMessageRootRef' },
messageRootRef: { control: 'object' },
messageIsScrollable: { control: 'boolean' },
},
};
export const DefaultStory = (args) => {
return <SignatureRequestMessage {...args} />;
};
DefaultStory.storyName = 'Default';
DefaultStory.args = {
data: JSON.parse(
JSON.stringify({
domain: {
name: 'happydapp.website',
},
message: {
string: 'haay wuurl',
number: 42,
},
primaryType: 'Mail',
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
Group: [
{ name: 'name', type: 'string' },
{ name: 'members', type: 'Person[]' },
],
Mail: [
{ name: 'from', type: 'Person' },
{ name: 'to', type: 'Person[]' },
{ name: 'contents', type: 'string' },
],
Person: [
{ name: 'name', type: 'string' },
{ name: 'wallets', type: 'address[]' },
],
},
}),
),
messageIsScrollable: true,
};

View File

@ -8,6 +8,46 @@ import SignatureRequest from './signature-request.container';
describe('Signature Request', () => {
const mockStore = {
metamask: {
tokenList: {
'0x514910771af9ca656af840dff83e8264ecf986ca': {
address: '0x514910771af9ca656af840dff83e8264ecf986ca',
symbol: 'LINK',
decimals: 18,
name: 'ChainLink Token',
iconUrl:
'https://crypto.com/price/coin-data/icon/LINK/color_icon.png',
aggregators: [
'Aave',
'Bancor',
'CMC',
'Crypto.com',
'CoinGecko',
'1inch',
'Paraswap',
'PMM',
'Zapper',
'Zerion',
'0x',
],
occurrences: 12,
unlisted: false,
},
},
identities: {
'0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e': {
name: 'Account 2',
address: '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e',
},
},
addressBook: {
undefined: {
0: {
address: '0x39a4e4Af7cCB654dB9500F258c64781c8FbD39F0',
name: '',
isEns: false,
},
},
},
provider: {
type: 'rpc',
},

View File

@ -5,7 +5,10 @@ import copyToClipboard from 'copy-to-clipboard';
import { shortenAddress } from '../../../../../../helpers/utils/util';
import Identicon from '../../../../../ui/identicon';
import { useI18nContext } from '../../../../../../hooks/useI18nContext';
import { getAddressBook } from '../../../../../../selectors';
import {
getMetadataContractName,
getAddressBook,
} from '../../../../../../selectors';
import NicknamePopovers from '../../../../modals/nickname-popovers';
const Address = ({
@ -20,15 +23,25 @@ const Address = ({
const addressBook = useSelector(getAddressBook);
const addressBookEntryObject = addressBook.find(
(entry) => entry.address === checksummedRecipientAddress,
(entry) =>
entry.address.toLowerCase() === checksummedRecipientAddress.toLowerCase(),
);
const recipientNickname = addressBookEntryObject?.name;
const recipientMetadataName = useSelector((state) =>
getMetadataContractName(state, checksummedRecipientAddress),
);
const recipientToRender = addressOnly
? recipientNickname ||
? recipientName ||
recipientNickname ||
recipientMetadataName ||
recipientEns ||
shortenAddress(checksummedRecipientAddress)
: recipientNickname || recipientEns || recipientName || t('newContract');
: recipientName ||
recipientNickname ||
recipientMetadataName ||
recipientEns ||
t('newContract');
return (
<div

View File

@ -7,6 +7,9 @@
.tx-insight {
overflow-x: hidden;
display: flex;
align-items: center;
cursor: pointer;
&-loading {
display: flex;
@ -167,6 +170,12 @@
}
}
}
.tx-insight-component-address {
&__sender-icon {
padding-right: 8px;
}
}
}

View File

@ -41,6 +41,8 @@ export default class TransactionListItemDetails extends PureComponent {
onClose: PropTypes.func.isRequired,
recipientEns: PropTypes.string,
recipientAddress: PropTypes.string,
recipientName: PropTypes.string,
recipientMetadataName: PropTypes.string,
rpcPrefs: PropTypes.object,
senderAddress: PropTypes.string.isRequired,
tryReverseResolveAddress: PropTypes.func.isRequired,
@ -139,6 +141,8 @@ export default class TransactionListItemDetails extends PureComponent {
showRetry,
recipientEns,
recipientAddress,
recipientName,
recipientMetadataName,
senderAddress,
isEarliestNonce,
senderNickname,
@ -238,6 +242,8 @@ export default class TransactionListItemDetails extends PureComponent {
recipientEns={recipientEns}
recipientAddress={recipientAddress}
recipientNickname={recipientNickname}
recipientName={recipientName}
recipientMetadataName={recipientMetadataName}
senderName={senderNickname}
senderAddress={senderAddress}
onRecipientClick={() => {

View File

@ -8,6 +8,9 @@ import {
getIsCustomNetwork,
getRpcPrefsForCurrentProvider,
getEnsResolutionByAddress,
getAccountName,
getMetadataContractName,
getMetaMaskIdentities,
} from '../../../selectors';
import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils';
import TransactionListItemDetails from './transaction-list-item-details.component';
@ -20,6 +23,12 @@ const mapStateToProps = (state, ownProps) => {
recipientEns = getEnsResolutionByAddress(state, address);
}
const addressBook = getAddressBook(state);
const identities = getMetaMaskIdentities(state);
const recipientName = getAccountName(identities, recipientAddress);
const recipientMetadataName = getMetadataContractName(
state,
recipientAddress,
);
const getNickName = (address) => {
const entry = addressBook.find((contact) => {
@ -38,6 +47,8 @@ const mapStateToProps = (state, ownProps) => {
recipientNickname: recipientAddress ? getNickName(recipientAddress) : null,
isCustomNetwork,
blockExplorerLinkText: getBlockExplorerLinkText(state),
recipientName,
recipientMetadataName,
};
};

View File

@ -22,7 +22,7 @@ The `ButtonBase` accepts all props below as well as all [Box](/docs/ui-component
Use the `size` prop and the `SIZES` object from `./ui/helpers/constants/design-system.js`
to change the size of `ButtonBase`. Defaults to `SIZES.MD`
Optional: `BUTTON_SIZES` from `./button-base` object can be used instead of `SIZES`.
Optional: `BUTTON_BASE_SIZES` from `./button-base` object can be used instead of `SIZES`.
Possible sizes include:
@ -86,6 +86,20 @@ import { ButtonBase } from '../ui/component-library';
</ButtonBase>
```
### Href
When an `href` prop is passed it will change the element to an anchor(`a`) tag.
<Canvas>
<Story id="ui-components-component-library-button-base-button-base-stories-js--href" />
</Canvas>
```jsx
import { ButtonBase } from '../ui/component-library';
<ButtonBase href="/metamask">Anchor Element</ButtonBase>;
```
### Disabled
Use the boolean `disabled` prop to disable button

View File

@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ButtonBase should render button element correctly and match snapshot 1`] = `
<div>
<button
class="box mm-button mm-button--size-md box--padding-right-4 box--padding-left-4 box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center"
data-testid="button-base"
>
<span
class="box text mm-button__content text--body-md text--color-inherit box--gap-2 box--flex-direction-row box--justify-content-center box--align-items-center box--display-flex"
>
Button base
</span>
</button>
</div>
`;

View File

@ -1,6 +1,6 @@
import { SIZES } from '../../../helpers/constants/design-system';
export const BUTTON_SIZES = {
export const BUTTON_BASE_SIZES = {
SM: SIZES.SM,
MD: SIZES.MD,
LG: SIZES.LG,

View File

@ -15,14 +15,15 @@ import {
SIZES,
FLEX_DIRECTION,
} from '../../../helpers/constants/design-system';
import { BUTTON_SIZES } from './button.constants';
import { BUTTON_BASE_SIZES } from './button-base.constants';
export const ButtonBase = ({
as = 'button',
block,
children,
className,
size = BUTTON_SIZES.MD,
href,
size = BUTTON_BASE_SIZES.MD,
icon,
iconPositionRight,
loading,
@ -30,12 +31,12 @@ export const ButtonBase = ({
iconProps,
...props
}) => {
const Tag = props?.href ? 'a' : as;
const Tag = href ? 'a' : as;
return (
<Box
as={Tag}
paddingLeft={size === BUTTON_SIZES.AUTO ? 0 : 4}
paddingRight={size === BUTTON_SIZES.AUTO ? 0 : 4}
paddingLeft={size === BUTTON_BASE_SIZES.AUTO ? 0 : 4}
paddingRight={size === BUTTON_BASE_SIZES.AUTO ? 0 : 4}
className={classnames(
'mm-button',
`mm-button--size-${size}`,
@ -61,13 +62,13 @@ export const ButtonBase = ({
iconPositionRight ? FLEX_DIRECTION.ROW_REVERSE : FLEX_DIRECTION.ROW
}
gap={2}
variant={size === BUTTON_SIZES.AUTO ? TEXT.INHERIT : TEXT.BODY_MD}
variant={size === BUTTON_BASE_SIZES.AUTO ? TEXT.INHERIT : TEXT.BODY_MD}
color={TEXT_COLORS.INHERIT}
>
{icon && (
<Icon
name={icon}
size={size === BUTTON_SIZES.AUTO ? SIZES.AUTO : SIZES.SM}
size={size === BUTTON_BASE_SIZES.AUTO ? SIZES.AUTO : SIZES.SM}
{...iconProps}
/>
)}
@ -77,7 +78,7 @@ export const ButtonBase = ({
<Icon
className="mm-button__icon-loading"
name={ICON_NAMES.LOADING_FILLED}
size={size === BUTTON_SIZES.AUTO ? SIZES.AUTO : SIZES.MD}
size={size === BUTTON_BASE_SIZES.AUTO ? SIZES.AUTO : SIZES.MD}
/>
)}
</Box>
@ -105,6 +106,10 @@ ButtonBase.propTypes = {
* Boolean to disable button
*/
disabled: PropTypes.bool,
/**
* When an `href` prop is passed, ButtonBase will automatically change the root element to be an `a` (anchor) tag
*/
href: PropTypes.string,
/**
* Add icon to left side of button text passing icon name
* The name of the icon to display. Should be one of ICON_NAMES
@ -125,9 +130,9 @@ ButtonBase.propTypes = {
loading: PropTypes.bool,
/**
* The size of the ButtonBase.
* Possible values could be 'SIZES.AUTO', 'SIZES.SM', 'SIZES.MD', 'SIZES.LG',
* Possible values could be 'SIZES.AUTO', 'SIZES.SM'(32px), 'SIZES.MD'(40px), 'SIZES.LG'(48px),
*/
size: PropTypes.oneOf(Object.values(BUTTON_SIZES)),
size: PropTypes.oneOf(Object.values(BUTTON_BASE_SIZES)),
/**
* Addition style properties to apply to the button.
*/

View File

@ -9,7 +9,7 @@ import {
import Box from '../../ui/box/box';
import { ICON_NAMES } from '../icon';
import { Text } from '../text';
import { BUTTON_SIZES } from './button.constants';
import { BUTTON_BASE_SIZES } from './button-base.constants';
import { ButtonBase } from './button-base';
import README from './README.mdx';
@ -66,7 +66,7 @@ export default {
},
size: {
control: 'select',
options: Object.values(BUTTON_SIZES),
options: Object.values(BUTTON_BASE_SIZES),
},
marginTop: {
options: marginSizeControlOptions,
@ -145,6 +145,12 @@ export const As = (args) => (
</Box>
);
export const Href = (args) => <ButtonBase {...args}>Anchor Element</ButtonBase>;
Href.args = {
href: '/metamask',
};
export const Disabled = (args) => (
<ButtonBase {...args}>Disabled Button</ButtonBase>
);

View File

@ -1,17 +1,18 @@
/* eslint-disable jest/require-top-level-describe */
import { render } from '@testing-library/react';
import React from 'react';
import { BUTTON_SIZES } from './button.constants';
import { BUTTON_BASE_SIZES } from './button-base.constants';
import { ButtonBase } from './button-base';
describe('ButtonBase', () => {
it('should render button element correctly', () => {
it('should render button element correctly and match snapshot', () => {
const { getByTestId, getByText, container } = render(
<ButtonBase data-testid="button-base">Button base</ButtonBase>,
);
expect(getByText('Button base')).toBeDefined();
expect(container.querySelector('button')).toBeDefined();
expect(getByTestId('button-base')).toHaveClass('mm-button');
expect(container).toMatchSnapshot();
});
it('should render anchor element correctly', () => {
@ -40,23 +41,35 @@ describe('ButtonBase', () => {
it('should render with different size classes', () => {
const { getByTestId } = render(
<>
<ButtonBase size={BUTTON_SIZES.AUTO} data-testid={BUTTON_SIZES.AUTO} />
<ButtonBase size={BUTTON_SIZES.SM} data-testid={BUTTON_SIZES.SM} />
<ButtonBase size={BUTTON_SIZES.MD} data-testid={BUTTON_SIZES.MD} />
<ButtonBase size={BUTTON_SIZES.LG} data-testid={BUTTON_SIZES.LG} />
<ButtonBase
size={BUTTON_BASE_SIZES.AUTO}
data-testid={BUTTON_BASE_SIZES.AUTO}
/>
<ButtonBase
size={BUTTON_BASE_SIZES.SM}
data-testid={BUTTON_BASE_SIZES.SM}
/>
<ButtonBase
size={BUTTON_BASE_SIZES.MD}
data-testid={BUTTON_BASE_SIZES.MD}
/>
<ButtonBase
size={BUTTON_BASE_SIZES.LG}
data-testid={BUTTON_BASE_SIZES.LG}
/>
</>,
);
expect(getByTestId(BUTTON_SIZES.AUTO)).toHaveClass(
`mm-button--size-${BUTTON_SIZES.AUTO}`,
expect(getByTestId(BUTTON_BASE_SIZES.AUTO)).toHaveClass(
`mm-button--size-${BUTTON_BASE_SIZES.AUTO}`,
);
expect(getByTestId(BUTTON_SIZES.SM)).toHaveClass(
`mm-button--size-${BUTTON_SIZES.SM}`,
expect(getByTestId(BUTTON_BASE_SIZES.SM)).toHaveClass(
`mm-button--size-${BUTTON_BASE_SIZES.SM}`,
);
expect(getByTestId(BUTTON_SIZES.MD)).toHaveClass(
`mm-button--size-${BUTTON_SIZES.MD}`,
expect(getByTestId(BUTTON_BASE_SIZES.MD)).toHaveClass(
`mm-button--size-${BUTTON_BASE_SIZES.MD}`,
);
expect(getByTestId(BUTTON_SIZES.LG)).toHaveClass(
`mm-button--size-${BUTTON_SIZES.LG}`,
expect(getByTestId(BUTTON_BASE_SIZES.LG)).toHaveClass(
`mm-button--size-${BUTTON_BASE_SIZES.LG}`,
);
});

View File

@ -1,2 +1,2 @@
export { ButtonBase } from './button-base';
export { BUTTON_SIZES } from './button.constants';
export { BUTTON_BASE_SIZES } from './button-base.constants';

View File

@ -0,0 +1,144 @@
import { Story, Canvas, ArgsTable } from '@storybook/addon-docs';
import { ButtonIcon } from './button-icon';
# ButtonIcon
The `ButtonIcon` is used for icons associated with a user action.
<Canvas>
<Story id="ui-components-component-library-button-icon-button-icon-stories-js--default-story" />
</Canvas>
## Props
The `ButtonIcon` accepts all props below as well as all [Box](/docs/ui-components-ui-box-box-stories-js--default-story#props) component props
<ArgsTable of={ButtonIcon} />
### Icon<span style={{ color: 'red' }}>\*</span>
Use the required `icon` prop with `ICON_NAMES` object from `./ui/components/component-library/icon` to select icon.
<Canvas>
<Story id="ui-components-component-library-button-icon-button-icon-stories-js--icon" />
</Canvas>
```jsx
import { ButtonIcon } from '../ui/component-library';
import { ICON_NAMES } from '../icon';
<ButtonIcon icon={ICON_NAMES.CLOSE_OUTLINE} ariaLabel="Close" />;
```
### Size
Use the `size` prop and the `SIZES` object from `./ui/helpers/constants/design-system.js`
to change the size of `ButtonIcon`. Defaults to `SIZES.SM`
Optional: `BUTTON_ICON_SIZES` from `./button-icon` object can be used instead of `SIZES`.
Possible sizes include:
- `SIZES.SM` 24px
- `SIZES.LG` 32px
<Canvas>
<Story id="ui-components-component-library-button-icon-button-icon-stories-js--size" />
</Canvas>
```jsx
import { SIZES } from '../../../helpers/constants/design-system';
import { ButtonIcon } from '../ui/component-library';
<ButtonIcon size={SIZES.SM} icon={ICON_NAMES.CLOSE_OUTLINE} ariaLabel="Close"/>
<ButtonIcon size={SIZES.LG} icon={ICON_NAMES.CLOSE_OUTLINE} ariaLabel="Close"/>
```
### Aria Label
Use the `ariaLabel` prop to set the name of the ButtonIcon for proper accessibility
<Canvas>
<Story id="ui-components-component-library-button-icon-button-icon-stories-js--aria-label" />
</Canvas>
```jsx
import { ButtonIcon } from '../ui/component-library';
<ButtonIcon as="button" icon={ICON_NAMES.CLOSE_OUTLINE} ariaLabel="Close"/>
<ButtonIcon as="a" href="https://metamask.io/" target="_blank" icon={ICON_NAMES.EXPORT} color={COLORS.PRIMARY_DEFAULT} ariaLabel="Visit MetaMask.io"/>
```
### As
Use the `as` box prop to change the element of `ButtonIcon`. Defaults to `button`.
Button `as` options:
- `button`
- `a`
<Canvas>
<Story id="ui-components-component-library-button-icon-button-icon-stories-js--as" />
</Canvas>
```jsx
import { ButtonIcon } from '../ui/component-library';
<ButtonIcon as="button" icon={ICON_NAMES.CLOSE_OUTLINE} ariaLabel="Close"/>
<ButtonIcon as="a" href="https://metamask.io/" target="_blank" icon={ICON_NAMES.EXPORT} color={COLORS.PRIMARY_DEFAULT} ariaLabel="Visit MetaMask.io"/>
```
### Href
When an `href` prop is passed it will change the element to an anchor(`a`) tag.
<Canvas>
<Story id="ui-components-component-library-button-icon-button-icon-stories-js--href" />
</Canvas>
```jsx
import { ButtonIcon } from '../ui/component-library';
<ButtonIcon
href="https://metamask.io/"
target="_blank"
icon={ICON_NAMES.EXPORT}
color={COLORS.PRIMARY_DEFAULT}
ariaLabel="Visit MetaMask.io"
/>;
```
### Color
Use the `color` prop and the `COLORS` object to change the color of the `ButtonIcon`. Defaults to `COLORS.ICON_DEFAULT`.
<Canvas>
<Story id="ui-components-component-library-button-icon-button-icon-stories-js--color" />
</Canvas>
```jsx
import { ButtonIcon } from '../ui/component-library';
<ButtonIcon
icon={ICON_NAMES.EXPORT}
color={COLORS.PRIMARY_DEFAULT}
ariaLabel="Visit MetaMask.io"
/>;
```
### Disabled
Use the boolean `disabled` prop to disable button
<Canvas>
<Story id="ui-components-component-library-button-icon-button-icon-stories-js--disabled" />
</Canvas>
```jsx
import { ButtonIcon } from '../ui/component-library';
<ButtonIcon icon={ICON_NAMES.CLOSE_OUTLINE} disabled ariaLabel="Close" />;
```

View File

@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ButtonIcon should render button element correctly 1`] = `
<div>
<button
aria-label="add"
class="box mm-button-icon mm-button-icon--size-lg box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-icon-default box--background-color-transparent box--rounded-lg"
data-testid="button-icon"
>
<div
class="box icon icon--size-lg box--flex-direction-row box--color-inherit"
style="mask-image: url('./images/icons/icon-add-square-filled.svg;"
/>
</button>
</div>
`;

View File

@ -0,0 +1,6 @@
import { SIZES } from '../../../helpers/constants/design-system';
export const BUTTON_ICON_SIZES = {
SM: SIZES.SM,
LG: SIZES.LG,
};

View File

@ -0,0 +1,101 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import {
ALIGN_ITEMS,
BORDER_RADIUS,
COLORS,
DISPLAY,
JUSTIFY_CONTENT,
} from '../../../helpers/constants/design-system';
import Box from '../../ui/box';
import { Icon } from '../icon';
import { BUTTON_ICON_SIZES } from './button-icon.constants';
export const ButtonIcon = ({
ariaLabel,
as = 'button',
className,
color = COLORS.ICON_DEFAULT,
href,
size = BUTTON_ICON_SIZES.LG,
icon,
disabled,
iconProps,
...props
}) => {
const Tag = href ? 'a' : as;
return (
<Box
aria-label={ariaLabel}
as={Tag}
className={classnames(
'mm-button-icon',
`mm-button-icon--size-${size}`,
{
'mm-button-icon--disabled': disabled,
},
className,
)}
color={color}
disabled={disabled}
display={DISPLAY.INLINE_FLEX}
justifyContent={JUSTIFY_CONTENT.CENTER}
alignItems={ALIGN_ITEMS.CENTER}
borderRadius={BORDER_RADIUS.LG}
backgroundColor={COLORS.TRANSPARENT}
href={href}
{...props}
>
<Icon name={icon} size={size} {...iconProps} />
</Box>
);
};
ButtonIcon.propTypes = {
/**
* String that adds an accessible name for ButtonIcon
*/
ariaLabel: PropTypes.string.isRequired,
/**
* The polymorphic `as` prop allows you to change the root HTML element of the Button component between `button` and `a` tag
*/
as: PropTypes.string,
/**
* An additional className to apply to the ButtonIcon.
*/
className: PropTypes.string,
/**
* The color of the ButtonIcon component should use the COLOR object from
* ./ui/helpers/constants/design-system.js
*/
color: PropTypes.oneOf(Object.values(COLORS)),
/**
* Boolean to disable button
*/
disabled: PropTypes.bool,
/**
* When an `href` prop is passed, ButtonIcon will automatically change the root element to be an `a` (anchor) tag
*/
href: PropTypes.string,
/**
* The name of the icon to display. Should be one of ICON_NAMES
*/
icon: PropTypes.string.isRequired, // Can't set PropTypes.oneOf(ICON_NAMES) because ICON_NAMES is an environment variable
/**
* iconProps accepts all the props from Icon
*/
iconProps: PropTypes.object,
/**
* The size of the ButtonIcon.
* Possible values could be 'SIZES.SM', 'SIZES.LG',
*/
size: PropTypes.oneOf(Object.values(BUTTON_ICON_SIZES)),
/**
* ButtonIcon accepts all the props from Box
*/
...Box.propTypes,
};

View File

@ -0,0 +1,32 @@
.mm-button-icon {
--button-icon-size: var(--size, 24px);
--button-icon-opacity-hover: 0.5; // TODO: replace with design tokens
--button-icon-opacity-disabled: 0.3; // TODO: replace with design tokens
height: var(--button-icon-size);
width: var(--button-icon-size);
padding: 0;
cursor: pointer;
// ButtonIcon default states
&:active,
&:hover {
opacity: var(--button-icon-opacity-hover);
}
&--disabled,
&:disabled {
opacity: var(--button-icon-opacity-disabled);
cursor: not-allowed;
}
// ButtonIcon Sizes
&--size-sm {
--button-icon-size: 24px;
}
&--size-lg {
--button-icon-size: 32px;
}
}

View File

@ -0,0 +1,186 @@
import React from 'react';
import {
ALIGN_ITEMS,
COLORS,
DISPLAY,
FLEX_DIRECTION,
SIZES,
} from '../../../helpers/constants/design-system';
import Box from '../../ui/box/box';
import { ICON_NAMES } from '../icon';
import { BUTTON_ICON_SIZES } from './button-icon.constants';
import { ButtonIcon } from './button-icon';
import README from './README.mdx';
const marginSizeControlOptions = [
undefined,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
'auto',
];
export default {
title: 'Components/ComponentLibrary/ButtonIcon',
id: __filename,
component: ButtonIcon,
parameters: {
docs: {
page: README,
},
},
argTypes: {
ariaLabel: {
control: 'text',
},
as: {
control: 'select',
options: ['button', 'a'],
},
className: {
control: 'text',
},
color: {
control: 'select',
options: Object.values(COLORS),
},
disabled: {
control: 'boolean',
},
href: {
control: 'string',
},
icon: {
control: 'select',
options: Object.values(ICON_NAMES),
},
size: {
control: 'select',
options: Object.values(BUTTON_ICON_SIZES),
},
marginTop: {
options: marginSizeControlOptions,
control: 'select',
table: { category: 'box props' },
},
marginRight: {
options: marginSizeControlOptions,
control: 'select',
table: { category: 'box props' },
},
marginBottom: {
options: marginSizeControlOptions,
control: 'select',
table: { category: 'box props' },
},
marginLeft: {
options: marginSizeControlOptions,
control: 'select',
table: { category: 'box props' },
},
},
};
export const DefaultStory = (args) => <ButtonIcon {...args} />;
DefaultStory.args = {
icon: ICON_NAMES.CLOSE_OUTLINE,
ariaLabel: 'Close',
};
DefaultStory.storyName = 'Default';
export const Icon = (args) => (
<ButtonIcon {...args} icon={ICON_NAMES.CLOSE_OUTLINE} ariaLabel="Close" />
);
export const Size = (args) => (
<Box
display={DISPLAY.FLEX}
alignItems={ALIGN_ITEMS.BASELINE}
gap={1}
marginBottom={2}
>
<ButtonIcon
{...args}
size={SIZES.SM}
icon={ICON_NAMES.CLOSE_OUTLINE}
ariaLabel="Close"
/>
<ButtonIcon
{...args}
size={SIZES.LG}
color={COLORS.PRIMARY}
icon={ICON_NAMES.CLOSE_OUTLINE}
ariaLabel="Close"
/>
</Box>
);
export const AriaLabel = (args) => (
<>
<ButtonIcon
as="button"
icon={ICON_NAMES.CLOSE_OUTLINE}
ariaLabel="Close"
{...args}
/>
<ButtonIcon
as="a"
href="https://metamask.io/"
target="_blank"
color={COLORS.PRIMARY_DEFAULT}
icon={ICON_NAMES.EXPORT}
ariaLabel="Visit MetaMask.io"
{...args}
/>
</>
);
export const As = (args) => (
<Box display={DISPLAY.FLEX} flexDirection={FLEX_DIRECTION.ROW} gap={2}>
<ButtonIcon {...args} icon={ICON_NAMES.CLOSE_OUTLINE} />
<ButtonIcon
as="a"
href="#"
{...args}
color={COLORS.PRIMARY_DEFAULT}
icon={ICON_NAMES.EXPORT}
/>
</Box>
);
export const Href = (args) => (
<ButtonIcon icon={ICON_NAMES.EXPORT} {...args} target="_blank" />
);
Href.args = {
href: 'https://metamask.io/',
color: COLORS.PRIMARY_DEFAULT,
};
export const Color = (args) => (
<ButtonIcon {...args} icon={ICON_NAMES.CLOSE_OUTLINE} />
);
Color.args = {
color: COLORS.PRIMARY_DEFAULT,
};
export const Disabled = (args) => (
<ButtonIcon {...args} icon={ICON_NAMES.CLOSE_OUTLINE} />
);
Disabled.args = {
disabled: true,
};

View File

@ -0,0 +1,146 @@
/* eslint-disable jest/require-top-level-describe */
import { render } from '@testing-library/react';
import React from 'react';
import { COLORS } from '../../../helpers/constants/design-system';
import { BUTTON_ICON_SIZES } from './button-icon.constants';
import { ButtonIcon } from './button-icon';
describe('ButtonIcon', () => {
it('should render button element correctly', () => {
const { getByTestId, container } = render(
<ButtonIcon
data-testid="button-icon"
icon="add-square-filled"
ariaLabel="add"
/>,
);
expect(container.querySelector('button')).toBeDefined();
expect(getByTestId('button-icon')).toHaveClass('mm-button-icon');
expect(container).toMatchSnapshot();
});
it('should render anchor element correctly', () => {
const { getByTestId, container } = render(
<ButtonIcon
as="a"
data-testid="button-icon"
icon="add-square-filled"
ariaLabel="add"
/>,
);
expect(getByTestId('button-icon')).toHaveClass('mm-button-icon');
const anchor = container.getElementsByTagName('a').length;
expect(anchor).toBe(1);
});
it('should render anchor element correctly using href', () => {
const { getByTestId, getByRole } = render(
<ButtonIcon
href="/metamask"
data-testid="button-icon"
icon="add-square-filled"
ariaLabel="add"
/>,
);
expect(getByTestId('button-icon')).toHaveClass('mm-button-icon');
expect(getByRole('link')).toBeDefined();
});
it('should render with different size classes', () => {
const { getByTestId } = render(
<>
<ButtonIcon
icon="add-square-filled"
ariaLabel="add"
size={BUTTON_ICON_SIZES.SM}
data-testid={BUTTON_ICON_SIZES.SM}
/>
<ButtonIcon
icon="add-square-filled"
ariaLabel="add"
size={BUTTON_ICON_SIZES.LG}
data-testid={BUTTON_ICON_SIZES.LG}
/>
</>,
);
expect(getByTestId(BUTTON_ICON_SIZES.SM)).toHaveClass(
`mm-button-icon--size-${BUTTON_ICON_SIZES.SM}`,
);
expect(getByTestId(BUTTON_ICON_SIZES.LG)).toHaveClass(
`mm-button-icon--size-${BUTTON_ICON_SIZES.LG}`,
);
});
it('should render with different colors', () => {
const { getByTestId } = render(
<>
<ButtonIcon
icon="add-square-filled"
ariaLabel="add"
color={COLORS.ICON_DEFAULT}
data-testid={COLORS.ICON_DEFAULT}
/>
<ButtonIcon
icon="add-square-filled"
ariaLabel="add"
color={COLORS.ERROR_DEFAULT}
data-testid={COLORS.ERROR_DEFAULT}
/>
</>,
);
expect(getByTestId(COLORS.ICON_DEFAULT)).toHaveClass(
`box--color-${COLORS.ICON_DEFAULT}`,
);
expect(getByTestId(COLORS.ERROR_DEFAULT)).toHaveClass(
`box--color-${COLORS.ERROR_DEFAULT}`,
);
});
it('should render with added classname', () => {
const { getByTestId } = render(
<ButtonIcon
data-testid="classname"
className="mm-button-icon--test"
icon="add-square-filled"
ariaLabel="add"
/>,
);
expect(getByTestId('classname')).toHaveClass('mm-button-icon--test');
});
it('should render with different button states', () => {
const { getByTestId } = render(
<>
<ButtonIcon
disabled
data-testid="disabled"
icon="add-square-filled"
ariaLabel="add"
/>
</>,
);
expect(getByTestId('disabled')).toHaveClass(`mm-button-icon--disabled`);
expect(getByTestId('disabled')).toBeDisabled();
});
it('should render with icon', () => {
const { getByTestId } = render(
<ButtonIcon
data-testid="icon"
icon="add-square-filled"
ariaLabel="add"
iconProps={{ 'data-testid': 'button-icon' }}
/>,
);
expect(getByTestId('button-icon')).toBeDefined();
});
it('should render with aria-label', () => {
const { getByLabelText } = render(
<ButtonIcon icon="add-square-filled" ariaLabel="add" />,
);
expect(getByLabelText('add')).toBeDefined();
});
});

View File

@ -0,0 +1,2 @@
export { ButtonIcon } from './button-icon';
export { BUTTON_ICON_SIZES } from './button-icon.constants';

View File

@ -43,12 +43,12 @@ import { ButtonLink } from '../ui/component-library/button/button-link/button-li
<ButtonLink size={SIZES.LG} />
```
### Type
### Danger
Use the `type` prop and the `BUTTON_TYPES` object from `./ui/helpers/constants/design-system.js` to change the context of `ButtonLink`.
Use the `danger` boolean prop to change the `ButtonPrimary` to danger color.
<Canvas>
<Story id="ui-components-component-library-button-link-button-link-stories-js--type" />
<Story id="ui-components-component-library-button-link-button-link-stories-js--danger" />
</Canvas>
```jsx

View File

@ -139,7 +139,7 @@ export const Size = (args) => (
</>
);
export const Type = (args) => (
export const Danger = (args) => (
<Box display={DISPLAY.FLEX} gap={1}>
<ButtonLink {...args}>Normal</ButtonLink>
{/* Test Anchor tag to match exactly as button */}

View File

@ -53,7 +53,7 @@ describe('ButtonLink', () => {
);
});
it('should render with different types', () => {
it('should render as danger', () => {
const { getByTestId } = render(
<>
<ButtonLink danger data-testid="danger" />

View File

@ -41,12 +41,12 @@ import { ButtonPrimary } from '../ui/component-library/button/button-primary/but
<ButtonPrimary size={SIZES.LG} />
```
### Type
### Danger
Use the `type` prop and the `BUTTON_TYPES` object from `./ui/helpers/constants/design-system.js` to change the context of `ButtonPrimary`.
Use the `danger` boolean prop to change the `ButtonPrimary` to danger color.
<Canvas>
<Story id="ui-components-component-library-button-primary-button-primary-stories-js--type" />
<Story id="ui-components-component-library-button-primary-button-primary-stories-js--danger" />
</Canvas>
```jsx

View File

@ -28,7 +28,7 @@ ButtonPrimary.propTypes = {
*/
className: PropTypes.string,
/**
* Boolean to change button type to Danger when true
* When true, `ButtonPrimary` color becomes Danger.
*/
danger: PropTypes.bool,
/**

View File

@ -122,7 +122,7 @@ export const Size = (args) => (
</Box>
);
export const Type = (args) => (
export const Danger = (args) => (
<Box display={DISPLAY.FLEX} gap={1}>
<ButtonPrimary {...args}>Normal</ButtonPrimary>
{/* Test Anchor tag to match exactly as button */}

View File

@ -66,7 +66,7 @@ describe('ButtonPrimary', () => {
);
});
it('should render with different types', () => {
it('should render as danger', () => {
const { getByTestId } = render(
<>
<ButtonPrimary danger data-testid="danger" />

View File

@ -41,12 +41,12 @@ import { ButtonSecondary } from '../ui/component-library/button/button-secondary
<ButtonSecondary size={SIZES.LG} />
```
### Type
### Danger
Use the `type` prop and the `BUTTON_TYPES` object from `./ui/helpers/constants/design-system.js` to change the context of `ButtonSecondary`.
Use the `danger` boolean prop to change the `ButtonPrimary` to danger color.
<Canvas>
<Story id="ui-components-component-library-button-secondary-button-secondary-stories-js--type" />
<Story id="ui-components-component-library-button-secondary-button-secondary-stories-js--danger" />
</Canvas>
```jsx

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